/// @brief Constructor /// @param filename The filename to open YUV4MPEGVideoProvider::YUV4MPEGVideoProvider(agi::fs::path const& filename, std::string const&) { fps_rat.num = -1; fps_rat.den = 1; try { #ifdef WIN32 sf = _wfopen(filename.c_str(), L"rb"); #else sf = fopen(filename.c_str(), "rb"); #endif if (!sf) throw agi::fs::FileNotFound(filename); CheckFileFormat(); ParseFileHeader(ReadHeader(0)); if (w <= 0 || h <= 0) throw VideoOpenError("Invalid resolution"); if (fps_rat.num <= 0 || fps_rat.den <= 0) { fps_rat.num = 25; fps_rat.den = 1; LOG_D("provider/video/yuv4mpeg") << "framerate info unavailable, assuming 25fps"; } if (pixfmt == Y4M_PIXFMT_NONE) pixfmt = Y4M_PIXFMT_420JPEG; if (imode == Y4M_ILACE_NOTSET) imode = Y4M_ILACE_UNKNOWN; luma_sz = w * h; switch (pixfmt) { case Y4M_PIXFMT_420JPEG: case Y4M_PIXFMT_420MPEG2: case Y4M_PIXFMT_420PALDV: chroma_sz = (w * h) >> 2; break; case Y4M_PIXFMT_422: chroma_sz = (w * h) >> 1; break; /// @todo add support for more pixel formats default: throw VideoOpenError("Unsupported pixel format"); } frame_sz = luma_sz + chroma_sz*2; num_frames = IndexFile(); if (num_frames <= 0 || seek_table.empty()) throw VideoOpenError("Unable to determine file length"); fseeko(sf, 0, SEEK_SET); } catch (...) { if (sf) fclose(sf); throw; } }
/// @brief Parsing constructor /// @param filename /// DummyVideoProvider::DummyVideoProvider(wxString filename) { wxString params; if (!filename.StartsWith(_T("?dummy:"), ¶ms)) { throw agi::FileNotFoundError("Attempted creating dummy video provider with non-dummy filename"); } wxStringTokenizer t(params, _T(":")); if (t.CountTokens() < 7) { throw VideoOpenError("Too few fields in dummy video parameter list"); } double fps; long _frames, _width, _height, red, green, blue; bool pattern = false; wxString field = t.GetNextToken(); if (!field.ToDouble(&fps)) { throw VideoOpenError("Unable to parse fps field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&_frames)) { throw VideoOpenError("Unable to parse framecount field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&_width)) { throw VideoOpenError("Unable to parse width field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&_height)) { throw VideoOpenError("Unable to parse height field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&red)) { throw VideoOpenError("Unable to parse red colour field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&green)) { throw VideoOpenError("Unable to parse green colour field in dummy video parameter list"); } field = t.GetNextToken(); if (!field.ToLong(&blue)) { throw VideoOpenError("Unable to parse blue colour field in dummy video parameter list"); } field = t.GetNextToken(); if (field == _T("c")) { pattern = true; } Create(fps, _frames, _width, _height, wxColour(red, green, blue), pattern); }
/// @brief Read a frame or file header at a given file position /// @param startpos The byte offset at where to start reading /// @param reset_pos If true, the function will reset the file position to what it was before the function call before returning /// @return A list of parameters std::vector<std::string> YUV4MPEGVideoProvider::ReadHeader(int64_t startpos, bool reset_pos) { int64_t oldpos = ftello(sf); std::vector<std::string> tags; std::string curtag; int bytesread = 0; int buf; if (fseeko(sf, startpos, SEEK_SET)) throw VideoOpenError("YUV4MPEG video provider: ReadHeader: failed seeking to position %d"); //XXX:, startpos))); // read header until terminating newline (0x0A) is found while ((buf = fgetc(sf)) != 0x0A) { if (ferror(sf)) throw VideoOpenError("ReadHeader: Failed to read from file"); if (feof(sf)) { // you know, this is one of the places where it would be really nice // to be able to throw an exception object that tells the caller that EOF was reached LOG_D("provider/video/yuv4mpeg") << "ReadHeader: Reached EOF, returning"; break; } // some basic low-effort sanity checking if (buf == 0x00) throw VideoOpenError("ReadHeader: Malformed header (unexpected NUL)"); if (++bytesread >= YUV4MPEG_HEADER_MAXLEN) throw VideoOpenError("ReadHeader: Malformed header (no terminating newline found)"); // found a new tag if (buf == 0x20) { tags.push_back(curtag); curtag.clear(); } else curtag.append((const char*)buf); } // if only one tag with no trailing space was found (possible in the // FRAME header case), make sure we get it if (!curtag.empty()) { tags.push_back(curtag); curtag.clear(); } if (reset_pos) fseeko(sf, oldpos, SEEK_SET); return tags; }
/// @brief Parses a frame header /// @param tags The list of parameters to parse /// @return The flags set, as a binary mask /// This function is currently unimplemented (it will always return Y4M_FFLAG_NONE). YUV4MPEGVideoProvider::Y4M_FrameFlags YUV4MPEGVideoProvider::ParseFrameHeader(const std::vector<std::string>& tags) { if (tags.front() == "FRAME") throw VideoOpenError("ParseFrameHeader: malformed frame header (bad magic)"); /// @todo implement parsing of frame flags return Y4M_FFLAG_NONE; }
/// @brief Constructor /// @param filename The filename to open FFmpegSourceVideoProvider::FFmpegSourceVideoProvider(wxString filename) : VideoSource(NULL) , VideoInfo(NULL) , Width(-1) , Height(-1) , FrameNumber(-1) , COMInited(false) { #ifdef WIN32 HRESULT res = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (SUCCEEDED(res)) COMInited = true; else if (res != RPC_E_CHANGED_MODE) throw VideoOpenError("COM initialization failure"); #endif // initialize ffmpegsource // FIXME: CPU detection? #if FFMS_VERSION >= ((2 << 24) | (14 << 16) | (0 << 8) | 0) FFMS_Init(0, 1); #else FFMS_Init(0); #endif ErrInfo.Buffer = FFMSErrMsg; ErrInfo.BufferSize = sizeof(FFMSErrMsg); ErrInfo.ErrorType = FFMS_ERROR_SUCCESS; ErrInfo.SubType = FFMS_ERROR_SUCCESS; SetLogLevel(); // and here we go try { LoadVideo(filename); } catch (wxString const& err) { Close(); throw VideoOpenError(STD_STR(err)); } catch (...) { Close(); throw; } }
/// @brief Indexes the file /// @return The number of frames found in the file /// This function goes through the file, finds and parses all file and frame headers, /// and creates a seek table that lists the byte positions of all frames so seeking /// can easily be done. int YUV4MPEGVideoProvider::IndexFile() { int framecount = 0; // the ParseFileHeader() call in LoadVideo() will already have read // the file header for us and set the seek position correctly while (true) { int64_t curpos = ftello(sf); // update position if (curpos < 0) throw VideoOpenError("IndexFile: ftello failed"); // continue reading headers until no more are found std::vector<std::string> tags = ReadHeader(curpos); curpos = ftello(sf); if (curpos < 0) throw VideoOpenError("IndexFile: ftello failed"); if (tags.empty()) break; // no more headers Y4M_FrameFlags flags = Y4M_FFLAG_NOTSET; if (tags.front() == "YUV4MPEG2") { ParseFileHeader(tags); continue; } else if (tags.front() == "FRAME") flags = ParseFrameHeader(tags); if (flags == Y4M_FFLAG_NONE) { framecount++; seek_table.push_back(curpos); // seek to next frame header start position if (fseeko(sf, frame_sz, SEEK_CUR)) throw VideoOpenError("IndexFile: failed seeking to position " + std::to_string(curpos + frame_sz)); } else { /// @todo implement rff flags etc } } return framecount; }
/// @brief Get provider /// @param video /// @return /// VideoProvider *VideoProviderFactory::GetProvider(std::string video) { //XXX std::vector<std::string> list = GetClasses(OPT_GET("Video/Provider")->GetString()); std::vector<std::string> list = GetClasses("ffmpegsource"); if (video.find("?dummy") == 0) list.insert(list.begin(), "Dummy"); list.insert(list.begin(), "YUV4MPEG"); bool fileFound = false; bool fileSupported = false; std::string errors; errors.reserve(1024); for (int i = 0; i < (signed)list.size(); ++i) { std::string err; try { VideoProvider *provider = Create(list[i], video); LOG_I("manager/video/provider") << list[i] << ": opened " << video; if (provider->WantsCaching()) { return new VideoProviderCache(provider); } return provider; } catch (agi::FileNotFoundError const&) { err = list[i] + ": file not found."; // Keep trying other providers as this one may just not be able to // open a valid path } catch (VideoNotSupported const&) { fileFound = true; err = list[i] + ": video is not in a supported format."; } catch (VideoOpenError const& ex) { fileSupported = true; err = list[i] + ": " + ex.GetMessage(); } catch (agi::vfr::Error const& ex) { fileSupported = true; err = list[i] + ": " + ex.GetMessage(); } errors += err; errors += "\n"; LOG_D("manager/video/provider") << err; } // No provider could open the file LOG_E("manager/video/provider") << "Could not open " << video; std::string msg = "Could not open " + video + ":\n" + errors; if (!fileFound) throw agi::FileNotFoundError(video); if (!fileSupported) throw VideoNotSupported(msg); throw VideoOpenError(msg); }
/// @brief Constructor /// @param filename The filename to open FFmpegSourceVideoProvider::FFmpegSourceVideoProvider(wxString filename) try : VideoSource(NULL, FFMS_DestroyVideoSource) , VideoInfo(NULL) , Width(-1) , Height(-1) , FrameNumber(-1) { ErrInfo.Buffer = FFMSErrMsg; ErrInfo.BufferSize = sizeof(FFMSErrMsg); ErrInfo.ErrorType = FFMS_ERROR_SUCCESS; ErrInfo.SubType = FFMS_ERROR_SUCCESS; SetLogLevel(); // and here we go LoadVideo(filename); } catch (wxString const& err) { throw VideoOpenError(STD_STR(err)); } catch (const char * err) { throw VideoOpenError(err); }
/// @brief Read a frame or file header at a given file position /// @param startpos The byte offset at where to start reading /// @param reset_pos If true, the function will reset the file position to what it was before the function call before returning /// @return A list of parameters std::vector<std::string> YUV4MPEGVideoProvider::ReadHeader(int64_t startpos) { std::vector<std::string> tags; std::string curtag; int bytesread = 0; int buf; if (fseeko(sf, startpos, SEEK_SET)) throw VideoOpenError("YUV4MPEG video provider: ReadHeader: failed seeking to position " + std::to_string(startpos)); // read header until terminating newline (0x0A) is found while ((buf = fgetc(sf)) != 0x0A) { if (ferror(sf)) throw VideoOpenError("ReadHeader: Failed to read from file"); if (feof(sf)) throw VideoOpenError("ReadHeader: Reached eof while reading header"); // some basic low-effort sanity checking if (buf == 0x00) throw VideoOpenError("ReadHeader: Malformed header (unexpected NUL)"); if (++bytesread >= YUV4MPEG_HEADER_MAXLEN) throw VideoOpenError("ReadHeader: Malformed header (no terminating newline found)"); // found a new tag if (buf == 0x20 && !curtag.empty()) { tags.push_back(curtag); curtag.clear(); } else curtag.push_back(buf); } // if only one tag with no trailing space was found (possible in the // FRAME header case), make sure we get it if (!curtag.empty()) tags.push_back(curtag); return tags; }
/// @brief Get provider /// @param video /// @return /// VideoProvider *VideoProviderFactory::GetProvider(wxString video) { std::vector<std::string> list = GetClasses(OPT_GET("Video/Provider")->GetString()); if (video.StartsWith("?dummy")) list.insert(list.begin(), "Dummy"); list.insert(list.begin(), "YUV4MPEG"); bool fileFound = false; bool fileSupported = false; std::string errors; errors.reserve(1024); for (auto const& factory : list) { std::string err; try { VideoProvider *provider = Create(factory, video); LOG_I("manager/video/provider") << factory << ": opened " << from_wx(video); if (provider->WantsCaching()) { return new VideoProviderCache(provider); } return provider; } catch (agi::FileNotFoundError const&) { err = factory + ": file not found."; // Keep trying other providers as this one may just not be able to // open a valid path } catch (VideoNotSupported const&) { fileFound = true; err = factory + ": video is not in a supported format."; } catch (VideoOpenError const& ex) { fileSupported = true; err = factory + ": " + ex.GetMessage(); } catch (agi::vfr::Error const& ex) { fileSupported = true; err = factory + ": " + ex.GetMessage(); } errors += err; errors += "\n"; LOG_D("manager/video/provider") << err; } // No provider could open the file LOG_E("manager/video/provider") << "Could not open " << from_wx(video); std::string msg = "Could not open " + from_wx(video) + ":\n" + errors; if (!fileFound) throw agi::FileNotFoundError(from_wx(video)); if (!fileSupported) throw VideoNotSupported(msg); throw VideoOpenError(msg); }
std::unique_ptr<VideoProvider> VideoProviderFactory::GetProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) { auto preferred = OPT_GET("Video/Provider")->GetString(); auto sorted = GetSorted(boost::make_iterator_range(std::begin(providers), std::end(providers)), preferred); bool found = false; bool supported = false; std::string errors; errors.reserve(1024); for (auto factory : sorted) { std::string err; try { auto provider = factory->create(filename, colormatrix, br); if (!provider) continue; LOG_I("manager/video/provider") << factory->name << ": opened " << filename; return provider->WantsCaching() ? CreateCacheVideoProvider(std::move(provider)) : std::move(provider); } catch (agi::fs::FileNotFound const&) { err = "file not found."; // Keep trying other providers as this one may just not be able to // open a valid path } catch (VideoNotSupported const&) { found = true; err = "video is not in a supported format."; } catch (VideoOpenError const& ex) { supported = true; err = ex.GetMessage(); } catch (agi::vfr::Error const& ex) { supported = true; err = ex.GetMessage(); } errors += std::string(factory->name) + ": " + err + "\n"; LOG_D("manager/video/provider") << factory->name << ": " << err; } // No provider could open the file LOG_E("manager/video/provider") << "Could not open " << filename; std::string msg = "Could not open " + filename.string() + ":\n" + errors; if (!found) throw agi::fs::FileNotFound(filename.string()); if (!supported) throw VideoNotSupported(msg); throw VideoOpenError(msg); }
std::unique_ptr<VideoProvider> VideoProviderFactory::GetProvider(agi::fs::path const& video_file, std::string const& colormatrix) { std::vector<std::string> factories = GetClasses(OPT_GET("Video/Provider")->GetString()); factories.insert(factories.begin(), "YUV4MPEG"); factories.insert(factories.begin(), "Dummy"); bool found = false; bool supported = false; std::string errors; errors.reserve(1024); for (auto const& factory : factories) { std::string err; try { auto provider = Create(factory, video_file, colormatrix); LOG_I("manager/video/provider") << factory << ": opened " << video_file; return provider->WantsCaching() ? agi::util::make_unique<VideoProviderCache>(std::move(provider)) : std::move(provider); } catch (agi::fs::FileNotFound const&) { err = "file not found."; // Keep trying other providers as this one may just not be able to // open a valid path } catch (VideoNotSupported const&) { found = true; err = "video is not in a supported format."; } catch (VideoOpenError const& ex) { supported = true; err = ex.GetMessage(); } catch (agi::vfr::Error const& ex) { supported = true; err = ex.GetMessage(); } errors += factory + ": " + err + "\n"; LOG_D("manager/video/provider") << factory << ": " << err; } // No provider could open the file LOG_E("manager/video/provider") << "Could not open " << video_file; std::string msg = "Could not open " + video_file.string() + ":\n" + errors; if (!found) throw agi::fs::FileNotFound(video_file.string()); if (!supported) throw VideoNotSupported(msg); throw VideoOpenError(msg); }
AvisynthVideoProvider::AvisynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix) { agi::acs::CheckFileRead(filename); std::lock_guard<std::mutex> lock(avs.GetMutex()); #ifdef _WIN32 if (agi::fs::HasExtension(filename, "avi")) { // Try to read the keyframes before actually opening the file as trying // to open the file while it's already open can cause problems with // badly written VFW decoders AVIFileInit(); PAVIFILE pfile; long hr = AVIFileOpen(&pfile, filename.c_str(), OF_SHARE_DENY_WRITE, 0); if (hr) { warning = "Unable to open AVI file for reading keyframes:\n"; switch (hr) { case AVIERR_BADFORMAT: warning += "The file is corrupted, incomplete or has an otherwise bad format."; break; case AVIERR_MEMORY: warning += "The file could not be opened because of insufficient memory."; break; case AVIERR_FILEREAD: warning += "An error occurred reading the file. There might be a problem with the storage media."; break; case AVIERR_FILEOPEN: warning += "The file could not be opened. It might be in use by another application, or you do not have permission to access it."; break; case REGDB_E_CLASSNOTREG: warning += "There is no handler installed for the file extension. This might indicate a fundamental problem in your Video for Windows installation, and can be caused by extremely stripped Windows installations."; break; default: warning += "Unknown error."; break; } goto file_exit; } PAVISTREAM ppavi; if (hr = AVIFileGetStream(pfile, &ppavi, streamtypeVIDEO, 0)) { warning = "Unable to open AVI video stream for reading keyframes:\n"; switch (hr) { case AVIERR_NODATA: warning += "The file does not contain a usable video stream."; break; case AVIERR_MEMORY: warning += "Not enough memory."; break; default: warning += "Unknown error."; break; } goto file_release; } AVISTREAMINFO avis; if (FAILED(AVIStreamInfo(ppavi,&avis,sizeof(avis)))) { warning = "Unable to read keyframes from AVI file:\nCould not get stream information."; goto stream_release; } for (size_t i = 0; i < avis.dwLength; i++) { if (AVIStreamIsKeyFrame(ppavi, i)) keyframes.push_back(i); } // If every frame is a keyframe then just discard the keyframe data as it's useless if (keyframes.size() == (size_t)avis.dwLength) keyframes.clear(); // Clean up stream_release: AVIStreamRelease(ppavi); file_release: AVIFileRelease(pfile); file_exit: AVIFileExit(); } #endif try { auto script = Open(filename); // Check if video was loaded properly if (!script.IsClip() || !script.AsClip()->GetVideoInfo().HasVideo()) throw VideoNotSupported("No usable video found"); vi = script.AsClip()->GetVideoInfo(); if (!vi.IsRGB()) { /// @todo maybe read ColorMatrix hints for d2v files? AVSValue args[2] = { script, "Rec601" }; bool force_bt601 = OPT_GET("Video/Force BT.601")->GetBool() || colormatrix == "TV.601"; bool bt709 = vi.width > 1024 || vi.height >= 600; if (bt709 && (!force_bt601 || colormatrix == "TV.709")) { args[1] = "Rec709"; colorspace = "TV.709"; } else colorspace = "TV.601"; const char *argnames[2] = { 0, "matrix" }; script = avs.GetEnv()->Invoke("ConvertToRGB32", AVSValue(args, 2), argnames); } else colorspace = "None"; RGB32Video = avs.GetEnv()->Invoke("Cache", script).AsClip(); vi = RGB32Video->GetVideoInfo(); fps = (double)vi.fps_numerator / vi.fps_denominator; } catch (AvisynthError const& err) { throw VideoOpenError("Avisynth error: " + std::string(err.msg)); } }
/// @brief Opens video /// @param filename The filename to open void FFmpegSourceVideoProvider::LoadVideo(wxString filename) { wxString FileNameShort = wxFileName(filename).GetShortPath(); FFMS_Indexer *Indexer = FFMS_CreateIndexer(FileNameShort.utf8_str(), &ErrInfo); if (!Indexer) throw agi::FileNotFoundError(ErrInfo.Buffer); std::map<int,wxString> TrackList = GetTracksOfType(Indexer, FFMS_TYPE_VIDEO); if (TrackList.size() <= 0) throw VideoNotSupported("no video tracks found"); // initialize the track number to an invalid value so we can detect later on // whether the user actually had to choose a track or not int TrackNumber = -1; if (TrackList.size() > 1) { TrackNumber = AskForTrackSelection(TrackList, FFMS_TYPE_VIDEO); // if it's still -1 here, user pressed cancel if (TrackNumber == -1) throw agi::UserCancelException("video loading cancelled by user"); } // generate a name for the cache file wxString CacheName = GetCacheFilename(filename); // try to read index agi::scoped_holder<FFMS_Index*, void (FFMS_CC*)(FFMS_Index*)> Index(FFMS_ReadIndex(CacheName.utf8_str(), &ErrInfo), FFMS_DestroyIndex); if (Index && FFMS_IndexBelongsToFile(Index, FileNameShort.utf8_str(), &ErrInfo)) Index = NULL; // time to examine the index and check if the track we want is indexed // technically this isn't really needed since all video tracks should always be indexed, // but a bit of sanity checking never hurt anyone if (Index && TrackNumber >= 0) { FFMS_Track *TempTrackData = FFMS_GetTrackFromIndex(Index, TrackNumber); if (FFMS_GetNumFrames(TempTrackData) <= 0) Index = NULL; } // moment of truth if (!Index) { int TrackMask = FFMS_TRACKMASK_NONE; if (OPT_GET("Provider/FFmpegSource/Index All Tracks")->GetBool() || OPT_GET("Video/Open Audio")->GetBool()) TrackMask = FFMS_TRACKMASK_ALL; Index = DoIndexing(Indexer, CacheName, TrackMask, GetErrorHandlingMode()); } else { FFMS_CancelIndexing(Indexer); } // update access time of index file so it won't get cleaned away wxFileName(CacheName).Touch(); // we have now read the index and may proceed with cleaning the index cache CleanCache(); // track number still not set? if (TrackNumber < 0) { // just grab the first track TrackNumber = FFMS_GetFirstIndexedTrackOfType(Index, FFMS_TYPE_VIDEO, &ErrInfo); if (TrackNumber < 0) throw VideoNotSupported(std::string("Couldn't find any video tracks: ") + ErrInfo.Buffer); } // set thread count int Threads = OPT_GET("Provider/Video/FFmpegSource/Decoding Threads")->GetInt(); if (FFMS_GetVersion() < ((2 << 24) | (17 << 16) | (2 << 8) | 1) && FFMS_GetSourceType(Index) == FFMS_SOURCE_LAVF) Threads = 1; // set seekmode // TODO: give this its own option? int SeekMode; if (OPT_GET("Provider/Video/FFmpegSource/Unsafe Seeking")->GetBool()) SeekMode = FFMS_SEEK_UNSAFE; else SeekMode = FFMS_SEEK_NORMAL; VideoSource = FFMS_CreateVideoSource(FileNameShort.utf8_str(), TrackNumber, Index, Threads, SeekMode, &ErrInfo); if (!VideoSource) throw VideoOpenError(std::string("Failed to open video track: ") + ErrInfo.Buffer); // load video properties VideoInfo = FFMS_GetVideoProperties(VideoSource); const FFMS_Frame *TempFrame = FFMS_GetFrame(VideoSource, 0, &ErrInfo); if (!TempFrame) throw VideoOpenError(std::string("Failed to decode first frame: ") + ErrInfo.Buffer); Width = TempFrame->EncodedWidth; Height = TempFrame->EncodedHeight; if (VideoInfo->SARDen > 0 && VideoInfo->SARNum > 0) DAR = double(Width) * VideoInfo->SARNum / ((double)Height * VideoInfo->SARDen); else DAR = double(Width) / Height; // Assuming TV for unspecified wxString ColorRange = TempFrame->ColorRange == FFMS_CR_JPEG ? "PC" : "TV"; int CS = TempFrame->ColorSpace; #if FFMS_VERSION >= ((2 << 24) | (17 << 16) | (1 << 8) | 0) if (CS != FFMS_CS_RGB && CS != FFMS_CS_BT470BG && OPT_GET("Video/Force BT.601")->GetBool()) { if (FFMS_SetInputFormatV(VideoSource, FFMS_CS_BT470BG, TempFrame->ColorRange, FFMS_GetPixFmt(""), &ErrInfo)) throw VideoOpenError(std::string("Failed to set input format: ") + ErrInfo.Buffer); CS = FFMS_CS_BT470BG; } #endif switch (CS) { case FFMS_CS_RGB: ColorSpace = "None"; break; case FFMS_CS_BT709: ColorSpace = wxString::Format("%s.709", ColorRange); break; case FFMS_CS_UNSPECIFIED: ColorSpace = wxString::Format("%s.%s", ColorRange, Width > 1024 || Height >= 600 ? "709" : "601"); break; case FFMS_CS_FCC: ColorSpace = wxString::Format("%s.FCC", ColorRange); break; case FFMS_CS_BT470BG: case FFMS_CS_SMPTE170M: ColorSpace = wxString::Format("%s.601", ColorRange); break; case FFMS_CS_SMPTE240M: ColorSpace = wxString::Format("%s.240M", ColorRange); break; default: throw VideoOpenError("Unknown video color space"); break; } const int TargetFormat[] = { FFMS_GetPixFmt("bgra"), -1 }; if (FFMS_SetOutputFormatV2(VideoSource, TargetFormat, Width, Height, FFMS_RESIZER_BICUBIC, &ErrInfo)) { throw VideoOpenError(std::string("Failed to set output format: ") + ErrInfo.Buffer); } // get frame info data FFMS_Track *FrameData = FFMS_GetTrackFromVideo(VideoSource); if (FrameData == NULL) throw VideoOpenError("failed to get frame data"); const FFMS_TrackTimeBase *TimeBase = FFMS_GetTimeBase(FrameData); if (TimeBase == NULL) throw VideoOpenError("failed to get track time base"); const FFMS_FrameInfo *CurFrameData; // build list of keyframes and timecodes std::vector<int> TimecodesVector; for (int CurFrameNum = 0; CurFrameNum < VideoInfo->NumFrames; CurFrameNum++) { CurFrameData = FFMS_GetFrameInfo(FrameData, CurFrameNum); if (CurFrameData == NULL) { throw VideoOpenError(STD_STR(wxString::Format("Couldn't get info about frame %d", CurFrameNum))); } // keyframe? if (CurFrameData->KeyFrame) KeyFramesList.push_back(CurFrameNum); // calculate timestamp and add to timecodes vector int Timestamp = (int)((CurFrameData->PTS * TimeBase->Num) / TimeBase->Den); TimecodesVector.push_back(Timestamp); } if (TimecodesVector.size() < 2) Timecodes = 25.0; else Timecodes = agi::vfr::Framerate(TimecodesVector); FrameNumber = 0; }
/// @brief Parses a list of parameters and sets reader state accordingly /// @param tags The list of parameters to parse void YUV4MPEGVideoProvider::ParseFileHeader(const std::vector<std::string>& tags) { if (tags.size() <= 1) throw VideoOpenError("ParseFileHeader: contentless header"); if (tags.front() == "YUV4MPEG2") throw VideoOpenError("ParseFileHeader: malformed header (bad magic)"); // temporary stuff int t_w = -1; int t_h = -1; int t_fps_num = -1; int t_fps_den = -1; Y4M_InterlacingMode t_imode = Y4M_ILACE_NOTSET; Y4M_PixelFormat t_pixfmt = Y4M_PIXFMT_NONE; for (unsigned i = 1; i < tags.size(); i++) { std::string tag; tag = tags[i]; if (tag.find("W") == 0) { tag.erase(0,1); t_w = agi::util::strtoi(tag); if (t_w == 0) throw VideoOpenError("ParseFileHeader: invalid width"); } else if (tag.find("H") == 0) { tag.erase(0,1); t_h = agi::util::strtoi(tag); if (t_h == 0) throw VideoOpenError("ParseFileHeader: invalid height"); } else if (tag.find("F") == 0) { tag.erase(0,1); int i = tag.find(":"); std::string num(tag.substr(0,i)); std::string den(tag.substr(i+1,den.size())); t_fps_num = agi::util::strtoi(num); t_fps_den = agi::util::strtoi(den); if ((t_fps_num == 0) || (t_fps_den == 0)) throw VideoOpenError("ParseFileHeader: invalid framerate"); } else if (tag.find("C") == 0) { tag.erase(0,1); // technically this should probably be case sensitive, // but being liberal in what you accept doesn't hurt agi::util::str_lower(tag); if (tag == "420") t_pixfmt = Y4M_PIXFMT_420JPEG; // is this really correct? else if (tag == "420jpeg") t_pixfmt = Y4M_PIXFMT_420JPEG; else if (tag == "420mpeg2") t_pixfmt = Y4M_PIXFMT_420MPEG2; else if (tag == "420paldv") t_pixfmt = Y4M_PIXFMT_420PALDV; else if (tag == "411") t_pixfmt = Y4M_PIXFMT_411; else if (tag == "422") t_pixfmt = Y4M_PIXFMT_422; else if (tag == "444") t_pixfmt = Y4M_PIXFMT_444; else if (tag == "444alpha") t_pixfmt = Y4M_PIXFMT_444ALPHA; else if (tag == "mono") t_pixfmt = Y4M_PIXFMT_MONO; else throw VideoOpenError("ParseFileHeader: invalid or unknown colorspace"); } else if (tag.find("I") == 0) { tag.erase(0,1); agi::util::str_lower(tag); if (tag == "p") t_imode = Y4M_ILACE_PROGRESSIVE; else if (tag == "t") t_imode = Y4M_ILACE_TFF; else if (tag == "b") t_imode = Y4M_ILACE_BFF; else if (tag == "m") t_imode = Y4M_ILACE_MIXED; else if (tag == "?") t_imode = Y4M_ILACE_UNKNOWN; else throw VideoOpenError("ParseFileHeader: invalid or unknown interlacing mode"); } else LOG_D("provider/video/yuv4mpeg") << "Unparsed tag: " << tags[i].c_str(); } // The point of all this is to allow multiple YUV4MPEG2 headers in a single file // (can happen if you concat several files) as long as they have identical // header flags. The spec doesn't explicitly say you have to allow this, // but the "reference implementation" (mjpegtools) does, so I'm doing it too. if (inited) { if (t_w > 0 && t_w != w) throw VideoOpenError("ParseFileHeader: illegal width change"); if (t_h > 0 && t_h != h) throw VideoOpenError("ParseFileHeader: illegal height change"); if ((t_fps_num > 0 && t_fps_den > 0) && (t_fps_num != fps_rat.num || t_fps_den != fps_rat.den)) throw VideoOpenError("ParseFileHeader: illegal framerate change"); if (t_pixfmt != Y4M_PIXFMT_NONE && t_pixfmt != pixfmt) throw VideoOpenError("ParseFileHeader: illegal colorspace change"); if (t_imode != Y4M_ILACE_NOTSET && t_imode != imode) throw VideoOpenError("ParseFileHeader: illegal interlacing mode change"); } else { w = t_w; h = t_h; fps_rat.num = t_fps_num; fps_rat.den = t_fps_den; pixfmt = t_pixfmt != Y4M_PIXFMT_NONE ? t_pixfmt : Y4M_PIXFMT_420JPEG; imode = t_imode != Y4M_ILACE_NOTSET ? t_imode : Y4M_ILACE_UNKNOWN; fps = double(fps_rat.num) / fps_rat.den; inited = true; } }
/// @brief Parses a list of parameters and sets reader state accordingly /// @param tags The list of parameters to parse void YUV4MPEGVideoProvider::ParseFileHeader(const std::vector<wxString>& tags) { if (tags.size() <= 1) throw VideoOpenError("ParseFileHeader: contentless header"); if (tags.front().Cmp("YUV4MPEG2")) throw VideoOpenError("ParseFileHeader: malformed header (bad magic)"); // temporary stuff int t_w = -1; int t_h = -1; int t_fps_num = -1; int t_fps_den = -1; Y4M_InterlacingMode t_imode = Y4M_ILACE_NOTSET; Y4M_PixelFormat t_pixfmt = Y4M_PIXFMT_NONE; for (unsigned i = 1; i < tags.size(); i++) { wxString tag; long tmp_long1 = 0; long tmp_long2 = 0; if (tags[i].StartsWith("W", &tag)) { if (!tag.ToLong(&tmp_long1)) throw VideoOpenError("ParseFileHeader: invalid width"); t_w = (int)tmp_long1; } else if (tags[i].StartsWith("H", &tag)) { if (!tag.ToLong(&tmp_long1)) throw VideoOpenError("ParseFileHeader: invalid height"); t_h = (int)tmp_long1; } else if (tags[i].StartsWith("F", &tag)) { if (!(tag.BeforeFirst(':')).ToLong(&tmp_long1) && tag.AfterFirst(':').ToLong(&tmp_long2)) throw VideoOpenError("ParseFileHeader: invalid framerate"); t_fps_num = (int)tmp_long1; t_fps_den = (int)tmp_long2; } else if (tags[i].StartsWith("C", &tag)) { // technically this should probably be case sensitive, // but being liberal in what you accept doesn't hurt tag.MakeLower(); if (tag == "420") t_pixfmt = Y4M_PIXFMT_420JPEG; // is this really correct? else if (tag == "420jpeg") t_pixfmt = Y4M_PIXFMT_420JPEG; else if (tag == "420mpeg2") t_pixfmt = Y4M_PIXFMT_420MPEG2; else if (tag == "420paldv") t_pixfmt = Y4M_PIXFMT_420PALDV; else if (tag == "411") t_pixfmt = Y4M_PIXFMT_411; else if (tag == "422") t_pixfmt = Y4M_PIXFMT_422; else if (tag == "444") t_pixfmt = Y4M_PIXFMT_444; else if (tag == "444alpha") t_pixfmt = Y4M_PIXFMT_444ALPHA; else if (tag == "mono") t_pixfmt = Y4M_PIXFMT_MONO; else throw VideoOpenError("ParseFileHeader: invalid or unknown colorspace"); } else if (tags[i].StartsWith("I", &tag)) { tag.MakeLower(); if (tag == "p") t_imode = Y4M_ILACE_PROGRESSIVE; else if (tag == "t") t_imode = Y4M_ILACE_TFF; else if (tag == "b") t_imode = Y4M_ILACE_BFF; else if (tag == "m") t_imode = Y4M_ILACE_MIXED; else if (tag == "?") t_imode = Y4M_ILACE_UNKNOWN; else throw VideoOpenError("ParseFileHeader: invalid or unknown interlacing mode"); } else LOG_D("provider/video/yuv4mpeg") << "Unparsed tag: " << STD_STR(tags[i]); } // The point of all this is to allow multiple YUV4MPEG2 headers in a single file // (can happen if you concat several files) as long as they have identical // header flags. The spec doesn't explicitly say you have to allow this, // but the "reference implementation" (mjpegtools) does, so I'm doing it too. if (inited) { if (t_w > 0 && t_w != w) throw VideoOpenError("ParseFileHeader: illegal width change"); if (t_h > 0 && t_h != h) throw VideoOpenError("ParseFileHeader: illegal height change"); if ((t_fps_num > 0 && t_fps_den > 0) && (t_fps_num != fps_rat.num || t_fps_den != fps_rat.den)) throw VideoOpenError("ParseFileHeader: illegal framerate change"); if (t_pixfmt != Y4M_PIXFMT_NONE && t_pixfmt != pixfmt) throw VideoOpenError("ParseFileHeader: illegal colorspace change"); if (t_imode != Y4M_ILACE_NOTSET && t_imode != imode) throw VideoOpenError("ParseFileHeader: illegal interlacing mode change"); } else { w = t_w; h = t_h; fps_rat.num = t_fps_num; fps_rat.den = t_fps_den; pixfmt = t_pixfmt != Y4M_PIXFMT_NONE ? t_pixfmt : Y4M_PIXFMT_420JPEG; imode = t_imode != Y4M_ILACE_NOTSET ? t_imode : Y4M_ILACE_UNKNOWN; fps = double(fps_rat.num) / fps_rat.den; inited = true; } }
/// @brief Opens video /// @param filename The filename to open void FFmpegSourceVideoProvider::LoadVideo(wxString filename) { wxString FileNameShort = wxFileName(filename).GetShortPath(); FFMS_Indexer *Indexer = FFMS_CreateIndexer(FileNameShort.utf8_str(), &ErrInfo); if (Indexer == NULL) { throw agi::FileNotFoundError(ErrInfo.Buffer); } std::map<int,wxString> TrackList = GetTracksOfType(Indexer, FFMS_TYPE_VIDEO); if (TrackList.size() <= 0) throw VideoNotSupported("no video tracks found"); // initialize the track number to an invalid value so we can detect later on // whether the user actually had to choose a track or not int TrackNumber = -1; if (TrackList.size() > 1) { TrackNumber = AskForTrackSelection(TrackList, FFMS_TYPE_VIDEO); // if it's still -1 here, user pressed cancel if (TrackNumber == -1) throw agi::UserCancelException("video loading cancelled by user"); } // generate a name for the cache file wxString CacheName = GetCacheFilename(filename); // try to read index FFMS_Index *Index = NULL; Index = FFMS_ReadIndex(CacheName.utf8_str(), &ErrInfo); bool IndexIsValid = false; if (Index != NULL) { if (FFMS_IndexBelongsToFile(Index, FileNameShort.utf8_str(), &ErrInfo)) { FFMS_DestroyIndex(Index); Index = NULL; } else IndexIsValid = true; } // time to examine the index and check if the track we want is indexed // technically this isn't really needed since all video tracks should always be indexed, // but a bit of sanity checking never hurt anyone if (IndexIsValid && TrackNumber >= 0) { FFMS_Track *TempTrackData = FFMS_GetTrackFromIndex(Index, TrackNumber); if (FFMS_GetNumFrames(TempTrackData) <= 0) { IndexIsValid = false; FFMS_DestroyIndex(Index); Index = NULL; } } // moment of truth if (!IndexIsValid) { int TrackMask = OPT_GET("Provider/FFmpegSource/Index All Tracks")->GetBool() ? FFMS_TRACKMASK_ALL : FFMS_TRACKMASK_NONE; try { // ignore audio decoding errors here, we don't care right now Index = DoIndexing(Indexer, CacheName, TrackMask, FFMS_IEH_IGNORE); } catch (wxString err) { throw VideoOpenError(STD_STR(err)); } } // update access time of index file so it won't get cleaned away wxFileName(CacheName).Touch(); // we have now read the index and may proceed with cleaning the index cache if (!CleanCache()) { //do something? } // track number still not set? if (TrackNumber < 0) { // just grab the first track TrackNumber = FFMS_GetFirstIndexedTrackOfType(Index, FFMS_TYPE_VIDEO, &ErrInfo); if (TrackNumber < 0) { FFMS_DestroyIndex(Index); Index = NULL; throw VideoNotSupported(std::string("Couldn't find any video tracks: ") + ErrInfo.Buffer); } } // set thread count int Threads = OPT_GET("Provider/Video/FFmpegSource/Decoding Threads")->GetInt(); // set seekmode // TODO: give this its own option? int SeekMode; if (OPT_GET("Provider/Video/FFmpegSource/Unsafe Seeking")->GetBool()) SeekMode = FFMS_SEEK_UNSAFE; else SeekMode = FFMS_SEEK_NORMAL; VideoSource = FFMS_CreateVideoSource(FileNameShort.utf8_str(), TrackNumber, Index, Threads, SeekMode, &ErrInfo); FFMS_DestroyIndex(Index); Index = NULL; if (VideoSource == NULL) { throw VideoOpenError(std::string("Failed to open video track: ") + ErrInfo.Buffer); } // load video properties VideoInfo = FFMS_GetVideoProperties(VideoSource); const FFMS_Frame *TempFrame = FFMS_GetFrame(VideoSource, 0, &ErrInfo); if (TempFrame == NULL) { throw VideoOpenError(std::string("Failed to decode first frame: ") + ErrInfo.Buffer); } Width = TempFrame->EncodedWidth; Height = TempFrame->EncodedHeight; if (FFMS_SetOutputFormatV(VideoSource, 1LL << FFMS_GetPixFmt("bgra"), Width, Height, FFMS_RESIZER_BICUBIC, &ErrInfo)) { throw VideoOpenError(std::string("Failed to set output format: ") + ErrInfo.Buffer); } // get frame info data FFMS_Track *FrameData = FFMS_GetTrackFromVideo(VideoSource); if (FrameData == NULL) throw VideoOpenError("failed to get frame data"); const FFMS_TrackTimeBase *TimeBase = FFMS_GetTimeBase(FrameData); if (TimeBase == NULL) throw VideoOpenError("failed to get track time base"); const FFMS_FrameInfo *CurFrameData; // build list of keyframes and timecodes std::vector<int> TimecodesVector; for (int CurFrameNum = 0; CurFrameNum < VideoInfo->NumFrames; CurFrameNum++) { CurFrameData = FFMS_GetFrameInfo(FrameData, CurFrameNum); if (CurFrameData == NULL) { throw VideoOpenError(STD_STR(wxString::Format(L"Couldn't get info about frame %d", CurFrameNum))); } // keyframe? if (CurFrameData->KeyFrame) KeyFramesList.push_back(CurFrameNum); // calculate timestamp and add to timecodes vector int Timestamp = (int)((CurFrameData->PTS * TimeBase->Num) / TimeBase->Den); TimecodesVector.push_back(Timestamp); } Timecodes = agi::vfr::Framerate(TimecodesVector); FrameNumber = 0; }
/// @brief Constructor /// @param filename The filename to open YUV4MPEGVideoProvider::YUV4MPEGVideoProvider(wxString fname) : sf(NULL) , inited(false) , w (0) , h (0) , num_frames(-1) , cur_fn(-1) , pixfmt(Y4M_PIXFMT_NONE) , imode(Y4M_ILACE_NOTSET) { fps_rat.num = -1; fps_rat.den = 1; try { wxString filename = wxFileName(fname).GetShortPath(); #ifdef WIN32 sf = _wfopen(filename.wc_str(), L"rb"); #else sf = fopen(filename.utf8_str(), "rb"); #endif if (sf == NULL) throw agi::FileNotFoundError(STD_STR(fname)); CheckFileFormat(); ParseFileHeader(ReadHeader(0, false)); if (w <= 0 || h <= 0) throw VideoOpenError("Invalid resolution"); if (fps_rat.num <= 0 || fps_rat.den <= 0) { fps_rat.num = 25; fps_rat.den = 1; LOG_D("provider/video/yuv4mpeg") << "framerate info unavailable, assuming 25fps"; } if (pixfmt == Y4M_PIXFMT_NONE) pixfmt = Y4M_PIXFMT_420JPEG; if (imode == Y4M_ILACE_NOTSET) imode = Y4M_ILACE_UNKNOWN; luma_sz = w * h; switch (pixfmt) { case Y4M_PIXFMT_420JPEG: case Y4M_PIXFMT_420MPEG2: case Y4M_PIXFMT_420PALDV: chroma_sz = (w * h) >> 2; break; case Y4M_PIXFMT_422: chroma_sz = (w * h) >> 1; break; /// @todo add support for more pixel formats default: throw VideoOpenError("Unsupported pixel format"); } frame_sz = luma_sz + chroma_sz*2; num_frames = IndexFile(); if (num_frames <= 0 || seek_table.empty()) throw VideoOpenError("Unable to determine file length"); cur_fn = 0; fseeko(sf, 0, SEEK_SET); } catch (...) { if (sf) fclose(sf); throw; } }