mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
Erroot v5 (#286)
* 插件数据库不同时,新建DB 对象赋值给插件 * MP4 plugin adds extraction, clips, images, compressed video, GOP clicp * remove mp4/util panic code
This commit is contained in:
1209
plugin/mp4/api_extract.go
Normal file
1209
plugin/mp4/api_extract.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,11 @@ var _ = m7s.InstallPlugin[MP4Plugin](m7s.PluginMeta{
|
||||
|
||||
func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
|
||||
return map[string]http.HandlerFunc{
|
||||
"/download/{streamPath...}": p.download,
|
||||
"/download/{streamPath...}": p.download,
|
||||
"/extractClip/{streamPath...}": p.extractClipToFileHandel,
|
||||
"/extractCompressed/{streamPath...}": p.extractCompressedVideoHandel,
|
||||
"/extractGop/{streamPath...}": p.extractGopVideoHandel,
|
||||
"/snap/{streamPath...}": p.snapHandel,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,54 @@ func (d *Demuxer) SeekTime(dts uint64) (sample *Sample, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 函数跳帧到dts 前面的第一个关键帧位置
|
||||
*
|
||||
* @param 参数名dts 跳帧位置
|
||||
*
|
||||
* @todo 待实现的功能或改进点 audioTrack 没有同步改进
|
||||
* @author erroot
|
||||
* @date 250614
|
||||
*
|
||||
**/
|
||||
func (d *Demuxer) SeekTimePreIDR(dts uint64) (sample *Sample, err error) {
|
||||
var audioTrack, videoTrack *Track
|
||||
for _, track := range d.Tracks {
|
||||
if track.Cid.IsAudio() {
|
||||
audioTrack = track
|
||||
} else if track.Cid.IsVideo() {
|
||||
videoTrack = track
|
||||
}
|
||||
}
|
||||
if videoTrack != nil {
|
||||
idx := videoTrack.SeekPreIDR(dts)
|
||||
if idx == -1 {
|
||||
return nil, errors.New("seek failed")
|
||||
}
|
||||
d.ReadSampleIdx[videoTrack.TrackId-1] = uint32(idx)
|
||||
sample = &videoTrack.Samplelist[idx]
|
||||
if audioTrack != nil {
|
||||
for i, sample := range audioTrack.Samplelist {
|
||||
if sample.Offset < int64(videoTrack.Samplelist[idx].Offset) {
|
||||
continue
|
||||
}
|
||||
d.ReadSampleIdx[audioTrack.TrackId-1] = uint32(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if audioTrack != nil {
|
||||
idx := audioTrack.Seek(dts)
|
||||
if idx == -1 {
|
||||
return nil, errors.New("seek failed")
|
||||
}
|
||||
d.ReadSampleIdx[audioTrack.TrackId-1] = uint32(idx)
|
||||
sample = &audioTrack.Samplelist[idx]
|
||||
} else {
|
||||
return nil, pkg.ErrNoTrack
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// func (d *Demuxer) decodeTRUN(trun *TrackRunBox) {
|
||||
// dataOffset := trun.Dataoffset
|
||||
// nextDts := d.currentTrack.StartDts
|
||||
|
||||
@@ -102,6 +102,28 @@ func (track *Track) Seek(dts uint64) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 函数跳帧到dts 前面的第一个关键帧位置
|
||||
*
|
||||
* @param 参数名dts 跳帧位置
|
||||
*
|
||||
* @author erroot
|
||||
* @date 250614
|
||||
*
|
||||
**/
|
||||
func (track *Track) SeekPreIDR(dts uint64) int {
|
||||
idx := 0
|
||||
for i, sample := range track.Samplelist {
|
||||
if track.Cid.IsVideo() && sample.KeyFrame {
|
||||
idx = i
|
||||
}
|
||||
if sample.Timestamp*1000/uint32(track.Timescale) > uint32(dts) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func (track *Track) makeEdtsBox() *ContainerBox {
|
||||
return CreateContainerBox(TypeEDTS, track.makeElstBox())
|
||||
}
|
||||
|
||||
338
plugin/mp4/util.go
Normal file
338
plugin/mp4/util.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package plugin_mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
mp4 "m7s.live/v5/plugin/mp4/pkg"
|
||||
"m7s.live/v5/plugin/mp4/pkg/box"
|
||||
)
|
||||
|
||||
func saveAsJPG(img image.Image, path string) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
opt := jpeg.Options{Quality: 90}
|
||||
return jpeg.Encode(file, img, &opt)
|
||||
}
|
||||
|
||||
func ExtractH264SPSPPS(extraData []byte) (sps, pps []byte, err error) {
|
||||
if len(extraData) < 7 {
|
||||
return nil, nil, fmt.Errorf("extradata too short")
|
||||
}
|
||||
|
||||
// 解析 SPS 数量 (第6字节低5位)
|
||||
spsCount := int(extraData[5] & 0x1F)
|
||||
offset := 6 // 当前解析位置
|
||||
|
||||
// 提取 SPS
|
||||
for i := 0; i < spsCount; i++ {
|
||||
if offset+2 > len(extraData) {
|
||||
return nil, nil, fmt.Errorf("invalid sps length")
|
||||
}
|
||||
spsLen := int(binary.BigEndian.Uint16(extraData[offset : offset+2]))
|
||||
offset += 2
|
||||
if offset+spsLen > len(extraData) {
|
||||
return nil, nil, fmt.Errorf("sps data overflow")
|
||||
}
|
||||
sps = extraData[offset : offset+spsLen]
|
||||
offset += spsLen
|
||||
}
|
||||
|
||||
// 提取 PPS 数量
|
||||
if offset >= len(extraData) {
|
||||
return nil, nil, fmt.Errorf("missing pps count")
|
||||
}
|
||||
ppsCount := int(extraData[offset])
|
||||
offset++
|
||||
|
||||
// 提取 PPS
|
||||
for i := 0; i < ppsCount; i++ {
|
||||
if offset+2 > len(extraData) {
|
||||
return nil, nil, fmt.Errorf("invalid pps length")
|
||||
}
|
||||
ppsLen := int(binary.BigEndian.Uint16(extraData[offset : offset+2]))
|
||||
offset += 2
|
||||
if offset+ppsLen > len(extraData) {
|
||||
return nil, nil, fmt.Errorf("pps data overflow")
|
||||
}
|
||||
pps = extraData[offset : offset+ppsLen]
|
||||
offset += ppsLen
|
||||
}
|
||||
return sps, pps, nil
|
||||
}
|
||||
|
||||
// 转换函数(支持动态插入参数集)
|
||||
func ConvertAVCCH264ToAnnexB(data []byte, extraData []byte, isFirst *bool) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
pos := 0
|
||||
|
||||
for pos < len(data) {
|
||||
if pos+4 > len(data) {
|
||||
break
|
||||
}
|
||||
nalSize := binary.BigEndian.Uint32(data[pos : pos+4])
|
||||
pos += 4
|
||||
nalStart := pos
|
||||
pos += int(nalSize)
|
||||
if pos > len(data) {
|
||||
break
|
||||
}
|
||||
nalu := data[nalStart:pos]
|
||||
nalType := nalu[0] & 0x1F
|
||||
|
||||
// 关键帧前插入SPS/PPS(仅需执行一次)
|
||||
if *isFirst && nalType == 5 {
|
||||
sps, pps, err := ExtractH264SPSPPS(extraData)
|
||||
if err != nil {
|
||||
//panic(err)
|
||||
return nil, err
|
||||
}
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
buf.Write(sps)
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
buf.Write(pps)
|
||||
//buf.Write(videoTrack.ExtraData)
|
||||
*isFirst = false // 仅首帧插入
|
||||
}
|
||||
|
||||
// 保留SEI单元(类型6)和所有其他单元
|
||||
if nalType == 5 || nalType == 6 { // IDR/SEI用4字节起始码
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
} else {
|
||||
buf.Write([]byte{0x00, 0x00, 0x01}) // 其他用3字节
|
||||
}
|
||||
buf.Write(nalu)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
/*
|
||||
H.264与H.265的AVCC格式差异
|
||||
VPS引入:H.265新增视频参数集(VPS),用于描述多层编码、时序等信息
|
||||
*/
|
||||
// 提取H.265的VPS/SPS/PPS(HEVCDecoderConfigurationRecord格式)
|
||||
func ExtractHEVCParams(extraData []byte) (vps, sps, pps []byte, err error) {
|
||||
if len(extraData) < 22 {
|
||||
return nil, nil, nil, errors.New("extra data too short")
|
||||
}
|
||||
|
||||
// HEVC的extradata格式参考ISO/IEC 14496-15
|
||||
offset := 22 // 跳过头部22字节
|
||||
if offset+2 > len(extraData) {
|
||||
return nil, nil, nil, errors.New("invalid extra data")
|
||||
}
|
||||
|
||||
numOfArrays := int(extraData[offset])
|
||||
offset++
|
||||
|
||||
for i := 0; i < numOfArrays; i++ {
|
||||
if offset+3 > len(extraData) {
|
||||
break
|
||||
}
|
||||
|
||||
naluType := extraData[offset] & 0x3F
|
||||
offset++
|
||||
count := int(binary.BigEndian.Uint16(extraData[offset:]))
|
||||
offset += 2
|
||||
|
||||
for j := 0; j < count; j++ {
|
||||
if offset+2 > len(extraData) {
|
||||
break
|
||||
}
|
||||
|
||||
naluSize := int(binary.BigEndian.Uint16(extraData[offset:]))
|
||||
offset += 2
|
||||
|
||||
if offset+naluSize > len(extraData) {
|
||||
break
|
||||
}
|
||||
|
||||
naluData := extraData[offset : offset+naluSize]
|
||||
offset += naluSize
|
||||
|
||||
// 根据类型存储参数集
|
||||
switch naluType {
|
||||
case 32: // VPS
|
||||
if vps == nil {
|
||||
vps = make([]byte, len(naluData))
|
||||
copy(vps, naluData)
|
||||
}
|
||||
case 33: // SPS
|
||||
if sps == nil {
|
||||
sps = make([]byte, len(naluData))
|
||||
copy(sps, naluData)
|
||||
}
|
||||
case 34: // PPS
|
||||
if pps == nil {
|
||||
pps = make([]byte, len(naluData))
|
||||
copy(pps, naluData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if vps == nil || sps == nil || pps == nil {
|
||||
return nil, nil, nil, errors.New("missing required parameter sets")
|
||||
}
|
||||
|
||||
return vps, sps, pps, nil
|
||||
}
|
||||
|
||||
// H.265的AVCC转Annex B
|
||||
func ConvertAVCCHEVCToAnnexB(data []byte, extraData []byte, isFirst *bool) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
pos := 0
|
||||
|
||||
// 首帧插入VPS/SPS/PPS
|
||||
if *isFirst {
|
||||
vps, sps, pps, err := ExtractHEVCParams(extraData)
|
||||
if err == nil {
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
buf.Write(vps)
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
buf.Write(sps)
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
buf.Write(pps)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 处理NALU
|
||||
for pos < len(data) {
|
||||
if pos+4 > len(data) {
|
||||
break
|
||||
}
|
||||
nalSize := binary.BigEndian.Uint32(data[pos : pos+4])
|
||||
pos += 4
|
||||
nalStart := pos
|
||||
pos += int(nalSize)
|
||||
if pos > len(data) {
|
||||
break
|
||||
}
|
||||
nalu := data[nalStart:pos]
|
||||
nalType := (nalu[0] >> 1) & 0x3F // H.265的NALU类型在头部的第2-7位
|
||||
|
||||
// 关键帧或参数集使用4字节起始码
|
||||
if nalType == 19 || nalType == 20 || nalType >= 32 && nalType <= 34 {
|
||||
buf.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||||
} else {
|
||||
buf.Write([]byte{0x00, 0x00, 0x01})
|
||||
}
|
||||
buf.Write(nalu)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// ffmpeg -hide_banner -i gop.mp4 -vf "select=eq(n\,15)" -vframes 1 -f image2 -pix_fmt bgr24 output.bmp
|
||||
func ProcessWithFFmpeg(samples []box.Sample, index int, videoTrack *mp4.Track) (image.Image, error) {
|
||||
// code := "h264"
|
||||
// if videoTrack.Cid == box.MP4_CODEC_H265 {
|
||||
// code = "hevc"
|
||||
// }
|
||||
cmd := exec.Command("ffmpeg",
|
||||
"-hide_banner",
|
||||
//"-f", code, //"h264" 强制指定输入格式为H.264裸流
|
||||
"-i", "pipe:0",
|
||||
"-vf", fmt.Sprintf("select=eq(n\\,%d)", index),
|
||||
"-vframes", "1",
|
||||
"-pix_fmt", "bgr24",
|
||||
"-f", "rawvideo",
|
||||
"pipe:1")
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
errOutput, _ := io.ReadAll(stderr)
|
||||
log.Printf("FFmpeg stderr: %s", errOutput)
|
||||
}()
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
log.Printf("cmd.Start失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
isFirst := true
|
||||
for _, sample := range samples {
|
||||
|
||||
if videoTrack.Cid == box.MP4_CODEC_H264 {
|
||||
annexb, _ := ConvertAVCCH264ToAnnexB(sample.Data, videoTrack.ExtraData, &isFirst)
|
||||
if _, err := stdin.Write(annexb); err != nil {
|
||||
log.Printf("写入失败: %v", err)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
annexb, _ := ConvertAVCCHEVCToAnnexB(sample.Data, videoTrack.ExtraData, &isFirst)
|
||||
if _, err := stdin.Write(annexb); err != nil {
|
||||
log.Printf("写入失败: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 读取原始RGB数据
|
||||
var buf bytes.Buffer
|
||||
if _, err = io.Copy(&buf, stdout); err != nil {
|
||||
log.Printf("读取失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err = cmd.Wait(); err != nil {
|
||||
log.Printf("cmd.Wait失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("ffmpeg 提取成功: data size:%v", buf.Len())
|
||||
|
||||
// 转换为image.Image对象
|
||||
data := buf.Bytes()
|
||||
//width, height := parseBMPDimensions(data)
|
||||
|
||||
width := int(videoTrack.Width)
|
||||
height := int(videoTrack.Height)
|
||||
|
||||
log.Printf("ffmpeg size: %v,%v", width, height)
|
||||
|
||||
//FFmpeg的 rawvideo 输出默认采用从上到下的扫描方式
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
//pos := (height-y-1)*width*3 + x*3
|
||||
pos := (y*width + x) * 3 // 关键修复:按行顺序读取
|
||||
img.Set(x, y, color.RGBA{
|
||||
R: data[pos+2],
|
||||
G: data[pos+1],
|
||||
B: data[pos],
|
||||
A: 255,
|
||||
})
|
||||
}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
Reference in New Issue
Block a user