本文的主角: ffmpeg
goav
。
之前写过一篇文章,实时展示摄像头内容 中有提到过一种实时展示摄像头内容的方式:集成ffmpeg相关的代码,并转换成图片传给web界面进行相应的展示,现补充下具体的实现。
ffmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案(百度百科)。
goav一个开源库,能够方便的在 go
中使用 C
的相应方法。要使用 goav
,还是得先熟悉 ffmpeg
的基本使用方式,这篇就不详述了,可以看大神雷霄骅的相关文章进行学习(真的赞)。
该仓库主要是通过 cgo
对 c
中的相关方法进行绑定,前人栽树后人乘凉,这里就直接拿来用了。但是该库集成的东西并不全面,若感觉不可靠,可自行实现。
这里主要使用 go
的 gin
框架作为 http
服务器,通过 websocket
实时地推送图片给 web
界面进行展示。
package api
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/giorgisio/goav/avcodec"
"github.com/giorgisio/goav/avdevice"
"github.com/giorgisio/goav/avformat"
"github.com/giorgisio/goav/avutil"
"github.com/giorgisio/goav/swscale"
"github.com/gorilla/websocket"
"image"
"image/color"
"image/jpeg"
"net/http"
"unsafe"
)
type wsParam struct {
Name string
}
// 用于测试实时读取摄像头的内容,并发送 base64 给界面
func ConnectWs(c *gin.Context) {
var param wsParam
if err := c.Bind(¶m); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, // disable origin check
}
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
defer conn.Close()
go tSendPic(conn)
_, _ , err = conn.ReadMessage()
if err != nil {
fmt.Println(err)
}
}
func tSendPic(conn *websocket.Conn) {
avdevice.AvdeviceRegisterAll()
avcodec.AvcodecRegisterAll()
pFormatCtx := avformat.AvformatAllocContext()
defer pFormatCtx.AvformatCloseInput()
inputFmt := avformat.AvFindInputFormat("dshow")
// 注意这里的第二个参数,需要改成自己的摄像头名称
if ret := avformat.AvformatOpenInput(&pFormatCtx, "video=USB2.0 PC CAMERA", inputFmt, nil); ret != 0 {
fmt.Println("Unable to open video USB2.0 PC CAMERA")
return
}
if pFormatCtx.AvformatFindStreamInfo(nil) < 0 {
fmt.Println("Unable to find stream")
return
}
videoindex := -1
nbStreams := int(pFormatCtx.NbStreams())
for i := 0; i < nbStreams; i++ {
streams := pFormatCtx.Streams()
if streams[i].Codec().GetCodecType() == avformat.AVMEDIA_TYPE_VIDEO {
videoindex = i
break
}
}
if videoindex == -1 {
fmt.Printf("Couldn't find a video stream.\n")
return
}
pCodecCtxOrig := pFormatCtx.Streams()[videoindex].Codec()
defer (*avcodec.Context)(unsafe.Pointer(pCodecCtxOrig)).AvcodecClose()
pCodec := avcodec.AvcodecFindDecoder(avcodec.CodecId(pCodecCtxOrig.GetCodecId()))
if pCodec == nil {
fmt.Println("没有找到解码器")
return
}
pCodecCtx := pCodec.AvcodecAllocContext3()
defer pCodecCtx.AvcodecClose()
if pCodecCtx.AvcodecCopyContext((*avcodec.Context)(unsafe.Pointer(pCodecCtxOrig))) != 0 {
fmt.Println("Couldn't copy codec context")
return
}
// Open codec
if pCodecCtx.AvcodecOpen2(pCodec, nil) < 0 {
fmt.Println("Could not open codec")
return
}
pFrame := avutil.AvFrameAlloc()
if pFrame == nil {
fmt.Println("Unable to allocate Frame")
return
}
defer avutil.AvFrameFree(pFrame)
pFrameRGB := avutil.AvFrameAlloc()
if pFrameRGB == nil {
fmt.Println("Unable to allocate RGB Frame")
return
}
defer avutil.AvFrameFree(pFrameRGB)
width := pCodecCtx.Width()
height := pCodecCtx.Height()
img_convert_ctx := swscale.SwsGetcontext(
width,
height,
(swscale.PixelFormat)(pCodecCtx.PixFmt()),
width,
height,
avcodec.AV_PIX_FMT_RGB24,
avcodec.SWS_BILINEAR,
nil,
nil,
nil,
)
numBytes := uintptr(avcodec.AvpictureGetSize(avcodec.AV_PIX_FMT_RGB24, width, height))
buffer := avutil.AvMalloc(numBytes)
defer avutil.AvFree(buffer)
avp := (*avcodec.Picture)(unsafe.Pointer(pFrameRGB))
avp.AvpictureFill((*uint8)(buffer), avcodec.AV_PIX_FMT_RGB24, width, height)
packet := avcodec.AvPacketAlloc()
defer packet.AvFreePacket()
for pFormatCtx.AvReadFrame(packet) >= 0 {
if packet.StreamIndex() != videoindex {
continue
}
var got_picture int
if ret := pCodecCtx.AvcodecDecodeVideo2((*avcodec.Frame)(unsafe.Pointer(pFrame)), &got_picture, packet); ret < 0 {
fmt.Println("Decode Error")
return
}
if got_picture > 0 {
swscale.SwsScale2(img_convert_ctx, avutil.Data(pFrame),
avutil.Linesize(pFrame), 0, height,
avutil.Data(pFrameRGB), avutil.Linesize(pFrameRGB))
// 直接存储
// 转成图片并传给界面
if buf, err := getJpegStreams(pFrameRGB, width, height); err == nil {
if er := conn.WriteMessage(websocket.TextMessage, buf); er != nil {
fmt.Println("write err: ", er)
return
}
}
} else {
fmt.Println("got_picture err: ", got_picture)
}
}
fmt.Println("exit video!!!")
}
func getJpegStreams(frame *avutil.Frame, width, height int) ([]byte, error) {
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
data0 := avutil.Data(frame)[0]
startPos := uintptr(unsafe.Pointer(data0)) + uintptr(y)*uintptr(avutil.Linesize(frame)[0])
//fmt.Println("startPos: ", startPos)
xxx := width * 3
for x := 0; x < width; x++ {
var pixel = make([]byte, 3)
for i := 0; i < 3; i++ {
element := *(*uint8)(unsafe.Pointer(startPos + uintptr(xxx)))
pixel[i] = element
xxx++
}
img.SetRGBA(x, y, color.RGBA{pixel[0], pixel[1], pixel[2], 0xff})
}
}
var b []byte
buffer := bytes.NewBuffer(b)
err := jpeg.Encode(buffer, img, nil)
if err != nil {
fmt.Println("jpeg.Encode err: ", err)
return nil, err
}
dst := make([]byte, base64.StdEncoding.EncodedLen(buffer.Len()))
base64.StdEncoding.Encode(dst, buffer.Bytes())
return dst, nil
}
html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ws获取图片数据展示title>
head>
<body>
<h1>测试服务器返回值h1>
<button onclick="openWs();">打开连接button>
<button onclick="closeWs();">关闭连接button>
<div><img id="img" />div>
<script>
var img = document.getElementById("img");
var ws;
function closeWs() {
if (ws != null) {
console.log("now close ws");
ws.close();
ws = null;
}
}
function openWs() {
if (ws != null) {
alert("已连接");
return
}
ws = new WebSocket("ws://127.0.0.1:4780/ws?name=grassto");
ws.onopen = function() {
console.log("open");
}
ws.onmessage = function(e){
//当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
// console.log(e);
img.src = "data:image/jpg;base64," + e.data;
}
ws.onclose = function(e){
//当客户端收到服务端发送的关闭连接请求时,触发onclose事件
console.log("close");
}
ws.onerror = function(e){
//如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
console.log(e);
}
}
script>
body>
html>
完整的代码可见gitee(平时写的一些测试都在这个仓库)。该示例的代码在 ginServer
目录下。
其实难点主要还是在于 ffmpeg
的使用,由于时间精力有限,并未进行深入研究,对音视频感兴趣的小伙伴可以去看雷神(雷霄骅)的博客。
感慨一下:知道雷神这个人,是在搜索 ffmpeg
的相关用法的时候出现的博客。翻看其博客内容,真是大为惊叹,文章通俗易懂,排版格式也看的很舒服,默默的给了个赞加关注。但是翻到最后评论的时候,才知道天妒英才,雷神已经走了,惋惜。
不求成为栽树人,但愿变成养树人。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)