1、概述
这两天做了一个视频通信近实时字幕生成工具,前端通过浏览器打开摄像头,生成用户画面,根据用户的语音近实时自动生成字幕展示在画面下方。对于没有接触过音视频处理的我来说,刚开始还是有点懵的,虽然借助了 chatgpt,但是还是走了一段时间的弯路。不过花了大概一天时间还是比较完美的实现了,还是非常有成就感的。谨以此记录最终成功的版本的实现思路和实现过程,文末附带源码和源码启动过程。
2、环境准备
第四节「详细过程」中会有这些工具安装或者申请教程
ffmpeg,一个强大的视频处理工具,此次主要用它来实现视频转成音频。
阿里云 OSS bucket
阿里云 语音识别项目
本地 golang 运行环境
3、实现思路
-
前端使用 WebRTC 调起摄像头,与后端建立 websocket 连接,每隔三秒发送一段视频二进制流到后端;
-
后端将视频流保存到本地,使用 ffmpeg 将本地视频转换成音频;
-
把音频上传到阿里云 OSS 对象存储服务器中;
-
获取到音频的访问地址;调用阿里云的语音识别功能的 sdk 解析出音频对应的文字内容;
-
后端通过 websocket 把文字内容回传给前端,前端进行字幕展示。
3.1 提示
谨以此提示来降低心理压力,看起来此项目设计到前后端项目的开发和部署,但是其实不对此工具不用产生太大的压力,因为很多操作都有现成工具可以借用。
虽然此次项目需要同时开发前后端,但是对于此次工具的开发,不需要把前端部署到服务器,只需编写一个简单的 html,用浏览器渲染打开即可。
chatpgt 可以一定程度上加快我们的问题解决过程,但是也不要全信它的内容,亲身经历被它坑了好多次。
github 上已有一些优秀的开源项目,比如此次所借用的开源项目wxbool/video-srt ,大大加快了项目的开发速度。
前后端 websocket 交互的实现也比较简单,几行代码就可以搞定。
4、详细过程
4.1 工具准备和安装
4.1.1 安装 ffmpeg
在 Mac 上安装方式是 brew install ffmpeg
(其他操作系统可以自行寻找安装教程),安装过程可能比较久,我安装了大概 40 分钟。
安装完毕执行ffmpeg -version
,输出如下信息说明安装成功。
4.1.2 创建阿里云的 RPM 用户
登录阿里云账号后,访问 https://ram.console.aliyun.com/users,创建用户
随后在进入用户首页,点击「创建 AcessKey」,身份验证通过后,会创建一个 RAM用户的 AcessKey
和 AccessKey Secret
,立刻把两个参数记录下来,因为这个 AccessKey Secret
只在创建时显示,后续不支持查看。
4.1.3 创建阿里云 OSS bucket
访问 OSS对象存储,点击立即开通,然后创建 bucket ,由于后续语音识别会访问 bucket 中的文件,而语音识别只能访问到公开的资源,所以还需要设置 bucket 的开放范围为「公开」
给 RPM 用户添加完全控制权限,否则后面运行代码时 oss 会报错 StatusCode=403, ErrorCode=AccessDenied, ErrorMessage="You have no right to access this object because of bucket acl.",
4.1.4 创建阿里云智能语音交互项目
访问 录音文件识别,点击立即开通,然后创建项目,获取到项目AppKey,记录下来。
4.1.5 golang 环境安装
wget "https://studygolang.com/dl/golang/go1.18.3.darwin-amd64.tar.gz" -O go.tar.gz tar -C /usr/local -xfz go.tar.gz sudo echo 'export GOROOT=/usr/local/go' >> ~/.zshrc sudo echo 'export GOPATH=~/go' >> ~/.zshrc sudo echo 'export PATH=$GOPATH/bin:$GOROOT/bin:$PATH' >> ~/.zshrc source ~/.zshrc
执行 go version
输出版本信息说明安装成功
4.2 前端实现
只有一个 html 页面,通过 websocket 跟后端建立连接,进行数据交互,包含一些必要的 dom 节点,以及三个按钮。
javascript 脚本包含四部分,
- 第一部分是使用
navigator.mediaDevices.getUserMedia
打开用户的媒体设备,这个工具函数底层是通过 WebRTC 来实现的,随后跟后端建立 websocket 连接,使用 ws.onmessage 将获取到的后端消息添加上 dom 节点里 - 后三部分分别是三个按钮所绑定的函数,
- startGenerageSubtitle() ,绑定「启动字幕生成」按钮,功能是启动字幕的生成,函数内部会启动定时器,以三秒为周期,记录用户媒体的视频流,通过 websocket 对象发送到后端
- stopGenerageSubtitle(),绑定「停止生成字幕」按钮,功能是停止生成字幕,删除定时器,终止视频流的记录和发送。
- clearGenerageSubtitle(),绑定「清空字幕」按钮,清空 html 页面已有的字幕,清除 dom 元素的节点内容。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>字幕生成</title> </head> <body> <h1>椿辉近实时字幕生成工具</h1> <div> <div style="width: 700px; float: left; display: block"> <video id="video" autoplay></video> <button id="startButton" onclick="startGenerageSubtitle()">启动字幕生成</button> <button id="stopButton" onclick="stopGenerageSubtitle()">停止生成字幕</button> <button id="clearButton" onclick="clearGenerageSubtitle()">清空字幕</button> <p id="subtitle" style="text-align: center"></p> </div> <div style="width: 500px; float: left; display: block"> <h3>所有字幕</h3> <p id="result"></p> </div> </div> <script> const video = document.getElementById('video'); const result = document.getElementById('result'); const subtitle = document.getElementById('subtitle'); let ws = null; let mediaRecorder = null; let isRecording = false; let intervalId = null; // 获取用户媒体设备 navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then((stream) => { console.log("ws ===>", ws); ws = new WebSocket('ws://localhost:8080'); video.srcObject = stream; // 建立WebSocket连接 ws.onopen = function (){ console.log('===> WebSocket连接已经建立'); }; ws.onmessage = function(map) { let newP = document.createElement("p");//创建一个p标签 newP.innerText = map.data; result.appendChild(newP); subtitle.textContent = map.data; console.log(map.data); } }) .catch((err) => { console.log(err); }); // 启动字幕生成 function startGenerageSubtitle() { if (isRecording) { console.log('===> 已经在生成字幕'); return; } console.log('===> 开始生成字幕'); isRecording = true; // 获取用户媒体设备 navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then((stream) => { console.log("每3秒发送一次视频流数据") // 每3秒发送一次视频流数据 intervalId = setInterval(() => { const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=h264' }); mediaRecorder.addEventListener('dataavailable', (event) => { if (event.data.size > 0) { // 发送数据到后端 ws.send(event.data); } }); mediaRecorder.start(); // console.log("mediaRecorder.start===", mediaRecorder) setTimeout(() => { // console.log("mediaRecorder.stop===", mediaRecorder) mediaRecorder.stop(); }, 3000); }, 3000); }) .catch((err) => { console.log(err); }); } // 停止生成字幕 function stopGenerageSubtitle() { if (!isRecording) { console.log('===> 没有在生成字幕'); return; } console.log('===> 停止生成字幕'); isRecording = false; clearInterval(intervalId); // mediaRecorder.stop(); } // 清空字幕 function clearGenerageSubtitle() { subtitle.textContent = ""; result.innerHTML = "<p></p>"; } </script> </body> </html>
4.3 后端实现
借助了一个开源项目wxbool/video-srt ,这个开源项目可以把本地视频文件转成音频(通过 ffmpeg 实现),传到 OSS,并调用阿里的语音识别服务获取到字幕信息,我对他进行了一些改造,加入了服务的监听启动,随后使用 websocket 接收前端视频流,把视频流转存成本地视频文件,最后调用了 video-srt 的原有逻辑代码,完成了视频流字幕的提取生成。下面是一些关键代码。
项目根路径的 main.go 以 http 服务监听 8080 端口的形式启动服务,接口的回调处理函数是 RecognizeHandler2
RecognizeHandler2() 函数的代码逻辑在根路径下的 handler.go 中,用 websocket 来处理这个 http 接口,循环读取前端的视频流,把视频流存储成一个本地视频文件,调用 getSubtitle() 函数提取视频文件中的字幕, getSubtitle() 封装了原开源项目 wxbool/video-srt 的既有能力。
5、效果演示
关闭所有代理,否则调用阿里云的 SDK 可能超时,以及访问阿里云的 OSS 也可能超时。
5.1 启动前的参数设置
如果你想运行本项目,请先拉取 luoChunhui-1024/video-subtitle 项目到本地,把项目根目录的 config.ini 中的各种参数替换成刚才让你记录下来的那些阿里云配置。
#字幕相关设置 [srt] #智能分段处理:true(开启) false(关闭) intelligent_block=true #阿里云Oss对象服务配置 #文档:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.6.582.4e7858a85Dr5pA [aliyunOss] # OSS 对外服务的访问域名 endpoint=oss-cn-beijing.aliyuncs.com # 存储空间(Bucket)名称 bucketName=my-test-bucket-lch # 存储空间(Bucket 域名)地址 bucketDomain=my-test-bucket-lch.oss-cn-beijing.aliyuncs.com accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu #阿里云语音识别配置 #文档: [aliyunClound] # 在管控台中创建的项目Appkey,项目的唯一标识 appKey=5Xcb7kOlcSFAF248 accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu
5.2 启动运行
先在后端项目的根路径对项目进行编译,编译完成后在项目根路径会生成一个 output
可执行文件
go build -tags="recorder" -mod=mod -o output
直接执行这个可执行文件,即可启动后端服务
./output
随后通过浏览器打开项目中的 html/index.html
文件,过程中可能会询问获取麦克风和摄像头权限,允许即可,这样前端也启动完成了。
提示:Mac 可以直接在浏览器的地址栏输入 html 页面的绝对路径来打开 html 页面
5.3 效果展示
整体界面如下,由于本人样貌丑陋,为了不影响大家学习的心情,所以打了马赛克。
点击「启动字幕生成」按钮,则会开始每三秒给后端发送一次视频流,后端经过大概 6~8 秒的处理,把视频字幕返回给前端进行展示。所以字幕相较于画面中的语音,是有 8~9 秒的延迟的。
画面右侧会展示已有的字幕,画面最下方则仅展示最新的字幕。
点击「停止字幕生成」按钮,终止给后端发送视频流的定时器。但是点击启动字幕生成按钮可以再次启动定时器,进行字幕生成。
点击「清空字幕」按钮,会同时清空画面右侧的「所有字幕」和画面下方的最新字幕。
6、项目地址
github:https://github.com/luoChunhui-1024/video-subtitle
7、参考和致谢
特别感谢 wxbool/video-srt 项目,本项目后端的大部分都是直接借用了该项目,也特别感谢 chatgpt,虽然它提供的代码和方式坑了我很多次,但是仍旧给我提供了很大的帮助。
其他参考
WebRTC 从实战到未来!前端如何实现一个最简单的音视频通话?
WebRTC API:MediaDevices.getUserMedia()
实时语音识别-websocket API(百度的产品,这次其实没有用上)
实时语音转写 API 文档(讯飞的产品,这次也没用上)