Please proceed here for a new version of the article. The method described below doesn't work anymore (and it's not really good either).
In videogames, we often need to show the player beautiful cinematics, either for ending or in the middle of the playthrough. To do it, we need to somehow load video frames and render them on the screen. Here's how to do that using Pixel game library and goav, Golang bindings for FFmpeg.
Initial setup
First we need to create a GLFW window for rendering OpenGL stuff:
package main
import (
"fmt"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
colors "golang.org/x/image/colornames"
)
const (
// WindowWidth is the width of the window.
WindowWidth = 1280
// WindowHeight is the height of the window.
WindowHeight = 720
)
func run() {
// Create a new window.
cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks!",
Bounds: pixel.R(0, 0,
float64(WindowWidth), float64(WindowHeight)),
VSync: false,
}
win, err := pixelgl.NewWindow(cfg)
handleError(err)
fps := 0
perSecond := time.Tick(time.Second)
for !win.Closed() {
win.Clear(colors.White)
win.Update()
// Show FPS in the window title.
fps++
select {
case <-perSecond:
win.SetTitle(fmt.Sprintf("%s | FPS: %d",
cfg.Title, fps))
fps = 0
default:
}
}
}
func main() {
pixelgl.Run(run)
}
func handleError(err error) {
if err != nil {
panic(err)
}
}
Obtaining video frames
Now we need to obtain frames from the video stream of the file. goav provides an example code on how to do that. To make it work with Pixel game library, we need to set the frame decoding format to avcodec.AV_PIX_FMT_RGBA
which is used by Pixel.
We also need a way to send the decoded frames to the renderer. For this task we will use a thread-safe frame buffer channel. First we should specify its size:
const (
FrameBufferSize = 1024
)
The greater the buffer size, the faster the frame transfer from the decoder to the renderer. Now to the channel creation:
frameBuffer := make(chan *pixel.PictureData, FrameBufferSize)
When the frame transfer is complete, we need to close the channel, but we also have to make sure the renderer got all the frames from the buffer. So here's the code for closing the frame buffer:
go func() {
for {
if len(frameBuffer) <= 0 {
close(frameBuffer)
break
}
}
}()
The complete code for reading video frames is presented below:
func readVideoFrames(videoPath string) <-chan *pixel.PictureData {
// Create a frame buffer.
frameBuffer := make(chan *pixel.PictureData, FrameBufferSize)
go func() {
// Open a video file.
pFormatContext := avformat.AvformatAllocContext()
if avformat.AvformatOpenInput(&pFormatContext, videoPath, nil, nil) != 0 {
fmt.Printf("Unable to open file %s\n", videoPath)
os.Exit(1)
}
// Retrieve the stream information.
if pFormatContext.AvformatFindStreamInfo(nil) < 0 {
fmt.Println("Couldn't find stream information")
os.Exit(1)
}
// Dump information about the video to stderr.
pFormatContext.AvDumpFormat(0, videoPath, 0)
// Find the first video stream
for i := 0; i < int(pFormatContext.NbStreams()); i++ {
switch pFormatContext.Streams()[i].
CodecParameters().AvCodecGetType() {
case avformat.AVMEDIA_TYPE_VIDEO:
// Get a pointer to the codec context for the video stream
pCodecCtxOrig := pFormatContext.Streams()[i].Codec()
// Find the decoder for the video stream
pCodec := avcodec.AvcodecFindDecoder(avcodec.
CodecId(pCodecCtxOrig.GetCodecId()))
if pCodec == nil {
fmt.Println("Unsupported codec!")
os.Exit(1)
}
// Copy context
pCodecCtx := pCodec.AvcodecAllocContext3()
if pCodecCtx.AvcodecCopyContext((*avcodec.
Context)(unsafe.Pointer(pCodecCtxOrig))) != 0 {
fmt.Println("Couldn't copy codec context")
os.Exit(1)
}
// Open codec
if pCodecCtx.AvcodecOpen2(pCodec, nil) < 0 {
fmt.Println("Could not open codec")
os.Exit(1)
}
// Allocate video frame
pFrame := avutil.AvFrameAlloc()
// Allocate an AVFrame structure
pFrameRGB := avutil.AvFrameAlloc()
if pFrameRGB == nil {
fmt.Println("Unable to allocate RGB Frame")
os.Exit(1)
}
// Determine required buffer size and allocate buffer
numBytes := uintptr(avcodec.AvpictureGetSize(
avcodec.AV_PIX_FMT_RGBA, pCodecCtx.Width(),
pCodecCtx.Height()))
buffer := avutil.AvMalloc(numBytes)
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avp := (*avcodec.Picture)(unsafe.Pointer(pFrameRGB))
avp.AvpictureFill((*uint8)(buffer),
avcodec.AV_PIX_FMT_RGBA, pCodecCtx.Width(), pCodecCtx.Height())
// initialize SWS context for software scaling
swsCtx := swscale.SwsGetcontext(
pCodecCtx.Width(),
pCodecCtx.Height(),
(swscale.PixelFormat)(pCodecCtx.PixFmt()),
pCodecCtx.Width(),
pCodecCtx.Height(),
avcodec.AV_PIX_FMT_RGBA,
avcodec.SWS_BILINEAR,
nil,
nil,
nil,
)
// Read frames and save first five frames to disk
packet := avcodec.AvPacketAlloc()
for pFormatContext.AvReadFrame(packet) >= 0 {
// Is this a packet from the video stream?
if packet.StreamIndex() == i {
// Decode video frame
response := pCodecCtx.AvcodecSendPacket(packet)
if response < 0 {
fmt.Printf("Error while sending a packet to the decoder: %s\n",
avutil.ErrorFromCode(response))
}
for response >= 0 {
response = pCodecCtx.AvcodecReceiveFrame(
(*avcodec.Frame)(unsafe.Pointer(pFrame)))
if response == avutil.AvErrorEAGAIN ||
response == avutil.AvErrorEOF {
break
} else if response < 0 {
//fmt.Printf("Error while receiving a frame from the decoder: %s\n",
//avutil.ErrorFromCode(response))
//return
}
// Convert the image from its native format to RGB
swscale.SwsScale2(swsCtx, avutil.Data(pFrame),
avutil.Linesize(pFrame), 0, pCodecCtx.Height(),
avutil.Data(pFrameRGB), avutil.Linesize(pFrameRGB))
// Save the frame to the frame buffer.
frame := getFrameRGBA(pFrameRGB,
pCodecCtx.Width(), pCodecCtx.Height())
frameBuffer <- frame
}
}
// Free the packet that was allocated by av_read_frame
packet.AvFreePacket()
}
go func() {
for {
if len(frameBuffer) <= 0 {
close(frameBuffer)
break
}
}
}()
// Free the RGB image
avutil.AvFree(buffer)
avutil.AvFrameFree(pFrameRGB)
// Free the YUV frame
avutil.AvFrameFree(pFrame)
// Close the codecs
pCodecCtx.AvcodecClose()
(*avcodec.Context)(unsafe.Pointer(pCodecCtxOrig)).AvcodecClose()
// Close the video file
pFormatContext.AvformatCloseInput()
// Stop after saving frames of first video straem
break
default:
fmt.Println("Didn't find a video stream")
os.Exit(1)
}
}
}()
return frameBuffer
}
We allocate a separate goroutine for the frame decoder so it can do its work in its own pace and just put all the results in the frame buffer.
Converting video frames
Now we need to bring the extracted video frame to the form appropriate for Pixel. First we should extract raw RGBA bytes:
func getFrameRGBA(frame *avutil.Frame, width, height int) *pixel.PictureData {
pix := []byte{}
for y := 0; y < height; y++ {
data0 := avutil.Data(frame)[0]
buf := make([]byte, width*4)
startPos := uintptr(unsafe.Pointer(data0)) +
uintptr(y)*uintptr(avutil.Linesize(frame)[0])
for i := 0; i < width*4; i++ {
element := *(*uint8)(unsafe.Pointer(startPos + uintptr(i)))
buf[i] = element
}
pix = append(pix, buf...)
}
return pixToPictureData(pix, width, height)
}
To create *pixel.PictureData
out of these bytes, we'll use this simple code:
func pixToPictureData(pixels []byte, width, height int) *pixel.PictureData {
picData := pixel.MakePictureData(pixel.
R(0, 0, float64(width), float64(height)))
for y := height - 1; y >= 0; y-- {
for x := 0; x < width; x++ {
picData.Pix[(height-y-1)*width+x].R = pixels[y*width*4+x*4+0]
picData.Pix[(height-y-1)*width+x].G = pixels[y*width*4+x*4+1]
picData.Pix[(height-y-1)*width+x].B = pixels[y*width*4+x*4+2]
picData.Pix[(height-y-1)*width+x].A = pixels[y*width*4+x*4+3]
}
}
return picData
}
We need to fill the Pix
array vice versa because this is how Pixel treats picture data.
Rendering video frames
Now let's create a new animated sprite to output the video frames:
videoSprite := pixel.NewSprite(nil, pixel.Rect{})
videoTransform := pixel.IM.Moved(pixel.V(
float64(WindowWidth)/2, float64(WindowHeight)/2))
frameBuffer := readVideoFrames(os.Args[1])
The path to the video file is specified as a command line argument.
Then let's start rendering the video frames:
select {
case frame, ok: = <-frameBuffer:
if !ok {
os.Exit(0)
}
if frame != nil {
videoSprite.Set(frame, frame.Rect)
}
default:
}
videoSprite.Draw(win, videoTransform)
Here's the result:
This method can be adapted for other Golang game engines like Ebiten if you know the way to convert RGBA bytes to the appropriate form.
Enjoy the full source code.
Top comments (2)
but it no sound
I wrote a new article on this topic. It tells how to play not only video but also sound from media files. medium.com/@maximgradan/playing-vi...