// Copyright (c) 2012, Thomas Goyne // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // Aegisub Project http://www.aegisub.org/ /// @file threaded_frame_source.cpp /// @see threaded_frame_source.h /// @ingroup video /// #include "threaded_frame_source.h" #include #include #include #include #include "ass_dialogue.h" #include "ass_exporter.h" #include "ass_file.h" #include "compat.h" #include "include/aegisub/context.h" #include "include/aegisub/subtitles_provider.h" #include "video_frame.h" #include "video_provider_manager.h" // Test if a line is a dialogue line which is not visible at the given time struct invisible_line : public std::unary_function { double time; invisible_line(double time) : time(time * 1000.) { } bool operator()(AssEntry const& entry) const { const AssDialogue *diag = dynamic_cast(&entry); return diag && (diag->Start > time || diag->End <= time); } }; std::shared_ptr ThreadedFrameSource::ProcFrame(int frameNum, double time, bool raw) { std::shared_ptr frame(new AegiVideoFrame, [](AegiVideoFrame *frame) { frame->Clear(); delete frame; }); { wxMutexLocker locker(providerMutex); try { frame->CopyFrom(videoProvider->GetFrame(frameNum)); } catch (VideoProviderError const& err) { throw VideoProviderErrorEvent(err); } } // This deliberately results in a call to LoadSubtitles while a render // is pending making the queued render use the new file if (!raw && provider) { try { wxMutexLocker locker(fileMutex); if (subs.get() && singleFrame != frameNum) { // Generally edits and seeks come in groups; if the last thing done // was seek it is more likely that the user will seek again and // vice versa. As such, if this is the first frame requested after // an edit, only export the currently visible lines (because the // other lines will probably not be viewed before the file changes // again), and if it's a different frame, export the entire file. if (singleFrame == -1) { // This will crash if any of the export filters try to use // anything but the subtitles, but that wouldn't be safe to // do anyway agi::Context c; memset(&c, 0, sizeof c); c.ass = subs.get(); AssExporter exporter(&c); exporter.AddAutoFilters(); exporter.ExportTransform(); singleFrame = frameNum; // Copying a nontrivially sized AssFile is fairly slow, so // instead muck around with its innards to just temporarily // remove the non-visible lines without deleting them std::deque full; for (auto& line : subs->Line) full.push_back(&line); subs->Line.remove_if(invisible_line(time)); try { provider->LoadSubtitles(subs.get()); subs->Line.clear(); boost::push_back(subs->Line, full | boost::adaptors::indirected); } catch(...) { subs->Line.clear(); boost::push_back(subs->Line, full | boost::adaptors::indirected); throw; } } else { provider->LoadSubtitles(subs.get()); subs.reset(); } } } catch (wxString const& err) { throw SubtitlesProviderErrorEvent(err); } provider->DrawSubtitles(*frame, time); } return frame; } void *ThreadedFrameSource::Entry() { while (!TestDestroy()) { double time; int frameNum; std::unique_ptr newSubs; { wxMutexLocker jobLocker(jobMutex); if (!run) return EXIT_SUCCESS; if (nextTime == -1.) { jobReady.Wait(); continue; } time = nextTime; frameNum = nextFrame; nextTime = -1.; newSubs = move(nextSubs); } if (newSubs) { wxMutexLocker fileLocker(fileMutex); subs = move(newSubs); singleFrame = -1; } try { FrameReadyEvent *evt = new FrameReadyEvent(ProcFrame(frameNum, time), time); evt->SetEventType(EVT_FRAME_READY); parent->QueueEvent(evt); } catch (wxEvent const& err) { // Pass error back to parent thread parent->QueueEvent(err.Clone()); } } return EXIT_SUCCESS; } static SubtitlesProvider *get_subs_provider(wxEvtHandler *parent) { try { return SubtitlesProviderFactory::GetProvider(); } catch (wxString const& err) { parent->AddPendingEvent(SubtitlesProviderErrorEvent(err)); return 0; } } ThreadedFrameSource::ThreadedFrameSource(wxString videoFileName, wxEvtHandler *parent) : wxThread(wxTHREAD_JOINABLE) , provider(get_subs_provider(parent)) , videoProvider(VideoProviderFactory::GetProvider(videoFileName)) , parent(parent) , nextFrame(-1) , nextTime(-1.) , singleFrame(-1) , jobReady(jobMutex) , run(true) { Create(); Run(); } ThreadedFrameSource::~ThreadedFrameSource() { { wxMutexLocker locker(jobMutex); run = false; jobReady.Signal(); } Wait(); } void ThreadedFrameSource::LoadSubtitles(AssFile *subs) throw() { subs = new AssFile(*subs); wxMutexLocker locker(jobMutex); // Set nextSubs and let the worker thread move it to subs so that we don't // have to lock fileMutex on the GUI thread, as that can be locked for // extended periods of time with slow-rendering subtitles nextSubs.reset(subs); } void ThreadedFrameSource::RequestFrame(int frame, double time) throw() { wxMutexLocker locker(jobMutex); nextTime = time; nextFrame = frame; jobReady.Signal(); } std::shared_ptr ThreadedFrameSource::GetFrame(int frame, double time, bool raw) { return ProcFrame(frame, time, raw); } wxDEFINE_EVENT(EVT_FRAME_READY, FrameReadyEvent); wxDEFINE_EVENT(EVT_VIDEO_ERROR, VideoProviderErrorEvent); wxDEFINE_EVENT(EVT_SUBTITLES_ERROR, SubtitlesProviderErrorEvent); VideoProviderErrorEvent::VideoProviderErrorEvent(VideoProviderError const& err) : agi::Exception(err.GetMessage(), &err) { SetEventType(EVT_VIDEO_ERROR); } SubtitlesProviderErrorEvent::SubtitlesProviderErrorEvent(wxString err) : agi::Exception(STD_STR(err), nullptr) { SetEventType(EVT_SUBTITLES_ERROR); }