CGO:Go中用指针访问C数组及goav中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

