Golang 게임에서 비디오 재생

42191 단어 gamedevgo
문서의 새 버전을 보려면 계속 진행하십시오here. 아래에 설명된 방법은 더 이상 작동하지 않습니다(실제로 좋지도 않습니다).

비디오 게임에서 우리는 종종 플레이어에게 아름다운 시네마틱을 보여주어야 합니다. 종료를 위해 또는 재생 도중에 말입니다. 이를 위해서는 어떻게든 비디오 프레임을 로드하고 화면에 렌더링해야 합니다. Pixel game librarygoav, FFmpeg용 Golang 바인딩을 사용하여 이를 수행하는 방법은 다음과 같습니다.

초기 설정



먼저 OpenGL 항목을 렌더링하기 위한 GLFW 창을 만들어야 합니다.

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)
    }
}


비디오 프레임 얻기



이제 파일의 비디오 스트림에서 프레임을 가져와야 합니다. goav는 이를 수행하는 방법에 대한 example code을 제공합니다. Pixel 게임 라이브러리와 함께 작동하려면 프레임 디코딩 형식을 Pixel에서 사용하는 avcodec.AV_PIX_FMT_RGBA로 설정해야 합니다.

또한 디코딩된 프레임을 렌더러로 보내는 방법도 필요합니다. 이 작업을 위해 스레드로부터 안전한 프레임 버퍼 채널을 사용합니다. 먼저 크기를 지정해야 합니다.

const (
    FrameBufferSize = 1024
)


버퍼 크기가 클수록 디코더에서 렌더러로의 프레임 전송 속도가 빨라집니다. 이제 채널 생성:

frameBuffer := make(chan *pixel.PictureData, FrameBufferSize)


프레임 전송이 완료되면 채널을 닫아야 하지만 렌더러가 버퍼에서 모든 프레임을 가져왔는지도 확인해야 합니다. 프레임 버퍼를 닫는 코드는 다음과 같습니다.

go func() {
    for {
        if len(frameBuffer) <= 0 {
            close(frameBuffer)
            break
        }
    }
}()


비디오 프레임을 읽기 위한 전체 코드는 다음과 같습니다.

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
}


우리는 프레임 디코더를 위한 별도의 고루틴을 할당하여 자체 속도로 작업을 수행하고 모든 결과를 프레임 버퍼에 넣습니다.

비디오 프레임 변환



이제 추출된 비디오 프레임을 Pixel에 적합한 형식으로 가져와야 합니다. 먼저 원시 RGBA 바이트를 추출해야 합니다.

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)
}


이 바이트에서 *pixel.PictureData를 생성하기 위해 다음과 같은 간단한 코드를 사용합니다.

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
}


Pixel이 사진 데이터를 처리하는 방식이기 때문에 Pix 배열을 그 반대로 채워야 합니다.

비디오 프레임 렌더링



이제 비디오 프레임을 출력하기 위해 새로운 애니메이션 스프라이트를 만들어 보겠습니다.

videoSprite := pixel.NewSprite(nil, pixel.Rect{})
videoTransform := pixel.IM.Moved(pixel.V(
    float64(WindowWidth)/2, float64(WindowHeight)/2))
frameBuffer := readVideoFrames(os.Args[1])


비디오 파일의 경로는 명령줄 인수로 지정됩니다.

그런 다음 비디오 프레임 렌더링을 시작하겠습니다.

select {
    case frame, ok: = <-frameBuffer:
        if !ok {
            os.Exit(0)
        }

        if frame != nil {
            videoSprite.Set(frame, frame.Rect)
        }

    default:
}

videoSprite.Draw(win, videoTransform)


결과는 다음과 같습니다.



이 방법은 RGBA 바이트를 적절한 형식으로 변환하는 방법을 알고 있는 경우 Ebiten과 같은 다른 Golang 게임 엔진에 적용할 수 있습니다.

마음껏 즐기세요source code .

좋은 웹페이지 즐겨찾기