前言
- 前文说到,我们的目标是要实现服务器一边推理一边播放视频的效果,并且知道服务器现在的推理状况
- 前文链接:http://t.csdnimg.cn/Gy6JU
- 服务器日志实时在webui中输出前文已经讲解,这里讲解如何边推边播
- 要实现边推边播,最重要的问题是要选择一种视频播放协议,使浏览器能直接播放没有完整生成的视频文件(不要直播推流那一套,毕竟服务器资源有限),其次要解决视频文件直接从内存中转存的问题
- 根据我们的要求,hls协议就能满足我们的要求:基于http协议,不需要单独再架设流媒体服务器,使用m3u8索引文件对视频文件进行概括,边播边加载,不会全部加载所有文件…
效果
实现
首先,我们需要根据hls协议,生成索引m3u8文件。
当前的状态是我们只有一个音频文件,视频文件是完全由机器推理生成的。
所以我们只能根据音频文件,自己拼凑一个m3u8文件:
默认我们每个ts文件是5s钟,那么根据音频文件的长度,就可以拼凑成一个完整的索引文件:
def create_m3u8_by_totalTime(totalTime: int, save_path_name: str):
'''根据总时长,按每5s一段,创建一个m3u8文件,返回每个ts文件的名字队列
:param totalTime 总时长,ms
:param save_path_name m3u8文件要存储的路径及名称,以.m3u8为后缀
:returns 返回每个ts的名字的队列对象及最后一个ts的时长(ms)
'''
dir = os.path.dirname(save_path_name)
if not os.path.exists(dir):
os.makedirs(dir)
segment = int(totalTime / 5000) if totalTime % 5000 == 0 else int(totalTime / 5000) + 1
tsQueue = queue.Queue(segment)
with open(save_path_name, 'w') as m3u8:
m3u8.write('#EXTM3U\n')
m3u8.write('#EXT-X-VERSION:3\n')
m3u8.write('#EXT-X-MEDIA-SEQUENCE:0\n') # 当播放打开M3U8时,以这个标签的值作为参考,播放对应的序列号的切片
m3u8.write('#EXT-X-ALLOW-CACHE:YES\n')
m3u8.write('#EXT-X-TARGETDURATION:6\n') # ts播放的最大时长,s
lastTime = -1
for i in range(segment):
if i + 1 == segment:
lastTime = totalTime % 5000
m3u8.write(f'#EXTINF:{5.0 if lastTime < 0 else lastTime / 1000},\n') # ts时长,注意有个逗号
m3u8.write(f'{i}.ts\n')
tsQueue.put(f'{i}.ts')
m3u8.write('#EXT-X-ENDLIST')
return tsQueue, lastTime
然后,我们需要将NeRF推理生成的图像帧转存为视频ts文件。
已知我们的视频都是25帧/秒,那5s的视频就应该是125帧,所以我们就按服务器每生成125帧图像的时候,就生成一次ts文件:
def create_ts_with_5sec(save_path:str,frame_ndarray:list,ts_index:int):
'''创建一个5s时长的ts文件'''
tmp_file = os.path.join(save_path,f'_tmp_quiet_{ts_index}.ts')
height,width,c = frame_ndarray[0].shape
print(f'======>视频width:{width},height:{height}')
#图像写入ts文件
process = (
ffmpeg.input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width, height),framerate=25)
.output(tmp_file, vcodec='libx264', r=25,output_ts_offset=ts_index * 5,hls_time=5,hls_segment_type='mpegts') #r:帧率,output_ts_offset:ts文件序列
.global_args('-y') # 覆盖同名文件
.run_async(pipe_stdin=True)
)
for frame in frame_ndarray:
process.stdin.write(frame.astype(np.uint8).tobytes())
time.sleep(0.01)
process.stdin.close()
process.wait()
通过ffmpeg-python包的api,指定输入为一个管道pipe,指定输出为mpegts,帧率为25,注意output_ts_offset要为一个正序增长的数。
通过process.stdin.write(frame.astype(np.uint8).tobytes())将内存中的每张图像都写入ffmepg管道中。
但这仅仅是写入了视频,还需要加上声音。
加声音这里有两种方式,一种是图像转视频输出的时候,不写入磁盘,直接就写入一个输出管道中,然后将管道中的数据再进行合并声音操作。
另一种是output的时候就存为一个临时文件,合并声音的时候再读取文件合并,最后输出一个正式文件。
目前第一种方案暂时未研究成功,这里记录第二种方案:
注意:这里音频读取的范围段是一个固定5s时长的范围
def seconds_to_time(seconds):
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
tmp_ts = ffmpeg.input(tmp_file)
audio = ffmpeg.input(audio_path_name, ss=seconds_to_time(ts_index * 5),t='5') #顺序读取5s的音频时长内容
joined = ffmpeg.concat(tmp_ts.video, audio, v=1, a=1).node
out = ffmpeg.output(joined[0], joined[1],tmp_file.replace('_tmp_quiet_',''),vcodec='libx264', r=25,output_ts_offset=ts_index * 5,hls_time=5,hls_segment_type='mpegts').global_args('-y') # 覆盖同名文件
out.run()
print('=====>ts生成完成!')
最终生成完成!
通过日志输出的方式,通知客户端m3u8文件的位置,让客户端加载m3u8文件,进行视频播放。
另外,数字人的图像推理,大多集中使用GPU,此处图像转存为ts格式视频文件,使用CPU,那这两个步骤可以并行执行。
思路就是通过一个队列,推理了一帧图像之后,就将图像的ndarray数组存入队列中,然后开启一个读取队列值的子线程,在子线程中取出图像数组,执行上面的生成ts文件的操作,就可以达到边推理边生成视频文件的效果。
相关代码我已放到gitee,有问题私信。
下一篇文章我们讲一下gradio怎么实时获取m3u8索引文件并实时播放