JS调用摄像头、实时视频流上传(一次不成功的试验)
思路
前端调用摄像头,获取视频流,从视频流中取一帧转图片,用websocket上传图片。
研究了一下发现了WebRTC这种技术,看到有博客说一些直播应用使用WebRTC将视频流发布到服务器上,然后用hls等技术在前端播放,但是我花了些时间还是没搞清楚WebRTC的原理,只能以后再尝试了。
1.前端代码
前端用的React+Typescript,所以这里贴出tsx代码。
逻辑比较简单,调用WebRTC的接口getUserMedia
获取视频流,然后把stream绑定到video元素上,接下来设个clock,每100ms抓一次视频帧(fps=10),将其画到canvas上后就可以调canvas.toDataUrl
把图片的位图数据编码成png格式,编码是必须的,因为编码后的数据一般比原始数据小一个数量级,节约带宽。也可以编码成其他格式,像jpeg或者webp,我对这些格式了解不多,可能压缩率最高的是webp。
其实到这一步已经出现问题了,800*800大小的png图片大小也近2M左右,这意味着一秒钟要上传
10 * 2M = 20M
的数据,十分不现实。没有测试过转webp格式,我觉得效果也不会太好,还是直接上传视频流比较好
export default class App extends React.Component<any, State> {
componentDidMount() {
let canvas = (this.refs.canvas as HTMLCanvasElement)
let canvasContext = canvas.getContext('2d') as CanvasRenderingContext2D
let video = (this.refs.video as HTMLVideoElement)
navigator.getUserMedia({video: true},
(stream: MediaStream) => {
video.srcObject = stream
video.play()
// upload stream to server
let ws = this.initWebSocket(
() => {
console.log('start send frames')
setInterval(() => {
canvasContext.drawImage(video, 0, 0, 800, 800)
// TODO: check if ws is closed
ws.send(canvas.toDataURL('image/png', 1))
}, 100) // 10 fps
}
)
},
(err: MediaStreamError) => console.error(err))
}
initWebSocket(onReady: () => void) {
let ws = new WebSocket(format('ws://%s:%d/ws', config.server.host, config.server.port))
ws.onopen = () => {
console.log('websocket opened')
onReady()
}
ws.onclose = () => console.log('websocket closed')
return ws
}
render() {
return (
<Layout style={{
display: 'block',
height: '100%', width: '100%'}}>
<Layout.Header>
<Typography.Title style={{color: 'white', textShadow: '1px 1px 3px black'}}>Face Detect</Typography.Title>
</Layout.Header>
<Layout.Content style={{width: '60%', height: '100%', margin: '0px auto'}}>
<video ref='video' width={800} height={800} style={{border: '1px black solid'}}></video>
<canvas ref='canvas' width={800} height={800} style={{display: 'block', border: '1px black solid'}}></canvas>
</Layout.Content>
</Layout>
)
}
}
2.后端代码
后端用go写的,篇幅有限,搭建http服务和websocket相关的代码就不贴出来了。功能就是把从websocket接收到的每一条消息,从DataUrl解码成Image对象。
import (
"bytes"
"encoding/base64"
"github.com/gorilla/websocket"
"image"
"image/png"
"net/http"
"github.com/Luncert/slog"
)
func serverWs(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error(err)
return
}
defer func() {
_ = conn.Close()
}()
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Error("read:", err)
break
}
// 将DataUrl解码成png字节数据
src := decodeDataUrl(message)
// 调image/png库解码出Image对象
img, err := png.Decode(bytes.NewReader(src))
if err != nil {
log.Fatal(err)
}
}
}
func decodeDataUrl(raw []byte) []byte {
// 去掉DataUrl中的元数据:从第一个字符到第一个','
i := 0
for raw[i] != ',' {
i++
}
i++
src := raw[i:]
// 调base64解码后续数据
maxLen := base64.StdEncoding.DecodedLen(len(src))
dst := make([]byte, maxLen)
if _, err := base64.StdEncoding.Decode(dst, src); err != nil {
log.Fatal(err)
}
return dst
}
这个方案到此为止了,接下来转向Rtmp