You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CGO:Go中用指针访问C数组及goav中AVFrame数据指针操作咨询

在Go的CGO中访问C数组及AVFrame数据的实战指南

刚好我之前折腾过FFmpeg+Go的视频播放器项目,对CGO操作C指针、AVFrame数据提取这块还算熟悉,给你一步步拆解清楚,从基础原理到实际代码都有,应该能帮你搞定带转场效果的播放器开发~

一、先搞懂CGO里怎么访问C数组

C数组到Go里会变成*C.type的指针(比如C的int[]对应Go的*C.int),但Go不能直接像C那样用arr[i]索引,得用两种常用方法:

1. 用C.GoBytes转Go切片(最安全,适合字节数组)

如果是字节类型的C数组(比如FFmpeg里的像素数据),直接用C.GoBytes把C指针转成Go的[]byte,不用手动管内存:

// 假设C端有个unsigned char* c_data,长度是totalBytes
goSlice := C.GoBytes(unsafe.Pointer(c_data), C.int(totalBytes))
// 之后就像普通Go切片一样读写就行

要是C数组是动态分配的,记得让C端负责释放(FFmpeg的内存别用Go的C.free处理,后面会讲)。

2. 用unsafe包转成Go切片(通用类型)

如果是其他类型的数组(比如int、自定义结构体),可以用unsafe把C指针转成Go的切片头,就能直接索引:

import "unsafe"

// 比如C的int* c_arr,长度是n
// 这里[1<<30]是个超大数组类型,Go不支持动态长度数组,所以用这个上限,再切片取前n个
goSlice := (*[1 << 30]C.int)(unsafe.Pointer(c_arr))[:n:n]

⚠️ 划重点:一定要确保C数组的长度确实是n,越界直接崩,千万要谨慎!

二、实战:从AVFrame里提取像素数据

回到你的场景,goav里的AVFrame,它的data是uint8_t*[8]的指针数组(对应YUV的Y、U、V等通道),linesize是int[8],记录每个通道每行的字节数(因为FFmpeg会做内存对齐,linesize可能比实际像素宽度×通道字节数大)。

1. 提取YUV三通道数据

以最常用的YUV420P格式为例,代码大概是这样:

import (
    "fmt"
    "unsafe"
    "github.com/giorgisio/goav/avutil"
)

func extractAVFrameYUV(frame *avutil.AVFrame) ([][]byte, error) {
    var yuvChannels [][]byte
    // YUV420P只有前3个通道有效:Y(全高)、U(半高)、V(半高)
    for i := 0; i < 3; i++ {
        if frame.Data[i] == nil {
            break
        }
        // 计算当前通道的高度:Y通道是frame.Height,U/V是Height/2
        channelHeight := frame.Height
        if i != 0 {
            channelHeight /= 2
        }
        // 总字节数 = linesize[i] × 通道高度
        totalBytes := int(frame.Linesize[i]) * channelHeight
        // 转成Go切片
        channelData := C.GoBytes(unsafe.Pointer(frame.Data[i]), C.int(totalBytes))
        yuvChannels = append(yuvChannels, channelData)
    }
    if len(yuvChannels) != 3 {
        return nil, fmt.Errorf("invalid YUV frame channels")
    }
    return yuvChannels, nil
}

2. 把YUV数据写入文件

拿到Y、U、V三个切片后,直接用Go的os包写入就行,比如保存成原始YUV文件:

import "os"

func saveYUVToFile(y, u, v []byte, filename string) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    // 按Y→U→V的顺序写入
    if _, err = f.Write(y); err != nil {
        return err
    }
    if _, err = f.Write(u); err != nil {
        return err
    }
    if _, err = f.Write(v); err != nil {
        return err
    }
    return nil
}

三、转OpenGL纹理的两种思路

要做视频转场,得把YUV数据转成OpenGL纹理,有两种常用方式:

1. 先转成RGBA格式(简单易上手)

用FFmpeg的sws_scale(goav里的swscale包)把YUV420P转成RGBA,再把RGBA数据传给OpenGL:

import (
    "fmt"
    "unsafe"
    "github.com/giorgisio/goav/avutil"
    "github.com/giorgisio/goav/swscale"
)

func convertYUVToRGBA(frame *avutil.AVFrame, width, height int) ([]byte, error) {
    // 创建格式转换上下文
    swsCtx := swscale.SwsGetContext(
        width, height, avutil.AV_PIX_FMT_YUV420P,
        width, height, avutil.AV_PIX_FMT_RGBA,
        swscale.SWS_BILINEAR, nil, nil, nil,
    )
    if swsCtx == nil {
        return nil, fmt.Errorf("failed to create sws context")
    }
    defer swscale.SwsFreeContext(swsCtx)

    // 分配RGBA帧内存
    rgbaFrame := avutil.AvFrameAlloc()
    if rgbaFrame == nil {
        return nil, fmt.Errorf("failed to alloc rgba frame")
    }
    defer avutil.AvFrameFree(&rgbaFrame)
    rgbaFrame.SetWidth(width)
    rgbaFrame.SetHeight(height)
    rgbaFrame.SetFormat(avutil.AV_PIX_FMT_RGBA)
    if avutil.AvFrameGetBuffer(rgbaFrame, 32) != 0 {
        return nil, fmt.Errorf("failed to get rgba frame buffer")
    }

    // 执行格式转换
    swscale.SwsScale(
        swsCtx, frame.Data(), frame.Linesize(), 0, height,
        rgbaFrame.Data(), rgbaFrame.Linesize(),
    )

    // 提取RGBA数据
    totalBytes := width * height * 4 // RGBA每个像素4字节
    rgbaData := C.GoBytes(unsafe.Pointer(rgbaFrame.Data()[0]), C.int(totalBytes))
    return rgbaData, nil
}

之后把rgbaData传给OpenGL创建纹理:

// 假设已经初始化好OpenGL上下文和纹理ID
gl.BindTexture(gl.TEXTURE_2D, textureID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, rgbaData)

2. 直接上传YUV三通道纹理(性能更高)

这种方式不用转格式,直接把Y、U、V三个通道分别上传成三个纹理,然后在片元着色器里做YUV到RGB的转换,适合实时播放(减少格式转换的开销)。大概步骤:

  • 创建三个2D纹理,分别对应Y、U、V
  • 把Y通道数据上传到第一个纹理(格式GL_RED),U/V上传到另外两个(格式GL_RED)
  • 片元着色器里采样三个纹理,用YUV转RGB的公式计算最终颜色值

这种方式性能更好,但需要懂点GLSL着色器的知识,适合追求性能的场景。

几个关键坑要避开

  • 内存管理:AVFrame的内存是FFmpeg分配的,千万别用Go的free去释放,要用avutil.AvFrameFree或者avutil.AvFrameUnref,否则会内存泄漏或者崩溃。
  • linesize的坑:FFmpeg的linesize是对齐后的字节数,别用width×通道字节数来算,一定要用linesize[i],不然会读错数据。
  • Windows编译问题:Windows下用goav要注意CGO的环境,比如用MinGW编译,或者配置好MSVC的环境变量,不然会出现链接错误,找不到FFmpeg的库。

内容的提问来源于stack exchange,提问作者nevernew

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:18:02