Golang 게임에서 비디오 재생
비디오 게임에서 우리는 종종 플레이어에게 아름다운 시네마틱을 보여주어야 합니다. 종료를 위해 또는 재생 도중에 말입니다. 이를 위해서는 어떻게든 비디오 프레임을 로드하고 화면에 렌더링해야 합니다. Pixel game library 및 goav, 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 .
Reference
이 문제에 관하여(Golang 게임에서 비디오 재생), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/zergon321/playing-video-in-a-golang-game-531h텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)