Golang实现服务器端图像流缩放功能探讨

Golang实现服务器端图像流缩放功能探讨 使用 gocv 将图像流式传输到我的 HTML5 页面中的 object 元素。

页面如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Cam Streaming with gocv</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link href="css/style.css" rel="stylesheet">
    </head>
    <body>
    <!-- <div id ="content"></div> -->
        <object data="http://localhost:8080/camera" width="300" height="200" alt="Cam streaming"></object>
    </body>
    <<script>
     /*   (function(){
                document.getElementById("content").innerHTML='<object type="text/html" data="http://localhost:8080/cam" ></object>';
        })();
        */
    </script>
</html>

我的 go 代码是:

// 此示例打开一个视频捕获设备,然后从中流式传输 MJPEG。
// 运行后,将浏览器指向命令行中传入的主机名/端口(例如 http://localhost:8080),您应该会看到实时视频流。
//
// 如何运行:
//
// mjpeg-streamer [camera ID] [host:port]
//
//		go get -u github.com/hybridgroup/mjpeg
// 		go run ./cmd/mjpeg-streamer/main.go 1 0.0.0.0:8080
//

package main

import (
	"fmt"
	"log"
	"net/http"

	_ "net/http/pprof"
	"opencv/mjpeg"

	"gocv.io/x/gocv"
)

var (
	deviceID int
	err      error
	webcam   *gocv.VideoCapture
	stream   *mjpeg.Stream
)

func main() {
	/*	if len(os.Args) < 3 {
			fmt.Println("How to run:\n\tmjpeg-streamer [camera ID] [host:port]")
			return
		}
	*/
	// 解析参数
	deviceID := 0   // os.Args[1]
	host := ":8080" //os.Args[2]

	// 打开摄像头
	webcam, err = gocv.OpenVideoCapture(deviceID)
	if err != nil {
		fmt.Printf("Error opening capture device: %v\n", deviceID)
		return
	}
	defer webcam.Close()

	// 创建 mjpeg 流
	stream = mjpeg.NewStream()

	// 开始捕获
	go mjpegCapture()

	fmt.Println("Capturing. Point your browser to " + host)

	// 启动 http 服务器
	http.Handle("/camera", stream)
	log.Fatal(http.ListenAndServe(host, nil))
}

func mjpegCapture() {
	img := gocv.NewMat()
	defer img.Close()

	for {
		if ok := webcam.Read(&img); !ok {
			fmt.Printf("Device closed: %v\n", deviceID)
			return
		}
		if img.Empty() {
			continue
		}

		buf, _ := gocv.IMEncode(".jpg", img)
		stream.UpdateJPEG(buf.GetBytes())
		buf.Close()
	}
}

opencv/mjpeg 中的流式传输函数是:

// Package mjpeg 实现了一个简单的 MJPEG 流式传输器。
//
// Stream 对象实现了 http.Handler 接口,允许像这样与 net/http 包一起使用:
//	stream = mjpeg.NewStream()
//	http.Handle("/camera", stream)
// 然后使用 stream.UpdateJPEG() 将新的 JPEG 帧推送到连接的客户端。
package mjpeg

import (
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"
)

// Stream 表示单个视频流。
type Stream struct {
	m             map[chan []byte]bool
	frame         []byte
	lock          sync.Mutex
	FrameInterval time.Duration
}

const boundaryWord = "MJPEGBOUNDARY"
const headerf = "\r\n" +
	"--" + boundaryWord + "\r\n" +
	"Content-Type: image/jpeg\r\n" +
	"Content-Length: %d\r\n" +
	"X-Timestamp: 0.000000\r\n" +
	"\r\n"

// ServeHTTP 以 MJPEG 流响应 HTTP 请求,实现了 http.Handler 接口。
func (s *Stream) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Println("Stream:", r.RemoteAddr, "connected")
	w.Header().Add("Content-Type", "multipart/x-mixed-replace;boundary="+boundaryWord)

	c := make(chan []byte)
	s.lock.Lock()
	s.m[c] = true
	s.lock.Unlock()

	for {
		time.Sleep(s.FrameInterval)
		b := <-c
		_, err := w.Write(b)
		if err != nil {
			break
		}
	}

	s.lock.Lock()
	delete(s.m, c)
	s.lock.Unlock()
	log.Println("Stream:", r.RemoteAddr, "disconnected")
}

// UpdateJPEG 将新的 JPEG 帧推送到客户端。
func (s *Stream) UpdateJPEG(jpeg []byte) {
	header := fmt.Sprintf(headerf, len(jpeg))
	if len(s.frame) < len(jpeg)+len(header) {
		s.frame = make([]byte, (len(jpeg)+len(header))*2)
	}

	copy(s.frame, header)
	copy(s.frame[len(header):], jpeg)

	s.lock.Lock()
	for c := range s.m {
		// 使用 Select 跳过正在休眠的流以丢弃帧。
		// 这可能需要进一步考虑。
		select {
		case c <- s.frame:
		default:
		}
	}
	s.lock.Unlock()
}

// NewStream 初始化并返回一个新的 Stream。
func NewStream() *Stream {
	return &Stream{
		m:             make(map[chan []byte]bool),
		frame:         make([]byte, len(headerf)),
		FrameInterval: 50 * time.Millisecond,
	}
}

