1
// Copyright (c) 2005-2007, Rodrigo Braz Monteiro
2
// All rights reserved.
4
// Redistribution and use in source and binary forms, with or without
5
// modification, are permitted provided that the following conditions are met:
7
// * Redistributions of source code must retain the above copyright notice,
8
// this list of conditions and the following disclaimer.
9
// * Redistributions in binary form must reproduce the above copyright notice,
10
// this list of conditions and the following disclaimer in the documentation
11
// and/or other materials provided with the distribution.
12
// * Neither the name of the Aegisub Group nor the names of its contributors
13
// may be used to endorse or promote products derived from this software
14
// without specific prior written permission.
16
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
// POSSIBILITY OF SUCH DAMAGE.
28
// Aegisub Project http://www.aegisub.org/
30
/// @file video_context.cpp
31
/// @brief Keep track of loaded video
37
#include "video_context.h"
39
#include "ass_dialogue.h"
42
#include "audio_controller.h"
44
#include "include/aegisub/context.h"
45
#include "include/aegisub/video_provider.h"
48
#include "selection_controller.h"
49
#include "subs_controller.h"
50
#include "time_range.h"
51
#include "threaded_frame_source.h"
53
#include "video_frame.h"
55
#include <libaegisub/fs.h>
56
#include <libaegisub/keyframe.h>
57
#include <libaegisub/path.h>
59
#include <wx/msgdlg.h>
61
VideoContext::VideoContext()
63
, playAudioOnStep(OPT_GET("Audio/Plays When Stepping Video"))
65
Bind(EVT_VIDEO_ERROR, &VideoContext::OnVideoError, this);
66
Bind(EVT_SUBTITLES_ERROR, &VideoContext::OnSubtitlesError, this);
67
Bind(wxEVT_TIMER, &VideoContext::OnPlayTimer, this);
69
OPT_SUB("Subtitle/Provider", &VideoContext::Reload, this);
70
OPT_SUB("Video/Provider", &VideoContext::Reload, this);
72
// It would be nice to find a way to move these to the individual providers
73
OPT_SUB("Provider/Avisynth/Allow Ancient", &VideoContext::Reload, this);
74
OPT_SUB("Provider/Avisynth/Memory Max", &VideoContext::Reload, this);
76
OPT_SUB("Provider/Video/FFmpegSource/Decoding Threads", &VideoContext::Reload, this);
77
OPT_SUB("Provider/Video/FFmpegSource/Unsafe Seeking", &VideoContext::Reload, this);
78
OPT_SUB("Video/Force BT.601", &VideoContext::Reload, this);
81
VideoContext::~VideoContext () {
84
VideoContext *VideoContext::Get() {
85
static VideoContext instance;
89
void VideoContext::Reset() {
90
config::path->SetToken("?video", "");
96
// Clean up video data
97
video_filename.clear();
101
video_provider = nullptr;
104
keyframes_filename.clear();
105
video_fps = agi::vfr::Framerate();
106
KeyframesOpen(keyframes);
107
if (!ovr_fps.IsLoaded()) TimecodesOpen(video_fps);
110
void VideoContext::SetContext(agi::Context *context) {
111
this->context = context;
112
context->ass->AddCommitListener(&VideoContext::OnSubtitlesCommit, this);
113
context->subsController->AddFileSaveListener(&VideoContext::OnSubtitlesSave, this);
116
void VideoContext::SetVideo(const agi::fs::path &filename) {
118
if (filename.empty()) {
123
bool commit_subs = false;
125
provider.reset(new ThreadedFrameSource(filename, context->ass->GetScriptInfo("YCbCr Matrix"), this));
126
video_provider = provider->GetVideoProvider();
127
video_filename = filename;
129
// Check that the script resolution matches the video resolution
130
int sx = context->ass->GetScriptInfoAsInt("PlayResX");
131
int sy = context->ass->GetScriptInfoAsInt("PlayResY");
133
int vy = GetHeight();
135
// If the script resolution hasn't been set at all just force it to the
137
if (sx == 0 && sy == 0) {
138
context->ass->SetScriptInfo("PlayResX", std::to_string(vx));
139
context->ass->SetScriptInfo("PlayResY", std::to_string(vy));
142
// If it has been set to something other than a multiple of the video
143
// resolution, ask the user if they want it to be fixed
144
else if (sx % vx != 0 || sy % vy != 0) {
145
switch (OPT_GET("Video/Check Script Res")->GetInt()) {
146
case 1: // Ask to change on mismatch
147
if (wxYES != wxMessageBox(
148
wxString::Format(_("The resolution of the loaded video and the resolution specified for the subtitles don't match.\n\nVideo resolution:\t%d x %d\nScript resolution:\t%d x %d\n\nChange subtitles resolution to match video?"), vx, vy, sx, sy),
149
_("Resolution mismatch"),
154
// Fallthrough to case 2
155
case 2: // Always change script res
156
context->ass->SetScriptInfo("PlayResX", std::to_string(vx));
157
context->ass->SetScriptInfo("PlayResY", std::to_string(vy));
160
default: // Never change
165
keyframes = video_provider->GetKeyFrames();
168
video_fps = video_provider->GetFPS();
169
if (ovr_fps.IsLoaded()) {
170
int ovr = wxMessageBox(_("You already have timecodes loaded. Would you like to replace them with timecodes from the video file?"), _("Replace timecodes?"), wxYES_NO | wxICON_QUESTION);
172
ovr_fps = agi::vfr::Framerate();
173
timecodes_filename.clear();
178
double dar = video_provider->GetDAR();
183
config::mru->Add("Video", filename);
184
config::path->SetToken("?video", filename);
187
std::string warning = video_provider->GetWarning();
188
if (!warning.empty())
189
wxMessageBox(to_wx(warning), "Warning", wxICON_WARNING | wxOK);
191
has_subtitles = false;
192
if (agi::fs::HasExtension(filename, "mkv"))
193
has_subtitles = MatroskaWrapper::HasSubtitles(filename);
195
provider->LoadSubtitles(context->ass);
197
KeyframesOpen(keyframes);
198
TimecodesOpen(FPS());
200
catch (agi::UserCancelException const&) { }
201
catch (agi::fs::FileSystemError const& err) {
202
config::mru->Remove("Video", filename);
203
wxMessageBox(to_wx(err.GetMessage()), "Error setting video", wxOK | wxICON_ERROR | wxCENTER);
205
catch (VideoProviderError const& err) {
206
wxMessageBox(to_wx(err.GetMessage()), "Error setting video", wxOK | wxICON_ERROR | wxCENTER);
210
context->ass->Commit(_("change script resolution"), AssFile::COMMIT_SCRIPTINFO);
215
void VideoContext::Reload() {
218
SetVideo(agi::fs::path(video_filename)); // explicitly copy videoFile since it's cleared in SetVideo
223
void VideoContext::OnSubtitlesCommit(int type, std::set<const AssEntry *> const& changed) {
224
if (!IsLoaded()) return;
226
if (changed.empty() || no_amend)
227
provider->LoadSubtitles(context->ass);
229
provider->UpdateSubtitles(context->ass, changed);
231
GetFrameAsync(frame_n);
236
void VideoContext::OnSubtitlesSave() {
239
context->ass->SetScriptInfo("VFR File", config::path->MakeRelative(GetTimecodesName(), "?script").generic_string());
240
context->ass->SetScriptInfo("Keyframes File", config::path->MakeRelative(GetKeyFramesName(), "?script").generic_string());
243
context->ass->SetScriptInfo("Video File", "");
244
context->ass->SaveUIState("Video Aspect Ratio", "");
245
context->ass->SaveUIState("Video Position", "");
250
if (ar_type == AspectRatio::Custom)
251
ar = "c" + std::to_string(ar_value);
253
ar = std::to_string((int)ar_type);
255
context->ass->SetScriptInfo("Video File", config::path->MakeRelative(video_filename, "?script").generic_string());
256
auto matrix = video_provider->GetColorSpace();
258
context->ass->SetScriptInfo("YCbCr Matrix", matrix);
259
context->ass->SaveUIState("Video Aspect Ratio", ar);
260
context->ass->SaveUIState("Video Position", std::to_string(frame_n));
263
void VideoContext::JumpToFrame(int n) {
264
if (!IsLoaded()) return;
266
bool was_playing = IsPlaying();
270
frame_n = mid(0, n, GetLength() - 1);
272
GetFrameAsync(frame_n);
279
void VideoContext::JumpToTime(int ms, agi::vfr::Time end) {
280
JumpToFrame(FrameAtTime(ms, end));
283
void VideoContext::GetFrameAsync(int n) {
284
provider->RequestFrame(n, TimeAtFrame(n));
287
std::shared_ptr<VideoFrame> VideoContext::GetFrame(int n, bool raw) {
288
return provider->GetFrame(n, TimeAtFrame(n), raw);
291
int VideoContext::GetWidth() const { return video_provider->GetWidth(); }
292
int VideoContext::GetHeight() const { return video_provider->GetHeight(); }
293
int VideoContext::GetLength() const { return video_provider->GetFrameCount(); }
295
void VideoContext::NextFrame() {
296
if (!video_provider || IsPlaying() || frame_n == video_provider->GetFrameCount())
299
JumpToFrame(frame_n + 1);
300
if (playAudioOnStep->GetBool())
301
context->audioController->PlayRange(TimeRange(TimeAtFrame(frame_n - 1), TimeAtFrame(frame_n)));
304
void VideoContext::PrevFrame() {
305
if (!video_provider || IsPlaying() || frame_n == 0)
308
JumpToFrame(frame_n - 1);
309
if (playAudioOnStep->GetBool())
310
context->audioController->PlayRange(TimeRange(TimeAtFrame(frame_n), TimeAtFrame(frame_n + 1)));
313
void VideoContext::Play() {
319
if (!IsLoaded()) return;
321
start_ms = TimeAtFrame(frame_n);
322
end_frame = GetLength() - 1;
324
context->audioController->PlayToEnd(start_ms);
326
playback_start_time = std::chrono::steady_clock::now();
330
void VideoContext::PlayLine() {
333
AssDialogue *curline = context->selectionController->GetActiveLine();
334
if (!curline) return;
336
context->audioController->PlayRange(TimeRange(curline->Start, curline->End));
338
// Round-trip conversion to convert start to exact
339
int startFrame = FrameAtTime(context->selectionController->GetActiveLine()->Start, agi::vfr::START);
340
start_ms = TimeAtFrame(startFrame);
341
end_frame = FrameAtTime(context->selectionController->GetActiveLine()->End, agi::vfr::END) + 1;
343
JumpToFrame(startFrame);
345
playback_start_time = std::chrono::steady_clock::now();
349
void VideoContext::Stop() {
352
context->audioController->Stop();
356
void VideoContext::OnPlayTimer(wxTimerEvent &) {
357
using namespace std::chrono;
358
int next_frame = FrameAtTime(start_ms + duration_cast<milliseconds>(steady_clock::now() - playback_start_time).count());
359
if (next_frame == frame_n) return;
361
if (next_frame >= end_frame)
364
frame_n = next_frame;
365
GetFrameAsync(frame_n);
370
double VideoContext::GetARFromType(AspectRatio type) const {
372
case AspectRatio::Default: return (double)GetWidth()/(double)GetHeight();
373
case AspectRatio::Fullscreen: return 4.0/3.0;
374
case AspectRatio::Widescreen: return 16.0/9.0;
375
case AspectRatio::Cinematic: return 2.35;
377
throw agi::InternalError("Bad AR type", nullptr);
380
void VideoContext::SetAspectRatio(double value) {
381
ar_type = AspectRatio::Custom;
382
ar_value = mid(.5, value, 5.);
383
ARChange(ar_type, ar_value);
386
void VideoContext::SetAspectRatio(AspectRatio type) {
387
ar_value = mid(.5, GetARFromType(type), 5.);
389
ARChange(ar_type, ar_value);
392
void VideoContext::LoadKeyframes(agi::fs::path const& filename) {
393
if (filename == keyframes_filename || filename.empty()) return;
395
keyframes = agi::keyframe::Load(filename);
396
keyframes_filename = filename;
397
KeyframesOpen(keyframes);
398
config::mru->Add("Keyframes", filename);
400
catch (agi::keyframe::Error const& err) {
401
wxMessageBox(to_wx(err.GetMessage()), "Error opening keyframes file", wxOK | wxICON_ERROR | wxCENTER, context->parent);
402
config::mru->Remove("Keyframes", filename);
404
catch (agi::fs::FileSystemError const& err) {
405
wxMessageBox(to_wx(err.GetMessage()), "Error opening keyframes file", wxOK | wxICON_ERROR | wxCENTER, context->parent);
406
config::mru->Remove("Keyframes", filename);
410
void VideoContext::SaveKeyframes(agi::fs::path const& filename) {
411
agi::keyframe::Save(filename, GetKeyFrames());
412
config::mru->Add("Keyframes", filename);
415
void VideoContext::CloseKeyframes() {
416
keyframes_filename.clear();
418
keyframes = video_provider->GetKeyFrames();
421
KeyframesOpen(keyframes);
424
void VideoContext::LoadTimecodes(agi::fs::path const& filename) {
425
if (filename == timecodes_filename || filename.empty()) return;
427
ovr_fps = agi::vfr::Framerate(filename);
428
timecodes_filename = filename;
429
config::mru->Add("Timecodes", filename);
430
OnSubtitlesCommit(0, std::set<const AssEntry*>());
431
TimecodesOpen(ovr_fps);
433
catch (agi::fs::FileSystemError const& err) {
434
wxMessageBox(to_wx(err.GetMessage()), "Error opening timecodes file", wxOK | wxICON_ERROR | wxCENTER, context->parent);
435
config::mru->Remove("Timecodes", filename);
437
catch (const agi::vfr::Error& e) {
438
wxLogError("Timecode file parse error: %s", to_wx(e.GetMessage()));
439
config::mru->Remove("Timecodes", filename);
442
void VideoContext::SaveTimecodes(agi::fs::path const& filename) {
444
FPS().Save(filename, IsLoaded() ? GetLength() : -1);
445
config::mru->Add("Timecodes", filename);
447
catch (agi::fs::FileSystemError const& err) {
448
wxMessageBox(to_wx(err.GetMessage()), "Error saving timecodes", wxOK | wxICON_ERROR | wxCENTER, context->parent);
451
void VideoContext::CloseTimecodes() {
452
ovr_fps = agi::vfr::Framerate();
453
timecodes_filename.clear();
454
OnSubtitlesCommit(0, std::set<const AssEntry*>());
455
TimecodesOpen(video_fps);
458
int VideoContext::TimeAtFrame(int frame, agi::vfr::Time type) const {
459
return (ovr_fps.IsLoaded() ? ovr_fps : video_fps).TimeAtFrame(frame, type);
462
int VideoContext::FrameAtTime(int time, agi::vfr::Time type) const {
463
return (ovr_fps.IsLoaded() ? ovr_fps : video_fps).FrameAtTime(time, type);
466
void VideoContext::OnVideoError(VideoProviderErrorEvent const& err) {
468
"Failed seeking video. The video file may be corrupt or incomplete.\n"
469
"Error message reported: %s",
470
to_wx(err.GetMessage()));
472
void VideoContext::OnSubtitlesError(SubtitlesProviderErrorEvent const& err) {
474
"Failed rendering subtitles. Error message reported: %s",
475
to_wx(err.GetMessage()));