1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00
vcmi/client/CVideoHandler.cpp

485 lines
11 KiB
C++
Raw Normal View History

/*
* CVideoHandler.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CVideoHandler.h"
#include "CMT.h"
#include "gui/CGuiHandler.h"
#include "eventsSDL/InputHandler.h"
#include "gui/FramerateManager.h"
#include "renderSDL/SDL_Extensions.h"
#include "CPlayerInterface.h"
#include "../lib/filesystem/Filesystem.h"
2009-06-28 02:33:59 +03:00
2023-01-30 00:12:43 +02:00
#include <SDL_render.h>
#ifndef DISABLE_VIDEO
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#ifdef _MSC_VER
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "swscale.lib")
#endif // _MSC_VER
// Define a set of functions to read data
static int lodRead(void* opaque, uint8_t* buf, int size)
{
auto video = reinterpret_cast<CVideoPlayer *>(opaque);
return static_cast<int>(video->data->read(buf, size));
}
static si64 lodSeek(void * opaque, si64 pos, int whence)
{
auto video = reinterpret_cast<CVideoPlayer *>(opaque);
if (whence & AVSEEK_SIZE)
return video->data->getSize();
return video->data->seek(pos);
}
CVideoPlayer::CVideoPlayer()
: stream(-1)
, format (nullptr)
, codecContext(nullptr)
, codec(nullptr)
, frame(nullptr)
, sws(nullptr)
, context(nullptr)
, texture(nullptr)
, dest(nullptr)
, destRect(0,0,0,0)
, pos(0,0,0,0)
, frameTime(0)
, doLoop(false)
{}
bool CVideoPlayer::open(std::string fname, bool scale)
2011-08-08 17:20:22 +03:00
{
return open(fname, true, false);
}
2009-06-28 02:33:59 +03:00
// loop = to loop through the video
// useOverlay = directly write to the screen.
bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scale)
{
close();
this->fname = fname;
2009-06-28 02:33:59 +03:00
doLoop = loop;
frameTime = 0;
ResourceID resource(std::string("Video/") + fname, EResType::VIDEO);
2012-08-08 15:17:25 +03:00
if (!CResourceHandler::get()->existsResource(resource))
{
2017-08-11 13:38:10 +02:00
logGlobal->error("Error: video %s was not found", resource.getName());
2012-08-08 15:17:25 +03:00
return false;
}
data = CResourceHandler::get()->load(resource);
static const int BUFFER_SIZE = 4096;
unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek);
format = avformat_alloc_context();
format->pb = context;
// filename is not needed - file was already open and stored in this->data;
int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr);
if (avfopen != 0)
{
return false;
}
// Retrieve stream information
if (avformat_find_stream_info(format, nullptr) < 0)
return false;
// Find the first video stream
stream = -1;
for(ui32 i=0; i<format->nb_streams; i++)
{
2022-05-10 18:25:48 +02:00
if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
2011-07-05 22:06:47 +03:00
{
stream = i;
break;
}
}
if (stream < 0)
// No video stream in that file
return false;
// Find the decoder for the video stream
2022-05-10 18:25:48 +02:00
codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
if (codec == nullptr)
{
// Unsupported codec
return false;
}
2022-05-10 18:25:48 +02:00
codecContext = avcodec_alloc_context3(codec);
if(!codecContext)
return false;
2022-05-10 18:25:48 +02:00
// Get a pointer to the codec context for the video stream
int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar);
if (ret < 0)
{
//We cannot get codec from parameters
avcodec_free_context(&codecContext);
2022-05-10 18:25:48 +02:00
return false;
}
// Open codec
if ( avcodec_open2(codecContext, codec, nullptr) < 0 )
{
// Could not open codec
codec = nullptr;
return false;
}
// Allocate video frame
frame = av_frame_alloc();
2016-11-27 16:48:18 +02:00
//setup scaling
if(scale)
{
2016-11-27 16:48:18 +02:00
pos.w = screen->w;
pos.h = screen->h;
}
else
{
2016-11-27 16:48:18 +02:00
pos.w = codecContext->width;
pos.h = codecContext->height;
}
// Allocate a place to put our YUV image on that screen
if (useOverlay)
{
texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
}
else
{
dest = CSDL_Ext::newSurface(pos.w, pos.h);
2009-06-28 02:33:59 +03:00
destRect.x = destRect.y = 0;
destRect.w = pos.w;
destRect.h = pos.h;
2009-06-28 02:33:59 +03:00
}
if (texture == nullptr && dest == nullptr)
return false;
if (texture)
{ // Convert the image into YUV format that SDL uses
2016-11-27 16:48:18 +02:00
sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
pos.w, pos.h,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC, nullptr, nullptr, nullptr);
}
else
{
AVPixelFormat screenFormat = AV_PIX_FMT_NONE;
if (screen->format->Bshift > screen->format->Rshift)
{
2012-12-01 09:30:52 +03:00
// this a BGR surface
switch (screen->format->BytesPerPixel)
{
case 2: screenFormat = AV_PIX_FMT_BGR565; break;
case 3: screenFormat = AV_PIX_FMT_BGR24; break;
case 4: screenFormat = AV_PIX_FMT_BGR32; break;
2012-12-01 09:30:52 +03:00
default: return false;
}
}
else
{
// this a RGB surface
switch (screen->format->BytesPerPixel)
{
case 2: screenFormat = AV_PIX_FMT_RGB565; break;
case 3: screenFormat = AV_PIX_FMT_RGB24; break;
case 4: screenFormat = AV_PIX_FMT_RGB32; break;
2012-12-01 09:30:52 +03:00
default: return false;
}
}
2016-11-27 16:48:18 +02:00
sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
pos.w, pos.h, screenFormat,
SWS_BICUBIC, nullptr, nullptr, nullptr);
2009-06-28 02:33:59 +03:00
}
if (sws == nullptr)
return false;
return true;
}
2009-06-28 02:33:59 +03:00
// Read the next frame. Return false on error/end of file.
bool CVideoPlayer::nextFrame()
{
AVPacket packet;
int frameFinished = 0;
2009-06-28 02:33:59 +03:00
bool gotError = false;
if (sws == nullptr)
2009-06-28 02:33:59 +03:00
return false;
while(!frameFinished)
{
2009-06-28 02:33:59 +03:00
int ret = av_read_frame(format, &packet);
if (ret < 0)
{
2009-06-28 02:33:59 +03:00
// Error. It's probably an end of file.
if (doLoop && !gotError)
{
2009-06-28 02:33:59 +03:00
// Rewind
frameTime = 0;
2015-01-25 15:28:11 +02:00
if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0)
2009-06-28 02:33:59 +03:00
break;
gotError = true;
}
else
{
2009-06-28 02:33:59 +03:00
break;
}
}
else
{
2009-06-28 02:33:59 +03:00
// Is this a packet from the video stream?
if (packet.stream_index == stream)
{
2009-06-28 02:33:59 +03:00
// Decode video frame
2022-05-10 18:25:48 +02:00
int rc = avcodec_send_packet(codecContext, &packet);
if (rc >=0)
packet.size = 0;
rc = avcodec_receive_frame(codecContext, frame);
if (rc >= 0)
frameFinished = 1;
2009-06-28 02:33:59 +03:00
// Did we get a video frame?
if (frameFinished)
{
2022-05-10 18:25:48 +02:00
uint8_t *data[4];
int linesize[4];
2009-06-28 02:33:59 +03:00
if (texture) {
2022-05-10 18:25:48 +02:00
av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1);
sws_scale(sws, frame->data, frame->linesize,
2022-05-10 18:25:48 +02:00
0, codecContext->height, data, linesize);
2022-05-10 18:25:48 +02:00
SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0],
data[1], linesize[1],
data[2], linesize[2]);
av_freep(&data[0]);
}
else
{
client/CVideoHandler.cpp: fix crash on video playback Avoid buffer overflow caused by sws_scale(): http://trac.ffmpeg.org/ticket/9254 Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() has a few requirements for target data buffers on rescaling: 1. buffer has to be aligned to be usable for SIMD instructions 2. buffer has to be padded to allow small overflow by SIMD instructions Unfortunately SDL_Surface does not provide these guarantees. This means that atempt to rescale directly into SDL surface causes memory corruption. Usually it happens on campaign selection screen where short video moves start spinning on mouse hover. To fix [1.] we use av_malloc() for memory allocation. To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. We have to use intermdiate buffer and then use memcpy() to land it to SDL_Surface. Without the change crash has the following backtrace: ``` (gdb) bt (c=0x47508940, src=0x1ffeffef50, srcStride=0x1ffeffef30, srcSliceY=0, srcSliceH=116, dst=0x1ffeffef70, dstStride=0x1ffeffef40) at src/libswscale/x86/yuv2rgb_template.c:119 (c=<optimized out>, srcSlice=<optimized out>, srcStride=0x432afa20, srcSliceY=<optimized out>, srcSliceH=116, dst=<optimized out>, dstStride=0x1ffefff0a0) at src/libswscale/swscale.c:969 (this=0x1abaa330, x=90, y=72, dst=0x1a85a4c0, forceRedraw=<optimized out>, update=<optimized out>) at ../vcmi-9999/client/CVideoHandler.cpp:332 at ../vcmi-9999/client/gui/CIntObject.cpp:83 at ../vcmi-9999/client/gui/CGuiHandler.cpp:462 ``` valgrind points to corruption right in sws_scale(): ``` Invalid write of size 8 at 0x6C50BD3: ??? (in /usr/lib64/libswscale.so.5.7.100) by 0x6C4FAE6: yuv420_rgb32_ssse3 (yuv2rgb_template.c:119) by 0x6C28DF2: sws_scale (swscale.c:969) by 0x4566F6: CVideoPlayer::nextFrame() (CVideoHandler.cpp:293) by 0x4573A6: CVideoPlayer::update(int, int, SDL_Surface*, bool, bool) (CVideoHandler.cpp:332) by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83) by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319) by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462) by 0x1F7450: mainLoop (CMT.cpp:1387) by 0x1F7450: main (CMT.cpp:513) Address 0x475088a8 is 0 bytes after a block of size 92,840 alloc'd at 0x483F7E5: malloc (vg_replace_malloc.c:380) by 0x52B4E23: SDL_malloc_REAL (SDL_malloc.c:5387) by 0x5266237: SDL_SIMDAlloc_REAL (SDL_cpuinfo.c:963) by 0x52EF042: SDL_CreateRGBSurfaceWithFormat_REAL (SDL_surface.c:123) by 0x2649AC: CSDL_Ext::newSurface(int, int, SDL_Surface*) (SDL_Extensions.cpp:42) by 0x457B20: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool, bool, bool) (CVideoHandler.cpp:182) by 0x457C60: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool) (CVideoHandler.cpp:84) by 0x35B14E: CCampaignScreen::CCampaignButton::show(SDL_Surface*) (CCampaignScreen.cpp:126) by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83) by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319) by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462) by 0x1F7450: mainLoop (CMT.cpp:1387) by 0x1F7450: main (CMT.cpp:513) ``` Signed-off-by: Sergei Trofimovich <slyfox@gentoo.org>
2021-07-25 12:25:37 +02:00
/* Avoid buffer overflow caused by sws_scale():
* http://trac.ffmpeg.org/ticket/9254
* Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
* has a few requirements for target data buffers on rescaling:
* 1. buffer has to be aligned to be usable for SIMD instructions
* 2. buffer has to be padded to allow small overflow by SIMD instructions
* Unfortunately SDL_Surface does not provide these guarantees.
* This means that atempt to rescale directly into SDL surface causes
* memory corruption. Usually it happens on campaign selection screen
* where short video moves start spinning on mouse hover.
*
* To fix [1.] we use av_malloc() for memory allocation.
* To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
* We have to use intermdiate buffer and then use memcpy() to land it
* to SDL_Surface.
*/
size_t pic_bytes = dest->pitch * dest->h;
size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
void * for_sws = av_malloc (pic_bytes + ffmped_pad);
2022-05-10 18:25:48 +02:00
data[0] = (ui8 *)for_sws;
linesize[0] = dest->pitch;
2009-06-28 02:33:59 +03:00
sws_scale(sws, frame->data, frame->linesize,
2022-05-10 18:25:48 +02:00
0, codecContext->height, data, linesize);
client/CVideoHandler.cpp: fix crash on video playback Avoid buffer overflow caused by sws_scale(): http://trac.ffmpeg.org/ticket/9254 Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() has a few requirements for target data buffers on rescaling: 1. buffer has to be aligned to be usable for SIMD instructions 2. buffer has to be padded to allow small overflow by SIMD instructions Unfortunately SDL_Surface does not provide these guarantees. This means that atempt to rescale directly into SDL surface causes memory corruption. Usually it happens on campaign selection screen where short video moves start spinning on mouse hover. To fix [1.] we use av_malloc() for memory allocation. To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. We have to use intermdiate buffer and then use memcpy() to land it to SDL_Surface. Without the change crash has the following backtrace: ``` (gdb) bt (c=0x47508940, src=0x1ffeffef50, srcStride=0x1ffeffef30, srcSliceY=0, srcSliceH=116, dst=0x1ffeffef70, dstStride=0x1ffeffef40) at src/libswscale/x86/yuv2rgb_template.c:119 (c=<optimized out>, srcSlice=<optimized out>, srcStride=0x432afa20, srcSliceY=<optimized out>, srcSliceH=116, dst=<optimized out>, dstStride=0x1ffefff0a0) at src/libswscale/swscale.c:969 (this=0x1abaa330, x=90, y=72, dst=0x1a85a4c0, forceRedraw=<optimized out>, update=<optimized out>) at ../vcmi-9999/client/CVideoHandler.cpp:332 at ../vcmi-9999/client/gui/CIntObject.cpp:83 at ../vcmi-9999/client/gui/CGuiHandler.cpp:462 ``` valgrind points to corruption right in sws_scale(): ``` Invalid write of size 8 at 0x6C50BD3: ??? (in /usr/lib64/libswscale.so.5.7.100) by 0x6C4FAE6: yuv420_rgb32_ssse3 (yuv2rgb_template.c:119) by 0x6C28DF2: sws_scale (swscale.c:969) by 0x4566F6: CVideoPlayer::nextFrame() (CVideoHandler.cpp:293) by 0x4573A6: CVideoPlayer::update(int, int, SDL_Surface*, bool, bool) (CVideoHandler.cpp:332) by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83) by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319) by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462) by 0x1F7450: mainLoop (CMT.cpp:1387) by 0x1F7450: main (CMT.cpp:513) Address 0x475088a8 is 0 bytes after a block of size 92,840 alloc'd at 0x483F7E5: malloc (vg_replace_malloc.c:380) by 0x52B4E23: SDL_malloc_REAL (SDL_malloc.c:5387) by 0x5266237: SDL_SIMDAlloc_REAL (SDL_cpuinfo.c:963) by 0x52EF042: SDL_CreateRGBSurfaceWithFormat_REAL (SDL_surface.c:123) by 0x2649AC: CSDL_Ext::newSurface(int, int, SDL_Surface*) (SDL_Extensions.cpp:42) by 0x457B20: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool, bool, bool) (CVideoHandler.cpp:182) by 0x457C60: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool) (CVideoHandler.cpp:84) by 0x35B14E: CCampaignScreen::CCampaignButton::show(SDL_Surface*) (CCampaignScreen.cpp:126) by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83) by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319) by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462) by 0x1F7450: mainLoop (CMT.cpp:1387) by 0x1F7450: main (CMT.cpp:513) ``` Signed-off-by: Sergei Trofimovich <slyfox@gentoo.org>
2021-07-25 12:25:37 +02:00
memcpy(dest->pixels, for_sws, pic_bytes);
av_free(for_sws);
2009-06-28 02:33:59 +03:00
}
}
}
2022-05-10 18:25:48 +02:00
av_packet_unref(&packet);
2009-06-28 02:33:59 +03:00
}
}
2009-06-28 02:33:59 +03:00
return frameFinished != 0;
}
2009-06-28 02:33:59 +03:00
void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update )
{
if (sws == nullptr)
2009-06-28 02:33:59 +03:00
return;
2009-06-28 02:33:59 +03:00
pos.x = x;
pos.y = y;
CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft());
2009-06-28 02:33:59 +03:00
if (update)
CSDL_Ext::updateRect(dst, pos);
2009-06-28 02:33:59 +03:00
}
void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update )
{
show(x, y, dst, update);
}
2009-06-28 02:33:59 +03:00
void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update )
{
if (sws == nullptr)
2009-06-28 02:33:59 +03:00
return;
#if (LIBAVUTIL_VERSION_MAJOR < 58)
auto packet_duration = frame->pkt_duration;
#else
auto packet_duration = frame->duration;
#endif
double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0;
if (frameTime >= frameEndTime )
2009-06-28 02:33:59 +03:00
{
if (nextFrame())
show(x,y,dst,update);
else
{
open(fname);
nextFrame();
// The y position is wrong at the first frame.
// Note: either the windows player or the linux player is
// broken. Compensate here until the bug is found.
show(x, y--, dst, update);
}
}
else
{
2009-06-28 02:33:59 +03:00
redraw(x, y, dst, update);
}
}
void CVideoPlayer::close()
{
fname.clear();
if (sws)
{
sws_freeContext(sws);
sws = nullptr;
}
if (texture)
{
SDL_DestroyTexture(texture);
texture = nullptr;
}
if (dest)
{
2009-06-28 02:33:59 +03:00
SDL_FreeSurface(dest);
dest = nullptr;
2009-06-28 02:33:59 +03:00
}
if (frame)
{
2016-11-27 16:48:18 +02:00
av_frame_free(&frame);//will be set to null
}
if (codec)
{
avcodec_close(codecContext);
codec = nullptr;
2022-05-10 18:25:48 +02:00
}
if (codecContext)
{
avcodec_free_context(&codecContext);
}
if (format)
{
avformat_close_input(&format);
}
if (context)
{
av_free(context);
context = nullptr;
}
}
2009-06-28 02:33:59 +03:00
// Plays a video. Only works for overlays.
bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
2009-06-28 02:33:59 +03:00
{
// Note: either the windows player or the linux player is
// broken. Compensate here until the bug is found.
y--;
pos.x = x;
pos.y = y;
frameTime = 0.0;
2009-06-28 02:33:59 +03:00
while(nextFrame())
{
if(stopOnKey)
{
GH.input().fetchEvents();
if(GH.input().ignoreEventsUntilInput())
return false;
}
2009-06-28 02:33:59 +03:00
SDL_Rect rect = CSDL_Ext::toSDL(pos);
SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
SDL_RenderPresent(mainRenderer);
2009-06-28 02:33:59 +03:00
#if (LIBAVUTIL_VERSION_MAJOR < 58)
auto packet_duration = frame->pkt_duration;
#else
auto packet_duration = frame->duration;
#endif
double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base);
uint32_t timeToSleepMillisec = 1000 * (frameDurationSec);
boost::this_thread::sleep(boost::posix_time::millisec(timeToSleepMillisec));
2009-06-28 02:33:59 +03:00
}
return true;
}
bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, bool stopOnKey, bool scale)
2009-06-28 02:33:59 +03:00
{
open(name, false, true, scale);
bool ret = playVideo(x, y, stopOnKey);
2009-06-28 02:33:59 +03:00
close();
return ret;
}
CVideoPlayer::~CVideoPlayer()
{
close();
}
#endif