1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-12 10:03:53 +02:00
vcmi/client/media/CVideoHandler.cpp

588 lines
17 KiB
C++

/*
* 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"
#ifndef DISABLE_VIDEO
#include "ISoundPlayer.h"
#include "../CGameInfo.h"
#include "../CMT.h"
#include "../CPlayerInterface.h"
#include "../eventsSDL/InputHandler.h"
#include "../gui/CGuiHandler.h"
#include "../gui/FramerateManager.h"
#include "../render/Canvas.h"
#include "../renderSDL/SDL_Extensions.h"
#include "../../lib/filesystem/CInputStream.h"
#include "../../lib/filesystem/Filesystem.h"
#include <SDL_render.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
// Define a set of functions to read data
static int lodRead(void * opaque, uint8_t * buf, int size)
{
auto * data = static_cast<CInputStream *>(opaque);
int bytes = static_cast<int>(data->read(buf, size));
if(bytes == 0)
return AVERROR_EOF;
return bytes;
}
static si64 lodSeek(void * opaque, si64 pos, int whence)
{
auto * data = static_cast<CInputStream *>(opaque);
if(whence & AVSEEK_SIZE)
return data->getSize();
return data->seek(pos);
}
[[noreturn]] static void throwFFmpegError(int errorCode)
{
std::array<char, AV_ERROR_MAX_STRING_SIZE> errorMessage{};
av_strerror(errorCode, errorMessage.data(), errorMessage.size());
throw std::runtime_error(errorMessage.data());
}
static std::unique_ptr<CInputStream> findVideoData(const VideoPath & videoToOpen)
{
if(CResourceHandler::get()->existsResource(videoToOpen))
return CResourceHandler::get()->load(videoToOpen);
auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/");
auto lowQualityVideo = videoToOpen.toType<EResType::VIDEO_LOW_QUALITY>();
auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType<EResType::VIDEO_LOW_QUALITY>();
if(CResourceHandler::get()->existsResource(highQualityVideoToOpenWithDir))
return CResourceHandler::get()->load(highQualityVideoToOpenWithDir);
if(CResourceHandler::get()->existsResource(lowQualityVideo))
return CResourceHandler::get()->load(lowQualityVideo);
return CResourceHandler::get()->load(lowQualityVideoWithDir);
}
void CVideoInstance::open(const VideoPath & videoToOpen)
{
input = findVideoData(videoToOpen);
}
void CVideoInstance::openContext(FFMpegStreamState & state)
{
static const int BUFFER_SIZE = 4096;
input->seek(0);
auto * buffer = static_cast<unsigned char *>(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg
state.context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek);
state.formatContext = avformat_alloc_context();
state.formatContext->pb = state.context;
// filename is not needed - file was already open and stored in this->data;
int avfopen = avformat_open_input(&state.formatContext, "dummyFilename", nullptr, nullptr);
if(avfopen != 0)
throwFFmpegError(avfopen);
// Retrieve stream information
int findStreamInfo = avformat_find_stream_info(state.formatContext, nullptr);
if(avfopen < 0)
throwFFmpegError(findStreamInfo);
}
void CVideoInstance::openCodec(FFMpegStreamState & state, int streamIndex)
{
state.streamIndex = streamIndex;
// Find the decoder for the stream
state.codec = avcodec_find_decoder(state.formatContext->streams[streamIndex]->codecpar->codec_id);
if(state.codec == nullptr)
throw std::runtime_error("Unsupported codec");
state.codecContext = avcodec_alloc_context3(state.codec);
if(state.codecContext == nullptr)
throw std::runtime_error("Failed to create codec context");
// Get a pointer to the codec context for the video stream
int ret = avcodec_parameters_to_context(state.codecContext, state.formatContext->streams[streamIndex]->codecpar);
if(ret < 0)
{
//We cannot get codec from parameters
avcodec_free_context(&state.codecContext);
throwFFmpegError(ret);
}
// Open codec
ret = avcodec_open2(state.codecContext, state.codec, nullptr);
if(ret < 0)
{
// Could not open codec
state.codec = nullptr;
throwFFmpegError(ret);
}
}
void CVideoInstance::openVideo()
{
openContext(video);
for(int i = 0; i < video.formatContext->nb_streams; i++)
{
if(video.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
openCodec(video, i);
return;
}
}
}
void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput)
{
if (video.streamIndex == -1)
throw std::runtime_error("Invalid file state! No video stream!");
// Allocate video frame
output.frame = av_frame_alloc();
//setup scaling
if(scaleToScreenSize)
{
output.dimensions.x = screen->w;
output.dimensions.y = screen->h;
}
else
{
output.dimensions.x = video.codecContext->width;
output.dimensions.y = video.codecContext->height;
}
// Allocate a place to put our YUV image on that screen
if (useTextureOutput)
{
std::array potentialFormats = {
AV_PIX_FMT_YUV420P, // -> SDL_PIXELFORMAT_IYUV - most of H3 videos use YUV format, so it is preferred to save some space & conversion time
AV_PIX_FMT_RGB32, // -> SDL_PIXELFORMAT_ARGB8888 - some .smk videos actually use palette, so RGB > YUV. This is also our screen texture format
AV_PIX_FMT_NONE
};
auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), video.codecContext->pix_fmt, false, nullptr);
if (preferredFormat == AV_PIX_FMT_YUV420P)
output.textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y);
else
output.textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y);
output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt,
output.dimensions.x, output.dimensions.y, preferredFormat,
SWS_BICUBIC, nullptr, nullptr, nullptr);
}
else
{
output.surface = CSDL_Ext::newSurface(output.dimensions.x, output.dimensions.y);
output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt,
output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32,
SWS_BICUBIC, nullptr, nullptr, nullptr);
}
if (output.sws == nullptr)
throw std::runtime_error("Failed to create sws");
}
bool CVideoInstance::nextFrame()
{
AVPacket packet;
for(;;)
{
int ret = av_read_frame(video.formatContext, &packet);
if(ret < 0)
{
if(ret == AVERROR_EOF)
return false;
throwFFmpegError(ret);
}
// Is this a packet from the video stream?
if(packet.stream_index == video.streamIndex)
{
// Decode video frame
int rc = avcodec_send_packet(video.codecContext, &packet);
if(rc < 0)
throwFFmpegError(rc);
rc = avcodec_receive_frame(video.codecContext, output.frame);
if(rc < 0)
throwFFmpegError(rc);
uint8_t * data[4] = {};
int linesize[4] = {};
if(output.textureYUV)
{
av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_YUV420P, 1);
sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize);
SDL_UpdateYUVTexture(output.textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]);
av_freep(&data[0]);
}
if(output.textureRGB)
{
av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32, 1);
sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize);
SDL_UpdateTexture(output.textureRGB, nullptr, data[0], linesize[0]);
av_freep(&data[0]);
}
if (output.surface)
{
// Avoid buffer overflow caused by sws_scale():
// http://trac.ffmpeg.org/ticket/9254
size_t pic_bytes = output.surface->pitch * output.surface->h;
size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
void * for_sws = av_malloc(pic_bytes + ffmped_pad);
data[0] = (ui8 *)for_sws;
linesize[0] = output.surface->pitch;
sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize);
memcpy(output.surface->pixels, for_sws, pic_bytes);
av_free(for_sws);
}
av_packet_unref(&packet);
return true;
}
}
}
bool CVideoInstance::videoEnded()
{
return output.videoEnded;
}
void CVideoInstance::close()
{
sws_freeContext(output.sws);
av_frame_free(&output.frame);
SDL_DestroyTexture(output.textureYUV);
SDL_DestroyTexture(output.textureRGB);
SDL_FreeSurface(output.surface);
closeState(video);
}
void CVideoInstance::closeState(FFMpegStreamState & streamState)
{
// state.videoStream.codec???
// state.audioStream.codec???
avcodec_close(video.codecContext);
avcodec_free_context(&video.codecContext);
avcodec_close(video.codecContext);
avcodec_free_context(&video.codecContext);
avformat_close_input(&video.formatContext);
av_free(video.context);
output = FFMpegVideoOutput();
video = FFMpegStreamState();
}
CVideoInstance::~CVideoInstance()
{
close();
}
Point CVideoInstance::size()
{
if(!output.frame)
throw std::runtime_error("Invalid video frame!");
return Point(output.frame->width, output.frame->height);
}
void CVideoInstance::show(const Point & position, Canvas & canvas)
{
if(output.sws == nullptr)
throw std::runtime_error("No video to show!");
CSDL_Ext::blitSurface(output.surface, canvas.getInternalSurface(), position);
}
void CVideoInstance::tick(uint32_t msPassed)
{
if(output.sws == nullptr)
throw std::runtime_error("No video to show!");
if(output.videoEnded)
throw std::runtime_error("Video already ended!");
# if(LIBAVUTIL_VERSION_MAJOR < 58)
auto packet_duration = output.frame->pkt_duration;
# else
auto packet_duration = frame->duration;
# endif
double frameEndTime = (output.frame->pts + packet_duration) * av_q2d(video.formatContext->streams[video.streamIndex]->time_base);
output.frameTime += msPassed / 1000.0;
if(output.frameTime >= frameEndTime)
{
if(!nextFrame())
output.videoEnded = true;
}
}
static int32_t sampleSizeBytes(int audioFormat)
{
switch (audioFormat)
{
case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits
case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar
return 1;
case AV_SAMPLE_FMT_S16: ///< signed 16 bits
case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar
return 2;
case AV_SAMPLE_FMT_S32: ///< signed 32 bits
case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar
case AV_SAMPLE_FMT_FLT: ///< float
case AV_SAMPLE_FMT_FLTP: ///< float, planar
return 4;
case AV_SAMPLE_FMT_DBL: ///< double
case AV_SAMPLE_FMT_DBLP: ///< double, planar
case AV_SAMPLE_FMT_S64: ///< signed 64 bits
case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar
return 8;
}
throw std::runtime_error("Invalid audio format");
}
static int32_t sampleWavType(int audioFormat)
{
switch (audioFormat)
{
case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits
case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar
case AV_SAMPLE_FMT_S16: ///< signed 16 bits
case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar
case AV_SAMPLE_FMT_S32: ///< signed 32 bits
case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar
case AV_SAMPLE_FMT_S64: ///< signed 64 bits
case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar
return 1; // PCM
case AV_SAMPLE_FMT_FLT: ///< float
case AV_SAMPLE_FMT_FLTP: ///< float, planar
case AV_SAMPLE_FMT_DBL: ///< double
case AV_SAMPLE_FMT_DBLP: ///< double, planar
return 3; // IEEE float
}
throw std::runtime_error("Invalid audio format");
}
void CVideoInstance::playAudio()
{
FFMpegStreamState audio;
openContext(audio);
for(int i = 0; i < audio.formatContext->nb_streams; i++)
{
if(audio.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
openCodec(audio, i);
break;
}
}
std::pair<std::unique_ptr<ui8 []>, si64> dat(std::make_pair(nullptr, 0));
if (audio.streamIndex < 0)
return; // nothing to play
const auto * codecpar = audio.formatContext->streams[audio.streamIndex]->codecpar;
AVFrame *frameAudio = av_frame_alloc();
AVFrame *frameVideo = av_frame_alloc();
AVPacket packet;
std::vector<ui8> samples;
int32_t sampleSize = sampleSizeBytes(codecpar->format);
samples.reserve(44100 * 5); // arbitrary 5-second buffer
while (av_read_frame(audio.formatContext, &packet) >= 0)
{
if (packet.stream_index == video.streamIndex)
{
// Decode video frame
int rc = avcodec_send_packet(video.codecContext, &packet);
if(rc < 0)
throwFFmpegError(rc);
rc = avcodec_receive_frame(video.codecContext, frameVideo);
if(rc < 0)
throwFFmpegError(rc);
}
if(packet.stream_index == audio.streamIndex)
{
int rc = avcodec_send_packet(audio.codecContext, &packet);
if(rc < 0)
throwFFmpegError(rc);
for (;;)
{
rc = avcodec_receive_frame(audio.codecContext, frameAudio);
if (rc == AVERROR(EAGAIN))
break;
if(rc < 0)
throwFFmpegError(rc);
int bytesToRead = frameAudio->nb_samples * 2 * sampleSize;
samples.insert(samples.end(), frameAudio->data[0], frameAudio->data[0] + bytesToRead);
}
}
av_packet_unref(&packet);
}
typedef struct WAV_HEADER {
ui8 RIFF[4] = {'R', 'I', 'F', 'F'};
ui32 ChunkSize;
ui8 WAVE[4] = {'W', 'A', 'V', 'E'};
ui8 fmt[4] = {'f', 'm', 't', ' '};
ui32 Subchunk1Size = 16;
ui16 AudioFormat = 1;
ui16 NumOfChan = 2;
ui32 SamplesPerSec = 22050;
ui32 bytesPerSec = 22050 * 2;
ui16 blockAlign = 2;
ui16 bitsPerSample = 32;
ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'};
ui32 Subchunk2Size;
} wav_hdr;
wav_hdr wav;
wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8;
wav.AudioFormat = sampleWavType(codecpar->format);
wav.NumOfChan = codecpar->channels;
wav.SamplesPerSec = codecpar->sample_rate;
wav.bytesPerSec = codecpar->sample_rate * sampleSize;
wav.bitsPerSample = sampleSize * 8;
wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44;
auto wavPtr = reinterpret_cast<ui8*>(&wav);
dat = std::make_pair(std::make_unique<ui8[]>(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr));
std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get());
std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr));
if (frameAudio)
av_frame_free(&frameAudio);
CCS->soundh->playSound(dat);
closeState(audio);
}
bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey)
{
CVideoInstance instance;
instance.open(name);
instance.playAudio();
instance.openVideo();
instance.prepareOutput(scale, useOverlay);
auto lastTimePoint = boost::chrono::steady_clock::now();
while(instance.nextFrame())
{
if(stopOnKey)
{
GH.input().fetchEvents();
if(GH.input().ignoreEventsUntilInput())
return false;
}
SDL_Rect rect;
rect.x = position.x;
rect.y = position.y;
rect.w = instance.output.dimensions.x;
rect.h = instance.output.dimensions.y;
if(useOverlay)
SDL_RenderFillRect(mainRenderer, &rect);
else
SDL_RenderClear(mainRenderer);
if (instance.output.textureYUV)
SDL_RenderCopy(mainRenderer, instance.output.textureYUV, nullptr, &rect);
else
SDL_RenderCopy(mainRenderer, instance.output.textureRGB, nullptr, &rect);
SDL_RenderPresent(mainRenderer);
#if (LIBAVUTIL_VERSION_MAJOR < 58)
auto packet_duration = instance.output.frame->pkt_duration;
#else
auto packet_duration = output.frame->duration;
#endif
// Framerate delay
double targetFrameTimeSeconds = packet_duration * av_q2d(instance.video.formatContext->streams[instance.video.streamIndex]->time_base);
auto targetFrameTime = boost::chrono::milliseconds(static_cast<int>(1000 * (targetFrameTimeSeconds)));
auto timePointAfterPresent = boost::chrono::steady_clock::now();
auto timeSpentBusy = boost::chrono::duration_cast<boost::chrono::milliseconds>(timePointAfterPresent - lastTimePoint);
if (targetFrameTime > timeSpentBusy)
boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy);
lastTimePoint = boost::chrono::steady_clock::now();
}
return true;
}
bool CVideoPlayer::playIntroVideo(const VideoPath & name)
{
return openAndPlayVideoImpl(name, Point(0,0), true, true, true);
}
void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position)
{
openAndPlayVideoImpl(name, position, false, false, false);
}
std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, bool scaleToScreen)
{
auto result = std::make_unique<CVideoInstance>();
result->open(name);
result->openVideo();
result->prepareOutput(scaleToScreen, false);
return result;
}
#endif