1 /*
2  * Copyright (c) 2015 John R. Bradley <jrb@turrettech.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include <obs-module.h>
18 #include <util/platform.h>
19 #include <util/dstr.h>
20 
21 #include "obs-ffmpeg-compat.h"
22 #include "obs-ffmpeg-formats.h"
23 
24 #include <media-playback/media.h>
25 
26 #define FF_LOG(level, format, ...) \
27 	blog(level, "[Media Source]: " format, ##__VA_ARGS__)
28 #define FF_LOG_S(source, level, format, ...)        \
29 	blog(level, "[Media Source '%s']: " format, \
30 	     obs_source_get_name(source), ##__VA_ARGS__)
31 #define FF_BLOG(level, format, ...) \
32 	FF_LOG_S(s->source, level, format, ##__VA_ARGS__)
33 
34 struct ffmpeg_source {
35 	mp_media_t media;
36 	bool media_valid;
37 	bool destroy_media;
38 
39 	struct SwsContext *sws_ctx;
40 	int sws_width;
41 	int sws_height;
42 	enum AVPixelFormat sws_format;
43 	uint8_t *sws_data;
44 	int sws_linesize;
45 	enum video_range_type range;
46 	bool is_linear_alpha;
47 	obs_source_t *source;
48 	obs_hotkey_id hotkey;
49 
50 	char *input;
51 	char *input_format;
52 	int buffering_mb;
53 	int speed_percent;
54 	bool is_looping;
55 	bool is_local_file;
56 	bool is_hw_decoding;
57 	bool is_clear_on_media_end;
58 	bool restart_on_activate;
59 	bool close_when_inactive;
60 	bool seekable;
61 
62 	pthread_t reconnect_thread;
63 	bool stop_reconnect;
64 	bool reconnect_thread_valid;
65 	volatile bool reconnecting;
66 	int reconnect_delay_sec;
67 
68 	enum obs_media_state state;
69 	obs_hotkey_pair_id play_pause_hotkey;
70 	obs_hotkey_id stop_hotkey;
71 };
72 
set_media_state(void * data,enum obs_media_state state)73 static void set_media_state(void *data, enum obs_media_state state)
74 {
75 	struct ffmpeg_source *s = data;
76 	s->state = state;
77 }
78 
is_local_file_modified(obs_properties_t * props,obs_property_t * prop,obs_data_t * settings)79 static bool is_local_file_modified(obs_properties_t *props,
80 				   obs_property_t *prop, obs_data_t *settings)
81 {
82 	UNUSED_PARAMETER(prop);
83 
84 	bool enabled = obs_data_get_bool(settings, "is_local_file");
85 	obs_property_t *input = obs_properties_get(props, "input");
86 	obs_property_t *input_format =
87 		obs_properties_get(props, "input_format");
88 	obs_property_t *local_file = obs_properties_get(props, "local_file");
89 	obs_property_t *looping = obs_properties_get(props, "looping");
90 	obs_property_t *buffering = obs_properties_get(props, "buffering_mb");
91 	obs_property_t *seekable = obs_properties_get(props, "seekable");
92 	obs_property_t *speed = obs_properties_get(props, "speed_percent");
93 	obs_property_t *reconnect_delay_sec =
94 		obs_properties_get(props, "reconnect_delay_sec");
95 	obs_property_set_visible(input, !enabled);
96 	obs_property_set_visible(input_format, !enabled);
97 	obs_property_set_visible(buffering, !enabled);
98 	obs_property_set_visible(local_file, enabled);
99 	obs_property_set_visible(looping, enabled);
100 	obs_property_set_visible(speed, enabled);
101 	obs_property_set_visible(seekable, !enabled);
102 	obs_property_set_visible(reconnect_delay_sec, !enabled);
103 
104 	return true;
105 }
106 
ffmpeg_source_defaults(obs_data_t * settings)107 static void ffmpeg_source_defaults(obs_data_t *settings)
108 {
109 	obs_data_set_default_bool(settings, "is_local_file", true);
110 	obs_data_set_default_bool(settings, "looping", false);
111 	obs_data_set_default_bool(settings, "clear_on_media_end", true);
112 	obs_data_set_default_bool(settings, "restart_on_activate", true);
113 	obs_data_set_default_bool(settings, "linear_alpha", false);
114 	obs_data_set_default_int(settings, "reconnect_delay_sec", 10);
115 	obs_data_set_default_int(settings, "buffering_mb", 2);
116 	obs_data_set_default_int(settings, "speed_percent", 100);
117 }
118 
119 static const char *media_filter =
120 	" (*.mp4 *.ts *.mov *.flv *.mkv *.avi *.mp3 *.ogg *.aac *.wav *.gif *.webm);;";
121 static const char *video_filter =
122 	" (*.mp4 *.ts *.mov *.flv *.mkv *.avi *.gif *.webm);;";
123 static const char *audio_filter = " (*.mp3 *.aac *.ogg *.wav);;";
124 
ffmpeg_source_getproperties(void * data)125 static obs_properties_t *ffmpeg_source_getproperties(void *data)
126 {
127 	struct ffmpeg_source *s = data;
128 	struct dstr filter = {0};
129 	struct dstr path = {0};
130 	UNUSED_PARAMETER(data);
131 
132 	obs_properties_t *props = obs_properties_create();
133 
134 	obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE);
135 
136 	obs_property_t *prop;
137 	// use this when obs allows non-readonly paths
138 	prop = obs_properties_add_bool(props, "is_local_file",
139 				       obs_module_text("LocalFile"));
140 
141 	obs_property_set_modified_callback(prop, is_local_file_modified);
142 
143 	dstr_copy(&filter, obs_module_text("MediaFileFilter.AllMediaFiles"));
144 	dstr_cat(&filter, media_filter);
145 	dstr_cat(&filter, obs_module_text("MediaFileFilter.VideoFiles"));
146 	dstr_cat(&filter, video_filter);
147 	dstr_cat(&filter, obs_module_text("MediaFileFilter.AudioFiles"));
148 	dstr_cat(&filter, audio_filter);
149 	dstr_cat(&filter, obs_module_text("MediaFileFilter.AllFiles"));
150 	dstr_cat(&filter, " (*.*)");
151 
152 	if (s && s->input && *s->input) {
153 		const char *slash;
154 
155 		dstr_copy(&path, s->input);
156 		dstr_replace(&path, "\\", "/");
157 		slash = strrchr(path.array, '/');
158 		if (slash)
159 			dstr_resize(&path, slash - path.array + 1);
160 	}
161 
162 	obs_properties_add_path(props, "local_file",
163 				obs_module_text("LocalFile"), OBS_PATH_FILE,
164 				filter.array, path.array);
165 	dstr_free(&filter);
166 	dstr_free(&path);
167 
168 	obs_properties_add_bool(props, "looping", obs_module_text("Looping"));
169 
170 	obs_properties_add_bool(props, "restart_on_activate",
171 				obs_module_text("RestartWhenActivated"));
172 
173 	prop = obs_properties_add_int_slider(props, "buffering_mb",
174 					     obs_module_text("BufferingMB"), 0,
175 					     16, 1);
176 	obs_property_int_set_suffix(prop, " MB");
177 
178 	obs_properties_add_text(props, "input", obs_module_text("Input"),
179 				OBS_TEXT_DEFAULT);
180 
181 	obs_properties_add_text(props, "input_format",
182 				obs_module_text("InputFormat"),
183 				OBS_TEXT_DEFAULT);
184 
185 	prop = obs_properties_add_int_slider(
186 		props, "reconnect_delay_sec",
187 		obs_module_text("ReconnectDelayTime"), 1, 60, 1);
188 	obs_property_int_set_suffix(prop, " S");
189 
190 	obs_properties_add_bool(props, "hw_decode",
191 				obs_module_text("HardwareDecode"));
192 
193 	obs_properties_add_bool(props, "clear_on_media_end",
194 				obs_module_text("ClearOnMediaEnd"));
195 
196 	prop = obs_properties_add_bool(
197 		props, "close_when_inactive",
198 		obs_module_text("CloseFileWhenInactive"));
199 
200 	obs_property_set_long_description(
201 		prop, obs_module_text("CloseFileWhenInactive.ToolTip"));
202 
203 	prop = obs_properties_add_int_slider(props, "speed_percent",
204 					     obs_module_text("SpeedPercentage"),
205 					     1, 200, 1);
206 	obs_property_int_set_suffix(prop, "%");
207 
208 	prop = obs_properties_add_list(props, "color_range",
209 				       obs_module_text("ColorRange"),
210 				       OBS_COMBO_TYPE_LIST,
211 				       OBS_COMBO_FORMAT_INT);
212 	obs_property_list_add_int(prop, obs_module_text("ColorRange.Auto"),
213 				  VIDEO_RANGE_DEFAULT);
214 	obs_property_list_add_int(prop, obs_module_text("ColorRange.Partial"),
215 				  VIDEO_RANGE_PARTIAL);
216 	obs_property_list_add_int(prop, obs_module_text("ColorRange.Full"),
217 				  VIDEO_RANGE_FULL);
218 
219 	obs_properties_add_bool(props, "linear_alpha",
220 				obs_module_text("LinearAlpha"));
221 
222 	obs_properties_add_bool(props, "seekable", obs_module_text("Seekable"));
223 
224 	return props;
225 }
226 
dump_source_info(struct ffmpeg_source * s,const char * input,const char * input_format)227 static void dump_source_info(struct ffmpeg_source *s, const char *input,
228 			     const char *input_format)
229 {
230 	FF_BLOG(LOG_INFO,
231 		"settings:\n"
232 		"\tinput:                   %s\n"
233 		"\tinput_format:            %s\n"
234 		"\tspeed:                   %d\n"
235 		"\tis_looping:              %s\n"
236 		"\tis_linear_alpha:         %s\n"
237 		"\tis_hw_decoding:          %s\n"
238 		"\tis_clear_on_media_end:   %s\n"
239 		"\trestart_on_activate:     %s\n"
240 		"\tclose_when_inactive:     %s",
241 		input ? input : "(null)",
242 		input_format ? input_format : "(null)", s->speed_percent,
243 		s->is_looping ? "yes" : "no", s->is_linear_alpha ? "yes" : "no",
244 		s->is_hw_decoding ? "yes" : "no",
245 		s->is_clear_on_media_end ? "yes" : "no",
246 		s->restart_on_activate ? "yes" : "no",
247 		s->close_when_inactive ? "yes" : "no");
248 }
249 
get_frame(void * opaque,struct obs_source_frame * f)250 static void get_frame(void *opaque, struct obs_source_frame *f)
251 {
252 	struct ffmpeg_source *s = opaque;
253 	obs_source_output_video(s->source, f);
254 }
255 
preload_frame(void * opaque,struct obs_source_frame * f)256 static void preload_frame(void *opaque, struct obs_source_frame *f)
257 {
258 	struct ffmpeg_source *s = opaque;
259 	if (s->close_when_inactive)
260 		return;
261 
262 	if (s->is_clear_on_media_end || s->is_looping)
263 		obs_source_preload_video(s->source, f);
264 
265 	if (!s->is_local_file && os_atomic_set_bool(&s->reconnecting, false))
266 		FF_BLOG(LOG_INFO, "Reconnected.");
267 }
268 
seek_frame(void * opaque,struct obs_source_frame * f)269 static void seek_frame(void *opaque, struct obs_source_frame *f)
270 {
271 	struct ffmpeg_source *s = opaque;
272 	obs_source_set_video_frame(s->source, f);
273 }
274 
get_audio(void * opaque,struct obs_source_audio * a)275 static void get_audio(void *opaque, struct obs_source_audio *a)
276 {
277 	struct ffmpeg_source *s = opaque;
278 	obs_source_output_audio(s->source, a);
279 
280 	if (!s->is_local_file && os_atomic_set_bool(&s->reconnecting, false))
281 		FF_BLOG(LOG_INFO, "Reconnected.");
282 }
283 
media_stopped(void * opaque)284 static void media_stopped(void *opaque)
285 {
286 	struct ffmpeg_source *s = opaque;
287 	if (s->is_clear_on_media_end) {
288 		obs_source_output_video(s->source, NULL);
289 	}
290 
291 	if ((s->close_when_inactive || !s->is_local_file) && s->media_valid)
292 		s->destroy_media = true;
293 
294 	set_media_state(s, OBS_MEDIA_STATE_ENDED);
295 	obs_source_media_ended(s->source);
296 }
297 
ffmpeg_source_open(struct ffmpeg_source * s)298 static void ffmpeg_source_open(struct ffmpeg_source *s)
299 {
300 	if (s->input && *s->input) {
301 		struct mp_media_info info = {
302 			.opaque = s,
303 			.v_cb = get_frame,
304 			.v_preload_cb = preload_frame,
305 			.v_seek_cb = seek_frame,
306 			.a_cb = get_audio,
307 			.stop_cb = media_stopped,
308 			.path = s->input,
309 			.format = s->input_format,
310 			.buffering = s->buffering_mb * 1024 * 1024,
311 			.speed = s->speed_percent,
312 			.force_range = s->range,
313 			.is_linear_alpha = s->is_linear_alpha,
314 			.hardware_decoding = s->is_hw_decoding,
315 			.is_local_file = s->is_local_file || s->seekable,
316 			.reconnecting = s->reconnecting,
317 		};
318 
319 		s->media_valid = mp_media_init(&s->media, &info);
320 	}
321 }
322 
ffmpeg_source_start(struct ffmpeg_source * s)323 static void ffmpeg_source_start(struct ffmpeg_source *s)
324 {
325 	if (!s->media_valid)
326 		ffmpeg_source_open(s);
327 
328 	if (!s->media_valid)
329 		return;
330 
331 	mp_media_play(&s->media, s->is_looping, s->reconnecting);
332 	if (s->is_local_file && (s->is_clear_on_media_end || s->is_looping))
333 		obs_source_show_preloaded_video(s->source);
334 	else
335 		obs_source_output_video(s->source, NULL);
336 	set_media_state(s, OBS_MEDIA_STATE_PLAYING);
337 	obs_source_media_started(s->source);
338 }
339 
ffmpeg_source_reconnect(void * data)340 static void *ffmpeg_source_reconnect(void *data)
341 {
342 	struct ffmpeg_source *s = data;
343 	os_sleep_ms(s->reconnect_delay_sec * 1000);
344 
345 	if (s->stop_reconnect || s->media_valid)
346 		goto finish;
347 
348 	bool active = obs_source_active(s->source);
349 	if (!s->close_when_inactive || active)
350 		ffmpeg_source_open(s);
351 
352 	if (!s->restart_on_activate || active)
353 		ffmpeg_source_start(s);
354 
355 finish:
356 	s->reconnect_thread_valid = false;
357 	return NULL;
358 }
359 
ffmpeg_source_tick(void * data,float seconds)360 static void ffmpeg_source_tick(void *data, float seconds)
361 {
362 	UNUSED_PARAMETER(seconds);
363 
364 	struct ffmpeg_source *s = data;
365 	if (s->destroy_media) {
366 		if (s->media_valid) {
367 			mp_media_free(&s->media);
368 			s->media_valid = false;
369 		}
370 
371 		s->destroy_media = false;
372 
373 		if (!s->is_local_file) {
374 			if (!os_atomic_set_bool(&s->reconnecting, true)) {
375 				FF_BLOG(LOG_WARNING, "Disconnected. "
376 						     "Reconnecting...");
377 			}
378 			if (pthread_create(&s->reconnect_thread, NULL,
379 					   ffmpeg_source_reconnect, s) != 0) {
380 				FF_BLOG(LOG_WARNING, "Could not create "
381 						     "reconnect thread");
382 				return;
383 			}
384 			s->reconnect_thread_valid = true;
385 		}
386 	}
387 }
388 
ffmpeg_source_update(void * data,obs_data_t * settings)389 static void ffmpeg_source_update(void *data, obs_data_t *settings)
390 {
391 	struct ffmpeg_source *s = data;
392 
393 	bool is_local_file = obs_data_get_bool(settings, "is_local_file");
394 
395 	char *input;
396 	char *input_format;
397 
398 	bfree(s->input);
399 	bfree(s->input_format);
400 
401 	if (is_local_file) {
402 		input = (char *)obs_data_get_string(settings, "local_file");
403 		input_format = NULL;
404 		s->is_looping = obs_data_get_bool(settings, "looping");
405 	} else {
406 		input = (char *)obs_data_get_string(settings, "input");
407 		input_format =
408 			(char *)obs_data_get_string(settings, "input_format");
409 		s->reconnect_delay_sec =
410 			(int)obs_data_get_int(settings, "reconnect_delay_sec");
411 		s->reconnect_delay_sec = s->reconnect_delay_sec == 0
412 						 ? 10
413 						 : s->reconnect_delay_sec;
414 		s->is_looping = false;
415 
416 		if (s->reconnect_thread_valid) {
417 			s->stop_reconnect = true;
418 			pthread_join(s->reconnect_thread, NULL);
419 			s->stop_reconnect = false;
420 		}
421 	}
422 
423 	s->close_when_inactive =
424 		obs_data_get_bool(settings, "close_when_inactive");
425 
426 	s->input = input ? bstrdup(input) : NULL;
427 	s->input_format = input_format ? bstrdup(input_format) : NULL;
428 	s->is_hw_decoding = obs_data_get_bool(settings, "hw_decode");
429 	s->is_clear_on_media_end =
430 		obs_data_get_bool(settings, "clear_on_media_end");
431 	s->restart_on_activate =
432 		obs_data_get_bool(settings, "restart_on_activate");
433 	s->range = (enum video_range_type)obs_data_get_int(settings,
434 							   "color_range");
435 	s->is_linear_alpha = obs_data_get_bool(settings, "linear_alpha");
436 	s->buffering_mb = (int)obs_data_get_int(settings, "buffering_mb");
437 	s->speed_percent = (int)obs_data_get_int(settings, "speed_percent");
438 	s->is_local_file = is_local_file;
439 	s->seekable = obs_data_get_bool(settings, "seekable");
440 
441 	if (s->speed_percent < 1 || s->speed_percent > 200)
442 		s->speed_percent = 100;
443 
444 	if (s->media_valid) {
445 		mp_media_free(&s->media);
446 		s->media_valid = false;
447 	}
448 
449 	bool active = obs_source_active(s->source);
450 	if (!s->close_when_inactive || active)
451 		ffmpeg_source_open(s);
452 
453 	dump_source_info(s, input, input_format);
454 	if (!s->restart_on_activate || active)
455 		ffmpeg_source_start(s);
456 }
457 
ffmpeg_source_getname(void * unused)458 static const char *ffmpeg_source_getname(void *unused)
459 {
460 	UNUSED_PARAMETER(unused);
461 	return obs_module_text("FFMpegSource");
462 }
463 
restart_hotkey(void * data,obs_hotkey_id id,obs_hotkey_t * hotkey,bool pressed)464 static void restart_hotkey(void *data, obs_hotkey_id id, obs_hotkey_t *hotkey,
465 			   bool pressed)
466 {
467 	UNUSED_PARAMETER(id);
468 	UNUSED_PARAMETER(hotkey);
469 
470 	if (!pressed)
471 		return;
472 
473 	struct ffmpeg_source *s = data;
474 	if (obs_source_showing(s->source))
475 		obs_source_media_restart(s->source);
476 }
477 
restart_proc(void * data,calldata_t * cd)478 static void restart_proc(void *data, calldata_t *cd)
479 {
480 	restart_hotkey(data, 0, NULL, true);
481 	UNUSED_PARAMETER(cd);
482 }
483 
get_duration(void * data,calldata_t * cd)484 static void get_duration(void *data, calldata_t *cd)
485 {
486 	struct ffmpeg_source *s = data;
487 	int64_t dur = 0;
488 	if (s->media.fmt)
489 		dur = s->media.fmt->duration;
490 
491 	calldata_set_int(cd, "duration", dur * 1000);
492 }
493 
get_nb_frames(void * data,calldata_t * cd)494 static void get_nb_frames(void *data, calldata_t *cd)
495 {
496 	struct ffmpeg_source *s = data;
497 	int64_t frames = 0;
498 
499 	if (!s->media.fmt) {
500 		calldata_set_int(cd, "num_frames", frames);
501 		return;
502 	}
503 
504 	int video_stream_index = av_find_best_stream(
505 		s->media.fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
506 
507 	if (video_stream_index < 0) {
508 		FF_BLOG(LOG_WARNING, "Getting number of frames failed: No "
509 				     "video stream in media file!");
510 		calldata_set_int(cd, "num_frames", frames);
511 		return;
512 	}
513 
514 	AVStream *stream = s->media.fmt->streams[video_stream_index];
515 
516 	if (stream->nb_frames > 0) {
517 		frames = stream->nb_frames;
518 	} else {
519 		FF_BLOG(LOG_DEBUG, "nb_frames not set, estimating using frame "
520 				   "rate and duration");
521 		AVRational avg_frame_rate = stream->avg_frame_rate;
522 		frames = (int64_t)ceil((double)s->media.fmt->duration /
523 				       (double)AV_TIME_BASE *
524 				       (double)avg_frame_rate.num /
525 				       (double)avg_frame_rate.den);
526 	}
527 
528 	calldata_set_int(cd, "num_frames", frames);
529 }
530 
ffmpeg_source_play_hotkey(void * data,obs_hotkey_pair_id id,obs_hotkey_t * hotkey,bool pressed)531 static bool ffmpeg_source_play_hotkey(void *data, obs_hotkey_pair_id id,
532 				      obs_hotkey_t *hotkey, bool pressed)
533 {
534 	UNUSED_PARAMETER(id);
535 	UNUSED_PARAMETER(hotkey);
536 
537 	if (!pressed)
538 		return false;
539 
540 	struct ffmpeg_source *s = data;
541 
542 	if (s->state == OBS_MEDIA_STATE_PLAYING ||
543 	    !obs_source_showing(s->source))
544 		return false;
545 
546 	obs_source_media_play_pause(s->source, false);
547 	return true;
548 }
549 
ffmpeg_source_pause_hotkey(void * data,obs_hotkey_pair_id id,obs_hotkey_t * hotkey,bool pressed)550 static bool ffmpeg_source_pause_hotkey(void *data, obs_hotkey_pair_id id,
551 				       obs_hotkey_t *hotkey, bool pressed)
552 {
553 	UNUSED_PARAMETER(id);
554 	UNUSED_PARAMETER(hotkey);
555 
556 	if (!pressed)
557 		return false;
558 
559 	struct ffmpeg_source *s = data;
560 
561 	if (s->state != OBS_MEDIA_STATE_PLAYING ||
562 	    !obs_source_showing(s->source))
563 		return false;
564 
565 	obs_source_media_play_pause(s->source, true);
566 	return true;
567 }
568 
ffmpeg_source_stop_hotkey(void * data,obs_hotkey_id id,obs_hotkey_t * hotkey,bool pressed)569 static void ffmpeg_source_stop_hotkey(void *data, obs_hotkey_id id,
570 				      obs_hotkey_t *hotkey, bool pressed)
571 {
572 	UNUSED_PARAMETER(id);
573 	UNUSED_PARAMETER(hotkey);
574 
575 	if (!pressed)
576 		return;
577 
578 	struct ffmpeg_source *s = data;
579 
580 	if (obs_source_showing(s->source))
581 		obs_source_media_stop(s->source);
582 }
583 
ffmpeg_source_create(obs_data_t * settings,obs_source_t * source)584 static void *ffmpeg_source_create(obs_data_t *settings, obs_source_t *source)
585 {
586 	UNUSED_PARAMETER(settings);
587 
588 	struct ffmpeg_source *s = bzalloc(sizeof(struct ffmpeg_source));
589 	s->source = source;
590 
591 	s->hotkey = obs_hotkey_register_source(source, "MediaSource.Restart",
592 					       obs_module_text("RestartMedia"),
593 					       restart_hotkey, s);
594 
595 	s->play_pause_hotkey = obs_hotkey_pair_register_source(
596 		s->source, "MediaSource.Play", obs_module_text("Play"),
597 		"MediaSource.Pause", obs_module_text("Pause"),
598 		ffmpeg_source_play_hotkey, ffmpeg_source_pause_hotkey, s, s);
599 
600 	s->stop_hotkey = obs_hotkey_register_source(source, "MediaSource.Stop",
601 						    obs_module_text("Stop"),
602 						    ffmpeg_source_stop_hotkey,
603 						    s);
604 
605 	proc_handler_t *ph = obs_source_get_proc_handler(source);
606 	proc_handler_add(ph, "void restart()", restart_proc, s);
607 	proc_handler_add(ph, "void get_duration(out int duration)",
608 			 get_duration, s);
609 	proc_handler_add(ph, "void get_nb_frames(out int num_frames)",
610 			 get_nb_frames, s);
611 
612 	ffmpeg_source_update(s, settings);
613 	return s;
614 }
615 
ffmpeg_source_destroy(void * data)616 static void ffmpeg_source_destroy(void *data)
617 {
618 	struct ffmpeg_source *s = data;
619 
620 	if (s->hotkey)
621 		obs_hotkey_unregister(s->hotkey);
622 	if (!s->is_local_file) {
623 		s->stop_reconnect = true;
624 		if (s->reconnect_thread_valid)
625 			pthread_join(s->reconnect_thread, NULL);
626 	}
627 	if (s->media_valid)
628 		mp_media_free(&s->media);
629 
630 	if (s->sws_ctx != NULL)
631 		sws_freeContext(s->sws_ctx);
632 	bfree(s->sws_data);
633 	bfree(s->input);
634 	bfree(s->input_format);
635 	bfree(s);
636 }
637 
ffmpeg_source_activate(void * data)638 static void ffmpeg_source_activate(void *data)
639 {
640 	struct ffmpeg_source *s = data;
641 
642 	if (s->restart_on_activate)
643 		obs_source_media_restart(s->source);
644 }
645 
ffmpeg_source_deactivate(void * data)646 static void ffmpeg_source_deactivate(void *data)
647 {
648 	struct ffmpeg_source *s = data;
649 
650 	if (s->restart_on_activate) {
651 		if (s->media_valid) {
652 			mp_media_stop(&s->media);
653 
654 			if (s->is_clear_on_media_end)
655 				obs_source_output_video(s->source, NULL);
656 		}
657 	}
658 }
659 
ffmpeg_source_play_pause(void * data,bool pause)660 static void ffmpeg_source_play_pause(void *data, bool pause)
661 {
662 	struct ffmpeg_source *s = data;
663 
664 	if (!s->media_valid)
665 		ffmpeg_source_open(s);
666 
667 	if (!s->media_valid)
668 		return;
669 
670 	mp_media_play_pause(&s->media, pause);
671 
672 	if (pause) {
673 
674 		set_media_state(s, OBS_MEDIA_STATE_PAUSED);
675 	} else {
676 
677 		set_media_state(s, OBS_MEDIA_STATE_PLAYING);
678 		obs_source_media_started(s->source);
679 	}
680 }
681 
ffmpeg_source_stop(void * data)682 static void ffmpeg_source_stop(void *data)
683 {
684 	struct ffmpeg_source *s = data;
685 
686 	if (s->media_valid) {
687 		mp_media_stop(&s->media);
688 		obs_source_output_video(s->source, NULL);
689 		set_media_state(s, OBS_MEDIA_STATE_STOPPED);
690 	}
691 }
692 
ffmpeg_source_restart(void * data)693 static void ffmpeg_source_restart(void *data)
694 {
695 	struct ffmpeg_source *s = data;
696 
697 	if (obs_source_showing(s->source))
698 		ffmpeg_source_start(s);
699 
700 	set_media_state(s, OBS_MEDIA_STATE_PLAYING);
701 }
702 
ffmpeg_source_get_duration(void * data)703 static int64_t ffmpeg_source_get_duration(void *data)
704 {
705 	struct ffmpeg_source *s = data;
706 	int64_t dur = 0;
707 
708 	if (s->media.fmt)
709 		dur = s->media.fmt->duration / INT64_C(1000);
710 
711 	return dur;
712 }
713 
ffmpeg_source_get_time(void * data)714 static int64_t ffmpeg_source_get_time(void *data)
715 {
716 	struct ffmpeg_source *s = data;
717 
718 	return mp_get_current_time(&s->media);
719 }
720 
ffmpeg_source_set_time(void * data,int64_t ms)721 static void ffmpeg_source_set_time(void *data, int64_t ms)
722 {
723 	struct ffmpeg_source *s = data;
724 
725 	if (!s->media_valid)
726 		return;
727 
728 	mp_media_seek_to(&s->media, ms);
729 }
730 
ffmpeg_source_get_state(void * data)731 static enum obs_media_state ffmpeg_source_get_state(void *data)
732 {
733 	struct ffmpeg_source *s = data;
734 
735 	return s->state;
736 }
737 
missing_file_callback(void * src,const char * new_path,void * data)738 static void missing_file_callback(void *src, const char *new_path, void *data)
739 {
740 	struct ffmpeg_source *s = src;
741 
742 	obs_source_t *source = s->source;
743 	obs_data_t *settings = obs_source_get_settings(source);
744 	obs_data_set_string(settings, "local_file", new_path);
745 	obs_source_update(source, settings);
746 	obs_data_release(settings);
747 
748 	UNUSED_PARAMETER(data);
749 }
750 
ffmpeg_source_missingfiles(void * data)751 static obs_missing_files_t *ffmpeg_source_missingfiles(void *data)
752 {
753 	struct ffmpeg_source *s = data;
754 	obs_missing_files_t *files = obs_missing_files_create();
755 
756 	if (s->is_local_file && strcmp(s->input, "") != 0) {
757 		if (!os_file_exists(s->input)) {
758 			obs_missing_file_t *file = obs_missing_file_create(
759 				s->input, missing_file_callback,
760 				OBS_MISSING_FILE_SOURCE, s->source, NULL);
761 
762 			obs_missing_files_add_file(files, file);
763 		}
764 	}
765 
766 	return files;
767 }
768 
769 struct obs_source_info ffmpeg_source = {
770 	.id = "ffmpeg_source",
771 	.type = OBS_SOURCE_TYPE_INPUT,
772 	.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO |
773 			OBS_SOURCE_DO_NOT_DUPLICATE |
774 			OBS_SOURCE_CONTROLLABLE_MEDIA,
775 	.get_name = ffmpeg_source_getname,
776 	.create = ffmpeg_source_create,
777 	.destroy = ffmpeg_source_destroy,
778 	.get_defaults = ffmpeg_source_defaults,
779 	.get_properties = ffmpeg_source_getproperties,
780 	.activate = ffmpeg_source_activate,
781 	.deactivate = ffmpeg_source_deactivate,
782 	.video_tick = ffmpeg_source_tick,
783 	.missing_files = ffmpeg_source_missingfiles,
784 	.update = ffmpeg_source_update,
785 	.icon_type = OBS_ICON_TYPE_MEDIA,
786 	.media_play_pause = ffmpeg_source_play_pause,
787 	.media_restart = ffmpeg_source_restart,
788 	.media_stop = ffmpeg_source_stop,
789 	.media_get_duration = ffmpeg_source_get_duration,
790 	.media_get_time = ffmpeg_source_get_time,
791 	.media_set_time = ffmpeg_source_set_time,
792 	.media_get_state = ffmpeg_source_get_state,
793 };
794