前言
- 前面两篇文章讲了Gradio输出日志和ER-NeRF推理的图转存为ts文件
- 这篇就讲讲如何在Gradio中实时的播放服务器生成的m3u8文件
- 要让web上能播放,那首先要有个播放器,支持hls协议的web播放器,有video.js和hls.js等等,原生的video也能播,我这里选用hls.js
- 除了播放器,还得要让web前端上能寻找到m3u8文件,也就是要让gradio能够支持直接通过url加载到m3u8文件,这涉及到gradio的静态文件挂载
- 我们还需要让gradio能够把播放器在我们设计的webui上展示出来,而gradio的组件库里面是没有可用加载hls.js脚本的播放器组件的,这里就涉及到我们需要自定义组件
效果
- 没有加载播放器时界面:
- 加载了播放器之后的组件:
实现
首先,我们需要让gradio能够通过url加载到我们的m3u8静态文件。
为了实现这个目标,我们需要引入FastAPI,使用FastAPI挂载一个静态目录,然后在gradio中挂载FastAPI。
#设置静态目录
app = FastAPI()
dir = os.path.abspath(os.getcwd()) #将当前根目录作为静态目录
app.mount('/static', StaticFiles(directory=dir), 'static')
将gradio和FastAPI挂载在一起(当前gradio本身是需要支持queue的),参考:https://www.gradio.app/docs/mount_gradio_app
#将fastapi的静态目录和gradio挂载一起
page = page.queue()
app = gr.mount_gradio_app(app, page,path='/')
如此操作之后,就不能再使用gradio的.launch()方法来启动应用了
launch()启动之后没有FastAPI效果,挂载也就白挂了
这里使用Uvicorn来启动,关于Uvicorn,参考网上资料
uvicorn main-webui:app --port 7861 #端口好不指定默认为8000
启动之后,我们就可以通过http://127.0.0.1/static/xx/xx/xx.m3u8访问到视频索引文件了。
接下来就自定义一个gradio的video组件,让他把视频播放出来。
由于视频的m3u8文件的url是动态生成的,客户端只能监听服务器的日志消息,当出现某种特殊消息的时候,客户端就认为出现了m3u8的地址了,就解析那条消息,获取到视频索引文件url,然后构建video组件进行播放:
所以,首先改造我们之前的log_out函数,让他解析特定消息中的m3u8地址,然后向web上输出一条video的html代码字符串,我们的前端日志解析代码读取到这段html代码后,就会在界面上特定的位置,将这段代码渲染出来:
video_div = '<div id="videoDiv" style="width:100%;height:512px;"><video style="width:100%;height:100%;" id="videoDom" controls m3u8="[M3U8URL]"></video></div>'
def log_out(new_log):
print(new_log)
if new_log.find('##M3U8##SUCCESS:')>-1:
#根据日志输出的信息处理,提取到m3u8的地址
video = new_log.replace('##M3U8##SUCCESS:', '')
video = video_div.replace('[M3U8URL]', video)
time.sleep(0.7) #让客户端有时间来出来视频显示
print('输出video:'+video)
return video
return new_log
然后改造webui上日志输出功能代码:
原来的代码见:给数字人生成加上界面,基于ER-NeRF/RAD-NeRF/AD-NeRF,Gradio框架构建WEBUI,使用HLS流媒体,实现边推理边播放——之一:在WEBUI中实时输出服务器控制台日志:https://blog.csdn.net/AJian759447583/article/details/133990434
_script = '''
async()=>{
.......
//将hls.js的代码加入到页面上
......
//监控日志输出及显示
let video= document.querySelector("#videoDivCom");
let output = document.querySelector("#logDivText .border-none");
if(!output){
return false;
}
let show = document.querySelector('#logShowDiv .container')
show.style.height='200px'
show.style.overflowY='scroll'
show.innerHTML=""
Object.defineProperty(output, "value", {
set: function (log) {
if(log && log!=''){
if(log.startsWith('<div id="videoDiv"')){
video.innerHTML=log
}else{
show.innerHTML = show.innerHTML+'<br>'+log
show.scrollTop=show.scrollHeight
}
}
return this.textContent = log;
}
});
......
}
'''
#在page页面加载的时候,将自定义的js加载进去
page.load(_js=_script)
如此,页面上就可以动态加载到视频,实现服务器GPU一边推理CPU一边转视频,webui客户端一边播放的效果。
那,如果再将这个过程的各个时间点加快,选用更好更快的服务器,然后再把实时TTS整合进来,一个实时根据文本生成指定声音并自动推理出指定音频的数字人视频就搞定了!
当然,这其中肯定有一亿点点细节需要处理…
包括我上面的三篇文章,也仅仅是对整体思路的一个大致讲解,具体实现上肯定还有一亿点点细节需要处理,前面两篇文章:
- 给数字人生成加上界面,基于ER-NeRF/RAD-NeRF/AD-NeRF,Gradio框架构建WEBUI,使用HLS流媒体,实现边推理边播放——之一:在WEBUI中实时输出服务器控制台日志:https://blog.csdn.net/AJian759447583/article/details/133990434
- 给数字人生成加上界面,基于ER-NeRF/RAD-NeRF/AD-NeRF,Gradio框架构建WEBUI,使用HLS流媒体,实现边推理边播放——之二:将ndarray内存序列图直接转成ts格式视频:https://blog.csdn.net/AJian759447583/article/details/133994863
总结
- 整体思路很简单直白,技术上不存在鸿沟
- 具体细节上有太多需要探究的内容,往往每一步都被某个技术小细节卡死,然后各种探究各种尝试,最后一定是经过了N多个小时的试验之后才能解决——当然也有可能还是解决不了
- 本次运用到的陌生技术,以前没有接触过的、不熟悉的:
1:服务器日志实时输出到web客户端(花了大量时间实验)
2:python的gradio框架(按官网学习,很好上手)
3:ffmpeg命令(很复杂,脑壳痛…)
4:在python中使用ffmpeg的管道(很好理解,但是输入管道同时输出也是管道时,花了大量时间调研、尝试,最终也没解决)
5:gradio的queue和fastapi及静态文件(找了一会儿方案,尝试了个把小时)
6:hls流媒体协议(略读)
7:m3u8文件格式(细读)
8:ts格式媒体文件的封包协议细节(ffmpeg转成ts时,VLC播放器音画正常,hls.js在web上播放只有声音没有画面,解决此问题花了大量时间,尝试了各种参数,最终确定有效的为:vcodec=‘libx264’, r=25,output_ts_offset=ts_index * 5,hls_time=5,hls_segment_type=‘mpegts’) - 待继续优化的点:python中使用ffmpeg进行管道输入和管道输出。目前同时进行管道输入输出会出现程序挂起的现象,即使写入一帧数据读取一帧数据也是同样现象,暂时未解。