我的输出如下:

enter image description here

我的问题是:

  1. 如何使流式传输的图像适应所选的 object 元素尺寸?
  2. 是否有办法将其流式传输到 video 元素?我尝试过但失败了。

注意:由于我不确定解决方案是在 go 端还是在 html5/JavaScript 端,所以我在这里发布。


更多关于Golang实现服务器端图像流缩放功能探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

@hyousef! 我不确定你是否能用HTML解决,但对于图像缩放,请使用libvips。 它更快且成本更低 🙂

我正在使用libvips处理图像。试试 https://github.com/davidbyttow/govips

更多关于Golang实现服务器端图像流缩放功能探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在将相机接收到的帧作为字节数组添加之前 如果添加边界词和内容类型,它将会正常工作

	fmt.Println("Capturing. Open http://" + host)

	// start http server
	http.HandleFunc("/video", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
		data := ""
		for {
			/*			fmt.Println("Frame ID: ", frame_id)
			 */mutex.Lock()
			data = "--frame\r\n  Content-Type: image/jpeg\r\n\r\n" + string(frame) + "\r\n\r\n"
			mutex.Unlock()
			time.Sleep(33 * time.Millisecond)
			w.Write([]byte(data))
		}
	})

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		t, _ := template.ParseFiles("index.html")
		t.Execute(w, "index")
	})

要实现服务器端图像流缩放以适应HTML object元素的尺寸,可以通过在Go端对图像进行实时缩放处理。以下是修改后的mjpegCapture函数,添加了图像缩放功能:

func mjpegCapture() {
    img := gocv.NewMat()
    defer img.Close()

    // 目标尺寸 - 根据你的object元素尺寸设置
    targetWidth := 300
    targetHeight := 200
    
    for {
        if ok := webcam.Read(&img); !ok {
            fmt.Printf("Device closed: %v\n", deviceID)
            return
        }
        if img.Empty() {
            continue
        }

        // 创建目标Mat用于缩放
        resized := gocv.NewMat()
        defer resized.Close()
        
        // 缩放图像到目标尺寸
        gocv.Resize(img, &resized, image.Pt(targetWidth, targetHeight), 0, 0, gocv.InterpolationLinear)
        
        // 编码为JPEG
        buf, _ := gocv.IMEncode(".jpg", resized)
        stream.UpdateJPEG(buf.GetBytes())
        buf.Close()
    }
}

对于流式传输到video元素,需要将MJPEG流转换为浏览器video元素支持的格式。以下是两种方法:

方法1:使用Canvas绘制MJPEG(HTML/JavaScript端):

<!DOCTYPE html>
<html>
<body>
    <canvas id="videoCanvas" width="300" height="200"></canvas>
    <script>
        const canvas = document.getElementById('videoCanvas');
        const ctx = canvas.getContext('2d');
        const img = new Image();
        
        // 创建图像流连接
        const streamUrl = 'http://localhost:8080/camera';
        const eventSource = new EventSource(streamUrl);
        
        eventSource.onmessage = function(event) {
            img.src = 'data:image/jpeg;base64,' + btoa(event.data);
            img.onload = function() {
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
            };
        };
    </script>
</body>
</html>

方法2:使用WebRTC实现真正的video元素流(需要更复杂的服务器端实现):

// 需要安装Pion WebRTC库
// go get github.com/pion/webrtc/v3

package main

import (
    "github.com/pion/webrtc/v3"
    "github.com/pion/webrtc/v3/pkg/media"
    "github.com/pion/webrtc/v3/pkg/media/ivfwriter"
)

func setupWebRTCStream() {
    // 创建PeerConnection
    config := webrtc.Configuration{}
    peerConnection, err := webrtc.NewPeerConnection(config)
    
    // 添加视频轨道
    videoTrack, err := webrtc.NewTrackLocalStaticSample(
        webrtc.RTPCodecCapability{MimeType: "video/VP8"},
        "video",
        "stream",
    )
    
    peerConnection.AddTrack(videoTrack)
    
    // 这里需要实现从gocv Mat到VP8帧的转换
    // 可以使用libvpx或ffmpeg进行编码
}

对于纯MJPEG流,video元素支持有限。更可靠的方法是使用MediaSource Extensions API:

<!DOCTYPE html>
<html>
<body>
    <video id="videoPlayer" width="300" height="200" autoplay></video>
    <script>
        const video = document.getElementById('videoPlayer');
        const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
        
        if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) {
            const mediaSource = new MediaSource();
            video.src = URL.createObjectURL(mediaSource);
            
            mediaSource.addEventListener('sourceopen', () => {
                const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
                
                // 从服务器获取流数据
                fetch('http://localhost:8080/camera')
                    .then(response => response.arrayBuffer())
                    .then(data => {
                        sourceBuffer.appendBuffer(data);
                        video.play();
                    });
            });
        }
    </script>
</body>
</html>

注意:要将MJPEG转换为MP4/H.264流,需要在Go端实现转码逻辑,可以使用C绑定调用FFmpeg或使用纯Go的编码器如codec/vpx

回到顶部