go(goav) 中使用 ffmpeg 获取摄像头视频流,并转换成图片,发送给前端界面实时展示

go(goav) 中使用 ffmpeg 获取摄像头视频流,并转换成图片,发送给前端界面实时展示,第1张

本文的主角: ffmpeg goav
之前写过一篇文章,实时展示摄像头内容 中有提到过一种实时展示摄像头内容的方式:集成ffmpeg相关的代码,并转换成图片传给web界面进行相应的展示,现补充下具体的实现。

ffmpeg简介

ffmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案(百度百科)。

goav

一个开源库,能够方便的在 go 中使用 C 的相应方法。要使用 goav,还是得先熟悉 ffmpeg 的基本使用方式,这篇就不详述了,可以看大神雷霄骅的相关文章进行学习(真的赞)。
该仓库主要是通过 cgoc 中的相关方法进行绑定,前人栽树后人乘凉,这里就直接拿来用了。但是该库集成的东西并不全面,若感觉不可靠,可自行实现。

实际效果

直接上代码

这里主要使用 gogin 框架作为 http 服务器,通过 websocket 实时地推送图片给 web 界面进行展示。

go
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(&param); 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 的相关用法的时候出现的博客。翻看其博客内容,真是大为惊叹,文章通俗易懂,排版格式也看的很舒服,默默的给了个赞加关注。但是翻到最后评论的时候,才知道天妒英才,雷神已经走了,惋惜。
不求成为栽树人,但愿变成养树人。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996344.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存