1 /////////////////////////////////////////////////////////////////////////////
2 // Name:        ProcessMenu.cpp
3 // Author:      Alex Thuering
4 // Created:     26.09.2014 (refactored)
5 // RCS-ID:      $Id: ProcessMenu.cpp,v 1.11 2016/12/11 10:06:31 ntalex Exp $
6 // Copyright:   (c) Alex Thuering
7 // Licence:     GPL
8 /////////////////////////////////////////////////////////////////////////////
9 
10 #include "ProcessMenu.h"
11 #include "DVD.h"
12 #include "Menu.h"
13 #include "Config.h"
14 #include "mediaenc_ffmpeg.h"
15 #include "mediatrc_ffmpeg.h"
16 #include <wxSVG/SVGDocument.h>
17 #include <wxSVG/SVGImageElement.h>
18 #include <wxSVG/SVGAnimateElement.h>
19 #include <wxSVG/SVGAnimateTransformElement.h>
20 #include <wxSVG/mediadec_ffmpeg.h>
21 
22 #define WARNING_MPLEX_MSG wxT("Warning: using of mplex (mjpegs-tool) for generation of menus with audio is disabled. This can produce not compliant DVD.")
23 
24 /** Constructor */
ProcessMenu(ProgressDlg * progressDlg,DVD * dvd,wxString dvdTmpDir)25 ProcessMenu::ProcessMenu(ProgressDlg* progressDlg, DVD* dvd, wxString dvdTmpDir): ProcessTranscode(progressDlg) {
26 	this->dvd = dvd;
27 	menuSubSteps = 0;
28 	for (int tsi = -1; tsi < (int) dvd->GetTitlesets().Count(); tsi++) {
29 		PgcArray& pgcs = dvd->GetPgcArray(tsi, true);
30 		for (int pgci = 0; pgci < (int) pgcs.Count(); pgci++) {
31 			Pgc* pgc = pgcs[pgci];
32 			if (pgc->GetVobs().Count() == 0)
33 				continue;
34 			// set temp file name
35 			wxString menuFile = dvdTmpDir + wxString::Format(_T("menu%d-%d.mpg"), tsi + 1, pgci);
36 			Vob* vob = pgc->GetVobs()[0];
37 			vob->SetTmpFilename(menuFile);
38 			// calculate sub steps
39 			Menu* menu = vob->GetMenu();
40 			WidescreenType widescreenType = pgcs.GetVideo().GetWidescreen();
41 			bool videoMenu = menu->GetSVG()->GetDuration() > 0;
42 			if (videoMenu) {
43 				menuSubSteps += 400; // generate mpeg(200) + transcode(200)
44 			} else if (vob->GetAudioFilenames().size() > 0 || s_config.GetUseMplexForMenus())
45 				menuSubSteps += 300; // generate mpeg(25+75) + transcode(200)
46 			else
47 				menuSubSteps += 25; // generate mpeg
48 			menuSubSteps += (menu->GetAspectRatio() == ar4_3 ? 1 : widescreenType != wtAUTO ? 2 : 3) * 50; // spumux
49 			menuVobs.Add(vob);
50 			menuWSTypes.Add((int) widescreenType);
51 		}
52 	}
53 }
54 
55 /** Returns true, if process need be executed */
IsNeedExecute()56 bool ProcessMenu::IsNeedExecute() {
57 	return menuVobs.Count() > 0;
58 }
59 
60 /** Executes process */
Execute()61 bool ProcessMenu::Execute() {
62 	if (menuVobs.Count() == 0)
63 		return true;
64 	if (progressDlg->WasCanceled())
65 		return false;
66 	progressDlg->SetSubSteps(menuSubSteps);
67 	for (unsigned int i = 0; i < menuVobs.Count(); i++) {
68 		wxString msg = wxString::Format(_("Generating menu %u of %lu"), i + 1, menuVobs.Count());
69 		if (i == 0) {
70 			progressDlg->AddSummaryMsg(msg);
71 		} else {
72 			progressDlg->ReplaceSummaryMsg(msg);
73 			progressDlg->AddDetailMsg(msg);
74 		}
75 		Vob* vob = (Vob*) menuVobs[i];
76 		WidescreenType wsType = (WidescreenType) menuWSTypes[i];
77 
78 		wxString audioFile;
79 		AudioFormat audioFormat = dvd->GetAudioFormat();
80 		if (vob->GetAudioFilenames().size() > 0 && vob->GetStreams().size() > 0
81 				&& vob->GetStreams()[0]->GetType() == stAUDIO) {
82 			audioFile = vob->GetAudioFilenames()[0];
83 			Stream* stream = vob->GetStreams()[0];
84 			audioFormat = stream->GetAudioFormat();
85 			if (audioFormat == afCOPY) {
86 				// set destination format if it set to COPY and codec is not mp2/ac3 or sample rate != 48 kHz
87 				if (stream->GetSourceCodecName() != wxT("mp2")
88 						&& stream->GetSourceCodecName() != wxT("ac3")
89 						&& stream->GetSourceCodecName() != wxT("liba52")) {
90 					audioFormat = dvd->GetAudioFormat();
91 				} else if (stream->GetSourceSampleRate() != 48000) {
92 					audioFormat = stream->GetSourceCodecName() == wxT("mp2") ? afMP2 : afAC3;
93 				}
94 			}
95 		}
96 
97 		if (!GenerateMenu(vob->GetMenu(), wsType, vob->GetTmpFilename(), audioFormat, audioFile, dvd->GetAudioBitrate()))
98 			return false;
99 	}
100 	progressDlg->IncStep();
101 	return true;
102 }
103 
GenerateMenu(Menu * menu,WidescreenType widescreenType,const wxString & menuFile,AudioFormat audioFormat,wxString audioFile,int audioBitrate)104 bool ProcessMenu::GenerateMenu(Menu* menu, WidescreenType widescreenType, const wxString& menuFile,
105 		AudioFormat audioFormat, wxString audioFile, int audioBitrate) {
106 	if (progressDlg->WasCanceled())
107 		return false;
108 	wxString mpegFile = menuFile + _T("_bg.mpg");
109 	wxString m2vFile = menuFile + _T("_bg.m2v");
110 	wxString audioFileTmp = menuFile + _T("_bg.audio");
111 	wxString btFile = menuFile + _T("_buttons.png");
112 	wxString hlFile = menuFile + _T("_highlight.png");
113 	wxString selFile = menuFile + _T("_select.png");
114 	wxString spuFile = menuFile + _T("_spumux.xml");
115 
116 	bool videoMenu = menu->GetSVG()->GetDuration() > 0;
117 	wxYield();
118 
119 	progressDlg->AddDetailMsg(_("Create menu MPEG"));
120 	if (s_config.GetMenuVideoBitrate() < 1000)
121 		s_config.SetMenuVideoBitrate(1000);
122 
123 	if (videoMenu) {
124 		// get background audio format
125 		AudioFormat bgAudioFormat = afNONE;
126 		wxString bgAudioOutputFormat;
127 		if (menu->HasVideoBackground() && audioFile.length() == 0) {
128 			wxFfmpegMediaDecoder ffmpeg;
129 			if (!ffmpeg.Load(menu->GetBackground()))
130 				return false;
131 			for (unsigned int i = 0; i < ffmpeg.GetStreamCount(); i++) {
132 				if (ffmpeg.GetStreamType(i) == stAUDIO) {
133 					if (ffmpeg.GetCodecName(i) != wxT("mp2") && ffmpeg.GetCodecName(i) != wxT("ac3")) {
134 						bgAudioFormat = audioFormat;
135 					} else if (ffmpeg.GetSampleRate(i) != 48000) {
136 						bgAudioFormat = ffmpeg.GetCodecName(i) == wxT("mp2") ? afMP2 : afAC3;
137 					} else {
138 						bgAudioFormat = afCOPY;
139 						bgAudioOutputFormat = ffmpeg.GetCodecName(i);
140 					}
141 					break;
142 				}
143 			}
144 		}
145 		// encode video
146 		bool hasAudio = audioFile.length() || bgAudioFormat != afNONE || s_config.GetUseMplexForMenus();
147 		wxFfmpegMediaEncoder ffmpeg(s_config.GetThreadCount());
148 		if (!ffmpeg.BeginEncode(hasAudio ? m2vFile : mpegFile, menu->GetVideoFormat(), hasAudio ? afNONE : audioFormat,
149 				menu->GetAspectRatio(), s_config.GetMenuVideoBitrate(), s_config.GetMenuVideoCBR())) {
150 			progressDlg->Failed(_("Error creation of menu"));
151 			return false;
152 		}
153 		wxSVGDocument* svg = menu->GetBackgroundSVG();
154 		int width = menu->GetFrameResolution().GetWidth();
155 		int height = menu->GetFrameResolution().GetHeight();
156 		progressDlg->AddDetailMsg(wxString::Format(_("Video duration: %f sec"), svg->GetDuration()));
157 		int frameCount = (int) (svg->GetDuration() * GetFps(menu->GetVideoFormat(), false));
158 		if (frameCount == 0)
159 			frameCount = s_config.GetMenuFrameCount();
160 		for (int f = 0; f < frameCount; f++) {
161 			if (progressDlg->WasCanceled())
162 				return false;
163 			if (f % 10 == 0) {
164 				wxString msg = wxString::Format(_("Encoding frame %d of %d"), (f > 0 ? f : 1), frameCount);
165 				if (f == 0) {
166 					progressDlg->AddDetailMsg(msg);
167 				} else {
168 					progressDlg->ReplaceLastDetailMsg(msg);
169 				}
170 			}
171 			svg->SetCurrentTime(((double) f) / GetFps(menu->GetVideoFormat(), false));
172 			wxImage img = svg->Render(width, height, NULL, false);
173 			if (!ffmpeg.EncodeImage(img, 1, progressDlg)) {
174 				progressDlg->Failed(_("Error creation of menu"));
175 				return false;
176 			}
177 			progressDlg->SetSubStep(200 * (f + 1) / frameCount);
178 		}
179 		ffmpeg.EndEncode();
180 		if (hasAudio) {
181 			if (audioFile.length() == 0 && bgAudioFormat == afNONE) {
182 				// encode audio
183 				audioFileTmp = menuFile + wxString(wxT("_bg.")) + (audioFormat == afAC3 ? wxT("ac3") : wxT("mp2"));
184 				double duration = (double) frameCount / GetFps(menu->GetVideoFormat(), false);
185 				if (!ffmpeg.BeginEncode(audioFileTmp, vfNONE, audioFormat, menu->GetAspectRatio(),
186 						s_config.GetMenuVideoBitrate(), s_config.GetMenuVideoCBR())
187 						|| !ffmpeg.EncodeAudio(duration, progressDlg)) {
188 					progressDlg->Failed(_("Error creation of menu"));
189 					return false;
190 				}
191 				ffmpeg.EndEncode();
192 			}
193 			Vob menuVob;
194 			menuVob.SetFilename(m2vFile);
195 			menuVob.GetVideoStream()->SetDestinationFormat(vfCOPY);
196 			if (audioFile.length()) {
197 				menuVob.AddAudioFile(audioFile);
198 			} else if (bgAudioFormat == afNONE) {
199 				menuVob.AddAudioFile(audioFileTmp);
200 			} else {
201 				progressDlg->AddDetailMsg(_("Transcode audio from") + wxString(wxT(" ")) + menu->GetBackground());
202 				// transcode audio
203 				wxFfmpegMediaTranscoder transcoder;
204 				if (!transcoder.AddInputFile(menu->GetBackground())) {
205 					progressDlg->Failed(wxT("Error by transcoding of ") + menu->GetBackground());
206 					return false;
207 				}
208 				transcoder.SetOutputFormat(bgAudioOutputFormat);
209 				if (!transcoder.SetOutputFile(audioFileTmp, vfNONE, false, menu->GetAspectRatio(), bgAudioFormat,
210 						sfNONE, 0, false, audioBitrate)) {
211 					progressDlg->Failed(_("Error transcoding of ") + menu->GetBackground());
212 					return false;
213 				}
214 				AVConvExecute exec(progressDlg, -1);
215 				if (!exec.Execute(transcoder.GetCmd())) {
216 					DeleteFile(audioFileTmp);
217 					progressDlg->Failed(_("Error transcoding of ") + menu->GetBackground());
218 					return false;
219 				}
220 				menuVob.AddAudioFile(audioFileTmp);
221 			}
222 			progressDlg->AddDetailMsg(_("Multiplexing audio and video"));
223 			for (unsigned int i = 0; i < menuVob.GetStreams().size(); i++) {
224 				Stream* stream = menuVob.GetStreams()[i];
225 				if (stream->GetType() == stAUDIO && stream->GetAudioFormat() == afCOPY) {
226 					// change destination format if it set to COPY and codec is not mp2/ac3 or sample rate != 48 kHz
227 					if (stream->GetSourceCodecName() != wxT("mp2") && stream->GetSourceCodecName() != wxT("ac3")) {
228 						stream->SetDestinationFormat(audioFormat);
229 					} else if (stream->GetSourceSampleRate() != 48000) {
230 						stream->SetDestinationFormat(stream->GetSourceCodecName() == wxT("mp2") ? afMP2 : afAC3);
231 					}
232 				}
233 			}
234 			menuVob.SetKeepAspectRatio(false);
235 			menuVob.SetTmpFilename(mpegFile);
236 			if (!s_config.GetUseMplexForMenus())
237 				progressDlg->AddDetailMsg(WARNING_MPLEX_MSG, *wxRED);
238 			if (!Transcode(&menuVob, menu->GetAspectRatio(), s_config.GetMenuVideoBitrate(), audioBitrate,
239 					s_config.GetUseMplexForMenus()))
240 				return false;
241 			if (s_config.GetRemoveTempFiles()) {
242 				DeleteFile(m2vFile);
243 				DeleteFile(audioFileTmp);
244 			}
245 		}
246 	} else { // menu with still image
247 		wxImage bgImage = menu->GetBackgroundImage();
248 		if (!bgImage.Ok()) {
249 			progressDlg->Failed(_("Error creation of menu"));
250 			return false;
251 		}
252 
253 		bool hasAudio = audioFile.length(); // || s_config.GetUseMplexForMenus(); disable mplex for still image
254 		int frameCount = s_config.GetMenuFrameCount();
255 		if (audioFile.length() > 0) {
256 			// get audio duration
257 			wxFfmpegMediaDecoder decoder;
258 			if (decoder.Load(audioFile)) {
259 				double duration = decoder.GetDuration();
260 				if (duration > 0) {
261 					progressDlg->AddDetailMsg(wxString::Format(_("Audio duration: %f sec"), duration));
262 					if (menu->GetVideoFormat() == vfPAL)
263 						frameCount = (int) (duration * 25);
264 					else
265 						frameCount = (int) (duration * 30000 / 1001);
266 				}
267 			}
268 		}
269 
270 		// encode video
271 		wxFfmpegMediaEncoder ffmpeg(s_config.GetThreadCount());
272 		if (frameCount < (isNTSC(menu->GetVideoFormat()) ? 15 : 12)) {
273 			frameCount = (isNTSC(menu->GetVideoFormat()) ? 15 : 12); // set minimum of frames to default GOP size
274 		}
275 		progressDlg->AddDetailMsg(_("Frame count of menu:") + wxString::Format(wxT(" %d"), frameCount));
276 		if (!ffmpeg.BeginEncode(hasAudio ? m2vFile : mpegFile, menu->GetVideoFormat(), hasAudio ? afNONE : audioFormat,
277 					menu->GetAspectRatio(), s_config.GetMenuVideoBitrate(), s_config.GetMenuVideoCBR())
278 				|| !ffmpeg.EncodeImage(bgImage, frameCount, progressDlg)) {
279 			progressDlg->Failed(_("Error creation of menu"));
280 			return false;
281 		}
282 		ffmpeg.EndEncode();
283 		progressDlg->IncSubStep(25);
284 
285 		if (hasAudio) { // audio file or mplex is used
286 			if (audioFile.length() == 0) {
287 				// encode audio
288 				audioFileTmp = menuFile + wxString(wxT("_bg.")) + (audioFormat == afAC3 ? wxT("ac3") : wxT("mp2"));
289 				double duration = (double) frameCount / GetFps(menu->GetVideoFormat(), false);
290 				if (!ffmpeg.BeginEncode(audioFileTmp, vfNONE, audioFormat, menu->GetAspectRatio(),
291 						s_config.GetMenuVideoBitrate(), s_config.GetMenuVideoCBR())
292 						|| !ffmpeg.EncodeAudio(duration, progressDlg)) {
293 					progressDlg->Failed(_("Error creation of menu"));
294 					return false;
295 				}
296 				ffmpeg.EndEncode();
297 			}
298 			progressDlg->IncSubStep(75);
299 			// mplex (and optionally transcode audio)
300 			if (progressDlg->WasCanceled())
301 				return false;
302 			progressDlg->AddDetailMsg(_("Multiplexing audio and video"));
303 			Vob menuVob;
304 			menuVob.SetFilename(m2vFile);
305 			menuVob.AddAudioFile(audioFile.length() ? audioFile : audioFileTmp);
306 			for (unsigned int i = 0; i < menuVob.GetStreams().size(); i++) {
307 				Stream* stream = menuVob.GetStreams()[i];
308 				if (stream->GetType() == stAUDIO)
309 					stream->SetDestinationFormat(audioFile.length() ? audioFormat : afCOPY);
310 			}
311 			menuVob.SetTmpFilename(mpegFile);
312 			if (!s_config.GetUseMplexForMenus())
313 				progressDlg->AddDetailMsg(WARNING_MPLEX_MSG, *wxRED);
314 			if (!Transcode(&menuVob, menu->GetAspectRatio(), s_config.GetMenuVideoBitrate(), audioBitrate,
315 					s_config.GetUseMplexForMenus()))
316 				return false;
317 			if (s_config.GetRemoveTempFiles()) {
318 				DeleteFile(m2vFile);
319 				DeleteFile(audioFileTmp);
320 			}
321 		}
322 	}
323 
324 	//spumux
325 	if (progressDlg->WasCanceled())
326 		return false;
327 	progressDlg->AddDetailMsg(_("Multiplexing subpictures into mpeg"));
328 	int stCount = menu->GetAspectRatio() == ar4_3 ? 1 : widescreenType != wtAUTO ? 2 : 3;
329 	for (int stIdx = 0; stIdx < stCount; stIdx++) {
330 		if (stIdx > 0) {
331 			if (mpegFile == menu->GetBackground())
332 				mpegFile = menuFile + _T("_bg.mpg");
333 			if (!wxRenameFile(menuFile, mpegFile, false)) {
334 				progressDlg->Failed(wxString::Format(_("Can't rename file '%s' in '%s'"), menuFile.c_str(), mpegFile.c_str()));
335 				return false;
336 			}
337 		}
338 		// save subpictures
339 		SubStreamMode mode = menu->GetAspectRatio() == ar4_3 ? ssmNORMAL : ssmWIDESCREEN;
340 		if (stIdx == 1)
341 			mode = widescreenType == wtNOLETTERBOX ? ssmPANSCAN : ssmLETTERBOX;
342 		else if (stIdx == 2)
343 			mode = ssmPANSCAN;
344 		wxImage* images = menu->GetSubPictures(mode);
345 		images[0].SaveFile(btFile);
346 		images[1].SaveFile(hlFile);
347 		images[2].SaveFile(selFile);
348 		delete[] images;
349 		// save spumux
350 		menu->SaveSpumux(spuFile, mode, btFile, hlFile, selFile);
351 		wxString cmd = s_config.GetSpumuxCmd();
352 		cmd.Replace(wxT("$FILE_CONF"), spuFile);
353 		cmd.Replace(wxT("$STREAM"), wxString::Format(wxT("%d"), stIdx));
354 		wxULongLong fileSize = wxFileName::GetSize(mpegFile);
355 		SpumuxExecute exec(progressDlg, fileSize != wxInvalidSize ? fileSize.ToDouble() : -1);
356 		if (!exec.Execute(cmd, mpegFile, menuFile)) {
357 			progressDlg->Failed();
358 			return false;
359 		}
360 		if (s_config.GetRemoveTempFiles() || stIdx + 1 < stCount) {
361 			if ((!videoMenu || mpegFile != menu->GetBackground()))
362 				DeleteFile(mpegFile);
363 			DeleteFile(btFile);
364 			DeleteFile(hlFile);
365 			DeleteFile(selFile);
366 			DeleteFile(spuFile);
367 		}
368 	}
369 
370 	wxYield();
371 	return true;
372 }
373