// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. #include "FFMPEGMediaPlayer.h" #include "Async/Async.h" #include "IMediaEventSink.h" #include "IMediaOptions.h" #include "Misc/Optional.h" #include "UObject/Class.h" #include "FFMPEGMediaTracks.h" #include "FFMPEGMediaSettings.h" extern "C" { #include "libavformat/avformat.h" #include "libavutil/opt.h" #include "libavutil/time.h" } #define FF_INPUT_BUFFER_PADDING_SIZE 32 /* FWmfVideoPlayer structors *****************************************************************************/ FFFMPEGMediaPlayer::FFFMPEGMediaPlayer(IMediaEventSink& InEventSink) : EventSink(InEventSink) , Tracks(MakeShared()) { check(Tracks.IsValid()); IOContext = nullptr; FormatContext = nullptr; stopped = true; } FFFMPEGMediaPlayer::~FFFMPEGMediaPlayer() { Close(); } /* IMediaPlayer interface *****************************************************************************/ void FFFMPEGMediaPlayer::Close() { if (Tracks->GetState() == EMediaState::Closed) { return; } // reset player stopped = true; MediaUrl = FString(); Tracks->Shutdown(); if (FormatContext) { FormatContext->video_codec = NULL; FormatContext->audio_codec = NULL; avformat_close_input(&FormatContext); FormatContext = nullptr; } if (IOContext) { av_free(IOContext->buffer); av_free(IOContext); IOContext = nullptr; } // notify listeners EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged); EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed); } IMediaCache& FFFMPEGMediaPlayer::GetCache() { return *this; } IMediaControls& FFFMPEGMediaPlayer::GetControls() { return *Tracks; } FString FFFMPEGMediaPlayer::GetInfo() const { return Tracks->GetInfo(); } FName FFFMPEGMediaPlayer::GetPlayerName() const { static FName PlayerName(TEXT("FFMPEGMedia")); return PlayerName; } FGuid FFFMPEGMediaPlayer::GetPlayerPluginGUID() const { // {938BEEB4-2E88-450E-9C1C-6109456279B3} static FGuid PlayerPluginGUID(0x938beeb4, 0x2e88450e, 0x9c1c6109, 0x456279b3); return PlayerPluginGUID; } IMediaSamples& FFFMPEGMediaPlayer::GetSamples() { return *Tracks; } FString FFFMPEGMediaPlayer::GetStats() const { FString Result; Tracks->AppendStats(Result); return Result; } IMediaTracks& FFFMPEGMediaPlayer::GetTracks() { return *Tracks; } FString FFFMPEGMediaPlayer::GetUrl() const { return MediaUrl; } IMediaView& FFFMPEGMediaPlayer::GetView() { return *this; } bool FFFMPEGMediaPlayer::Open(const FString& Url, const IMediaOptions* Options, const FMediaPlayerOptions* PlayerOptions) { Close(); if (Url.IsEmpty()) { return false; } const bool Precache = (Options != nullptr) ? Options->GetMediaOption("PrecacheFile", false) : false; return InitializePlayer(nullptr, Url, Precache, PlayerOptions); } bool FFFMPEGMediaPlayer::Open(const FString& Url, const IMediaOptions* Options) { return Open(Url, Options, nullptr); } bool FFFMPEGMediaPlayer::Open(const TSharedRef& Archive, const FString& OriginalUrl, const IMediaOptions* /*Options*/) { Close(); if (Archive->TotalSize() == 0) { UE_LOG(LogFFMPEGMedia, Verbose, TEXT("Player %p: Cannot open media from archive (archive is empty)"), this); return false; } if (OriginalUrl.IsEmpty()) { UE_LOG(LogFFMPEGMedia, Verbose, TEXT("Player %p: Cannot open media from archive (no original URL provided)"), this); return false; } return InitializePlayer(Archive, OriginalUrl, false, nullptr); } void FFFMPEGMediaPlayer::TickFetch(FTimespan DeltaTime, FTimespan Timecode) { bool MediaSourceChanged = false; bool TrackSelectionChanged = false; Tracks->GetFlags(MediaSourceChanged, TrackSelectionChanged); if (MediaSourceChanged) { EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged); } if (TrackSelectionChanged) { } if (MediaSourceChanged || TrackSelectionChanged) { Tracks->ClearFlags(); } } void FFFMPEGMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan Timecode) { Tracks->TickInput(DeltaTime, Timecode); // forward session events TArray OutEvents; Tracks->GetEvents(OutEvents); for (const auto& Event : OutEvents) { EventSink.ReceiveMediaEvent(Event); } // process deferred tasks TFunction Task; while (PlayerTasks.Dequeue(Task)) { Task(); } } /* FFFMPEGMediaPlayer implementation *****************************************************************************/ bool FFFMPEGMediaPlayer::InitializePlayer(const TSharedPtr& Archive, const FString& Url, bool Precache, const FMediaPlayerOptions* PlayerOptions ) { UE_LOG(LogFFMPEGMedia, Verbose, TEXT("Player %llx: Initializing %s (archive = %s, precache = %s)"), this, *Url, Archive.IsValid() ? TEXT("yes") : TEXT("no"), Precache ? TEXT("yes") : TEXT("no")); const auto Settings = GetDefault(); check(Settings != nullptr); MediaUrl = Url; // initialize presentation on a separate thread const EAsyncExecution Execution = Precache ? EAsyncExecution::Thread : EAsyncExecution::ThreadPool; TFunction Task = [Archive, Url, Precache, PlayerOptions, TracksPtr = TWeakPtr(Tracks), ThisPtr=this]() { TSharedPtr PinnedTracks = TracksPtr.Pin(); if (PinnedTracks.IsValid() ) { AVFormatContext* context = ThisPtr->ReadContext(Archive, Url, Precache); if (context) { PinnedTracks->Initialize(context, Url, PlayerOptions); } } }; Async(Execution, Task); return true; } int FFFMPEGMediaPlayer::DecodeInterruptCallback(void *ctx) { FFFMPEGMediaPlayer* player = static_cast(ctx); return player->stopped?1:0; } int FFFMPEGMediaPlayer::ReadtStreamCallback(void* opaque, uint8_t* buf, int buf_size) { FFFMPEGMediaPlayer* player = static_cast(opaque); int64 Position = player->CurrentArchive->Tell(); int64 Size = player->CurrentArchive->TotalSize(); int64 BytesToRead = buf_size; if (BytesToRead > (int64)Size) { BytesToRead = Size; } if ((Size - BytesToRead) < player->CurrentArchive->Tell()) { BytesToRead = Size - Position; } if (BytesToRead > 0) { player->CurrentArchive->Serialize(buf, BytesToRead); } player->CurrentArchive->Seek(Position + BytesToRead); return BytesToRead; } int64_t FFFMPEGMediaPlayer::SeekStreamCallback(void *opaque, int64_t offset, int whence) { FFFMPEGMediaPlayer* player = static_cast(opaque); if (whence == AVSEEK_SIZE) { return player->CurrentArchive->TotalSize(); } int64_t pos = player->CurrentArchive->Tell(); player->CurrentArchive->Seek(pos + offset); return player->CurrentArchive->Tell(); } AVFormatContext* FFFMPEGMediaPlayer::ReadContext(const TSharedPtr& Archive, const FString& Url, bool Precache) { AVDictionary *format_opts = NULL; int scan_all_pmts_set = 0; FormatContext = avformat_alloc_context(); stopped = false; FormatContext->interrupt_callback.callback = DecodeInterruptCallback; FormatContext->interrupt_callback.opaque = this; if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) { av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE); scan_all_pmts_set = 1; } const auto Settings = GetDefault(); if ( Settings->RtspTransport != ERTSPTransport::Default) { switch (Settings->RtspTransport) { case ERTSPTransport::Udp: av_dict_set(&format_opts, "rtsp_transport", "udp", 0); break; case ERTSPTransport::Tcp: av_dict_set(&format_opts, "rtsp_transport", "tcp", 0); break; case ERTSPTransport::UdpMulticast: av_dict_set(&format_opts, "rtsp_transport", "udp_multicast", 0); break; case ERTSPTransport::Http: av_dict_set(&format_opts, "rtsp_transport", "http", 0); break; case ERTSPTransport::Https: av_dict_set(&format_opts, "rtsp_transport", "https", 0); break; default: break; } } if ( Settings->ZeroLatencyStreaming) { av_dict_set(&format_opts, "fflags", "nobuffer", 0); } int err = 0; if (!Archive.IsValid()) { if (Url.StartsWith(TEXT("file://"))) { const TCHAR* FilePath = &Url[7]; err = avformat_open_input(&FormatContext, TCHAR_TO_UTF8(FilePath), NULL, &format_opts); } else { err = avformat_open_input(&FormatContext, TCHAR_TO_UTF8(*Url), NULL, &format_opts); } } else { CurrentArchive = Archive; const int ioBufferSize = 32768; unsigned char * ioBuffer = (unsigned char *)av_malloc(ioBufferSize + FF_INPUT_BUFFER_PADDING_SIZE); IOContext = avio_alloc_context(ioBuffer, ioBufferSize, 0, this, ReadtStreamCallback, NULL, SeekStreamCallback); FormatContext->pb = IOContext; err = avformat_open_input(&FormatContext, "InMemoryFile", NULL, &format_opts); } if (err < 0) { char errbuf[128]; const char *errbuf_ptr = errbuf; #if PLATFORM_WINDOWS if (av_strerror(err, errbuf, sizeof(errbuf)) < 0) strerror_s(errbuf, 128, AVUNERROR(err)); #else if (av_strerror(err, errbuf, sizeof(errbuf)) < 0) errbuf_ptr = strerror(AVUNERROR(err)); #endif PlayerTasks.Enqueue([=]() { EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpenFailed); }); if ( FormatContext ) { avformat_close_input(&FormatContext); FormatContext = nullptr; } stopped = true; return FormatContext; } if (scan_all_pmts_set) av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE); AVDictionaryEntry *t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX); if (t) { UE_LOG(LogFFMPEGMedia, Error, TEXT("Option %s not found"), UTF8_TO_TCHAR(t->key)); PlayerTasks.Enqueue([=]() { EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpenFailed); }); if ( FormatContext ) { avformat_close_input(&FormatContext); FormatContext = nullptr; } stopped = true; return FormatContext; } av_dict_free(&format_opts); av_format_inject_global_side_data(FormatContext); err = avformat_find_stream_info(FormatContext, NULL); if (FormatContext->pb) FormatContext->pb->eof_reached = 0; // FIXME hack, ffplay maybe should not use avio_feof() to test for the end #ifdef UE_BUILD_DEBUG dumpFFMPEGInfo(); #endif return FormatContext; } static void print_option(const AVClass *clazz, const AVOption *o) { FString type; switch (o->type) { case AV_OPT_TYPE_BINARY: type = TEXT("hexadecimal string"); break; case AV_OPT_TYPE_STRING: type = TEXT("string"); break; case AV_OPT_TYPE_INT: case AV_OPT_TYPE_INT64: type = TEXT("integer"); break; case AV_OPT_TYPE_FLOAT: case AV_OPT_TYPE_DOUBLE: type = TEXT("float"); break; case AV_OPT_TYPE_RATIONAL: type = TEXT("rational number"); break; case AV_OPT_TYPE_FLAGS: type = TEXT("flags"); break; case AV_OPT_TYPE_BOOL: type = TEXT("bool"); break; case AV_OPT_TYPE_SAMPLE_FMT:type = TEXT("SampleFmt"); break; default: type = TEXT("value"); break; } FString flags; if (o->flags & AV_OPT_FLAG_ENCODING_PARAM) { flags = TEXT("input"); if (o->flags & AV_OPT_FLAG_ENCODING_PARAM) flags += TEXT("/"); } if (o->flags & AV_OPT_FLAG_ENCODING_PARAM) flags += TEXT("output"); FString help; if (o->help) help = o->help; FString possibleValues; if (o->unit) { const AVOption *u = av_opt_next(&clazz, NULL); while (u) { bool found = false; if (u->type == AV_OPT_TYPE_CONST && u->unit && !strcmp(u->unit, o->unit)) { possibleValues += "\n"; possibleValues += "\t\t* "; possibleValues += u->name; found = true; } u = av_opt_next(&clazz, u); } } if ( o->unit) { UE_LOG(LogFFMPEGMedia, Display, TEXT("\t%s - %s: %s %s"), *type, UTF8_TO_TCHAR(o->name), *help, *possibleValues); } else { UE_LOG(LogFFMPEGMedia, Display, TEXT( "\t%s - %s: %s"), *type, UTF8_TO_TCHAR(o->name), *help); } } void FFFMPEGMediaPlayer::dumpOptions(const AVClass *clazz) { const AVOption *o = av_opt_next(&clazz, NULL); while (o != NULL) { if (o->type != AV_OPT_TYPE_CONST) { print_option(clazz, o); } o = av_opt_next(clazz, o); } } void FFFMPEGMediaPlayer::dumpFFMPEGInfo() { UE_LOG(LogFFMPEGMedia, Display, TEXT("AVFormat configuration: %s"), UTF8_TO_TCHAR(avformat_configuration())); if ( FormatContext ) { if(FormatContext->iformat) UE_LOG(LogFFMPEGMedia, Display, TEXT("Format Name: %s"), UTF8_TO_TCHAR(FormatContext->iformat->name)); FString sz_duration = TEXT(" Duration: "); if (FormatContext->duration != AV_NOPTS_VALUE) { int hours, mins, secs, us; int64_t duration = FormatContext->duration + (FormatContext->duration <= INT64_MAX - 5000 ? 5000 : 0); secs = duration / AV_TIME_BASE; us = duration % AV_TIME_BASE; mins = secs / 60; secs %= 60; hours = mins / 60; mins %= 60; sz_duration += FString::Printf(TEXT("%02d:%02d:%02d.%02d"), hours, mins, secs,(100 * us) / AV_TIME_BASE); } else { sz_duration += TEXT("N/A"); } if (FormatContext->start_time != AV_NOPTS_VALUE) { int secs, us; sz_duration += ", start: "; secs = llabs(FormatContext->start_time / AV_TIME_BASE); us = llabs(FormatContext->start_time % AV_TIME_BASE); sz_duration += FString::Printf(TEXT("%s%d.%06d"),FormatContext->start_time >= 0 ? TEXT("") : TEXT("-"),secs,(int)av_rescale(us, 1000000, AV_TIME_BASE)); } sz_duration += TEXT(", bitrate: "); if (FormatContext->bit_rate) { int64_t br = FormatContext->bit_rate / 1000; sz_duration += FString::Printf(TEXT("0x%08x%08x kb/s"), (uint32_t)(br >> 32),(uint32_t)(br & 0xFFFFFFFF)); } else sz_duration += TEXT("N/A"); sz_duration += TEXT("\n"); for (unsigned int i = 0; i < FormatContext->nb_chapters; i++) { AVChapter *ch = FormatContext->chapters[i]; sz_duration += FString::Printf(TEXT(" Chapter %d: "), i); sz_duration += FString::Printf(TEXT("start %f, "), ch->start * av_q2d(ch->time_base)); sz_duration += FString::Printf(TEXT("end %f\n"), ch->end * av_q2d(ch->time_base)); } if (FormatContext->nb_programs) { unsigned int total = 0; for (unsigned int j = 0; j < FormatContext->nb_programs; j++) { AVDictionaryEntry *name = av_dict_get(FormatContext->programs[j]->metadata, "name", NULL, 0); sz_duration += FString::Printf(TEXT(" Program %d %s\n"), FormatContext->programs[j]->id, name ? name->value : ""); total += FormatContext->programs[j]->nb_stream_indexes; } if (total < FormatContext->nb_streams) sz_duration += TEXT(" No Program\n"); } UE_LOG(LogFFMPEGMedia, Display, TEXT("%s"), *sz_duration); // UE_LOG(LogFFMPEGMedia, Display, TEXT("\n\nDefault format options")); // dumpOptions(avformat_get_class()); // // if (FormatContext->iformat && FormatContext->iformat->priv_class) { // UE_LOG(LogFFMPEGMedia, Display, TEXT("\n\nFormat Options\n")); // dumpOptions(FormatContext->iformat->priv_class); // } // // UE_LOG(LogFFMPEGMedia, Display, TEXT("\n\nDefault codec options\n")); // dumpOptions(avcodec_get_class()); // // if ( FormatContext->video_codec && FormatContext->video_codec->priv_class ) { // UE_LOG(LogFFMPEGMedia, Display, TEXT("\n\nVideo Codec\n")); // dumpOptions( FormatContext->video_codec->priv_class); // } // // if (FormatContext->audio_codec && FormatContext->audio_codec->priv_class) { // UE_LOG(LogFFMPEGMedia, Display, TEXT("\n\nAudio Codec\n")); // dumpOptions(FormatContext->audio_codec->priv_class); // } } }