From c3dae7cf4b9399c12a6c00bae8ae1affe078d6c9 Mon Sep 17 00:00:00 2001 From: James Richardson Date: Sat, 30 Dec 2023 22:57:28 +0000 Subject: [PATCH] change logging on ctrl-c / log where profile came from --- README.md | 6 +- bin/gopro-dashboard.py | 438 ++++++++++++++++---------------- gopro_overlay/ffmpeg_profile.py | 11 +- 3 files changed, 230 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 791a95b..15698d2 100644 --- a/README.md +++ b/README.md @@ -120,15 +120,13 @@ of [examples](docs/xml/examples/README.md) ## FFMPEG Control & GPUs -*Experimental* - FFMPEG has **a lot** of options! This program comes with some mostly sensible defaults, but to use GPUs and control the output much more carefully, including framerates and bitrates, you can use a JSON file containing a number of 'profiles' and select the profile you want when running the program. -For more details on how to select these, and an example of Nvidia GPU, please see [docs/bin/PERFORMANCE_GUIDE.md](docs/bin/PERFORMANCE_GUIDE.md) +For more details on how to select these, and an example of Nvidia GPU, please see the guide in [docs/bin#ffmpeg-profiles](docs/bin#ffmpeg-profiles) -Please also see [PERFORMANCE.md](PERFORMANCE.md) +Please also see other docs [PERFORMANCE.md](PERFORMANCE.md) and [docs/bin/PERFORMANCE_GUIDE.md](docs/bin/PERFORMANCE_GUIDE.md) ## Converting to GPX files diff --git a/bin/gopro-dashboard.py b/bin/gopro-dashboard.py index af57303..2961e07 100644 --- a/bin/gopro-dashboard.py +++ b/bin/gopro-dashboard.py @@ -133,259 +133,261 @@ def fmtdt(dt: datetime.datetime): timers = Timers(printing=args.print_timings) - with timers.timer("program"): - with timers.timer("loading timeseries"): + try: + with timers.timer("program"): + with timers.timer("loading timeseries"): - if args.use_gpx_only: + if args.use_gpx_only: - start_date: Optional[datetime.datetime] = None - end_date: Optional[datetime.datetime] = None - duration: Optional[Timeunit] = None + start_date: Optional[datetime.datetime] = None + end_date: Optional[datetime.datetime] = None + duration: Optional[Timeunit] = None - if args.input: - inputpath = assert_file_exists(args.input) + if args.input: + inputpath = assert_file_exists(args.input) - recording = ffmpeg_gopro.find_recording(inputpath) - dimensions = recording.video.dimension + recording = ffmpeg_gopro.find_recording(inputpath) + dimensions = recording.video.dimension - duration = recording.video.duration + duration = recording.video.duration - fns = { - "file-created": lambda f: f.ctime, - "file-modified": lambda f: f.mtime, - "file-accessed": lambda f: f.atime - } + fns = { + "file-created": lambda f: f.ctime, + "file-modified": lambda f: f.mtime, + "file-accessed": lambda f: f.atime + } - if args.video_time_start: - start_date = fns[args.video_time_start](recording.file) - end_date = start_date + duration.timedelta() + if args.video_time_start: + start_date = fns[args.video_time_start](recording.file) + end_date = start_date + duration.timedelta() - if args.video_time_end: - start_date = fns[args.video_time_end](recording.file) - duration.timedelta() - end_date = start_date + duration.timedelta() + if args.video_time_end: + start_date = fns[args.video_time_end](recording.file) - duration.timedelta() + end_date = start_date + duration.timedelta() - else: - generate = "overlay" + else: + generate = "overlay" + + external_file: Path = assert_file_exists(args.gpx) + fit_or_gpx_timeseries = load_external(external_file, units) - external_file: Path = assert_file_exists(args.gpx) - fit_or_gpx_timeseries = load_external(external_file, units) + log(f"GPX/FIT file: {fmtdt(fit_or_gpx_timeseries.min)} -> {fmtdt(fit_or_gpx_timeseries.max)}") - log(f"GPX/FIT file: {fmtdt(fit_or_gpx_timeseries.min)} -> {fmtdt(fit_or_gpx_timeseries.max)}") + # Give a bit of information here about what is going on + if start_date is not None: + log(f"Video File Dates: {fmtdt(start_date)} -> {fmtdt(end_date)}") - # Give a bit of information here about what is going on - if start_date is not None: - log(f"Video File Dates: {fmtdt(start_date)} -> {fmtdt(end_date)}") + overlap = DateRange(start=start_date, end=end_date).overlap_seconds( + DateRange(start=fit_or_gpx_timeseries.min, end=fit_or_gpx_timeseries.max) + ) - overlap = DateRange(start=start_date, end=end_date).overlap_seconds( - DateRange(start=fit_or_gpx_timeseries.min, end=fit_or_gpx_timeseries.max) + if overlap == 0: + fatal( + "Video file and GPX/FIT file don't overlap in time - See " + "https://github.com/time4tea/gopro-dashboard-overlay/tree/main/docs/bin#create-a-movie" + "-from-gpx-and-video-not-created-with-gopro" + ) + + frame_meta = timeseries_to_framemeta( + fit_or_gpx_timeseries, + units, + start_date=start_date, + duration=duration ) + video_duration = frame_meta.duration() + packets_per_second = 10 + else: + inputpath = assert_file_exists(args.input) - if overlap == 0: - fatal( - "Video file and GPX/FIT file don't overlap in time - See " - "https://github.com/time4tea/gopro-dashboard-overlay/tree/main/docs/bin#create-a-movie" - "-from-gpx-and-video-not-created-with-gopro" + counter = ReasonCounter() + + loader = GoproLoader( + ffmpeg_gopro=ffmpeg_gopro, + units=units, + flags=args.load, + gps_lock_filter=gpmd_filters.standard( + dop_max=args.gps_dop_max, + speed_max=units.Quantity(args.gps_speed_max, args.gps_speed_max_units), + bbox=args.gps_bbox_lon_lat, + report=counter.because ) - - frame_meta = timeseries_to_framemeta( - fit_or_gpx_timeseries, - units, - start_date=start_date, - duration=duration - ) - video_duration = frame_meta.duration() - packets_per_second = 10 - else: - inputpath = assert_file_exists(args.input) - - counter = ReasonCounter() - - loader = GoproLoader( - ffmpeg_gopro=ffmpeg_gopro, - units=units, - flags=args.load, - gps_lock_filter=gpmd_filters.standard( - dop_max=args.gps_dop_max, - speed_max=units.Quantity(args.gps_speed_max, args.gps_speed_max_units), - bbox=args.gps_bbox_lon_lat, - report=counter.because ) - ) - gopro = loader.load(inputpath) + gopro = loader.load(inputpath) - gpmd_filters.poor_report(counter) + gpmd_filters.poor_report(counter) - frame_meta = gopro.framemeta + frame_meta = gopro.framemeta - dimensions = gopro.recording.video.dimension - video_duration = gopro.recording.video.duration - packets_per_second = frame_meta.packets_per_second() + dimensions = gopro.recording.video.dimension + video_duration = gopro.recording.video.duration + packets_per_second = frame_meta.packets_per_second() - if len(frame_meta) == 0: - log("No GPS Information found in the Video - Was GPS Recording enabled?") - log("If you have a GPX File, See https://github.com/time4tea/gopro-dashboard-overlay/tree/main" - "/docs/bin#create-a-movie-from-gpx-and-video-not-created-with-gopro") - exit(1) + if len(frame_meta) == 0: + log("No GPS Information found in the Video - Was GPS Recording enabled?") + log("If you have a GPX File, See https://github.com/time4tea/gopro-dashboard-overlay/tree/main" + "/docs/bin#create-a-movie-from-gpx-and-video-not-created-with-gopro") + exit(1) - if args.gpx: - external_file: Path = args.gpx - fit_or_gpx_timeseries = load_external(external_file, units) - log(f"GPX/FIT file: {fmtdt(fit_or_gpx_timeseries.min)} -> {fmtdt(fit_or_gpx_timeseries.max)}") - overlap = DateRange(start=frame_meta.date_at(frame_meta.min), - end=frame_meta.date_at(frame_meta.max)).overlap_seconds( - DateRange(start=fit_or_gpx_timeseries.min, end=fit_or_gpx_timeseries.max) + if args.gpx: + external_file: Path = args.gpx + fit_or_gpx_timeseries = load_external(external_file, units) + log(f"GPX/FIT file: {fmtdt(fit_or_gpx_timeseries.min)} -> {fmtdt(fit_or_gpx_timeseries.max)}") + overlap = DateRange(start=frame_meta.date_at(frame_meta.min), + end=frame_meta.date_at(frame_meta.max)).overlap_seconds( + DateRange(start=fit_or_gpx_timeseries.min, end=fit_or_gpx_timeseries.max) + ) + + if overlap == 0: + fatal( + "Video file and GPX/FIT file don't overlap in time - See " + "https://github.com/time4tea/gopro-dashboard-overlay/tree/main/docs/bin#create-a-movie" + "-from-gpx-and-video-not-created-with-gopro") + + log(f"GPX/FIT Timeseries has {len(fit_or_gpx_timeseries)} data points.. merging...") + merge_gpx_with_gopro(fit_or_gpx_timeseries, frame_meta) + + if args.overlay_size: + dimensions = dimension_from(args.overlay_size) + + if len(frame_meta) < 1: + fatal(f"Unable to load GoPro metadata from {inputpath}. Use --debug-metadata to see more information") + + log(f"Generating overlay at {dimensions}") + log(f"Timeseries has {len(frame_meta)} data points") + log("Processing....") + + with timers.timer("processing"): + locked_2d = lambda e: e.gpsfix in GPS_FIXED_VALUES + locked_3d = lambda e: e.gpsfix == GPSFix.LOCK_3D.value + + frame_meta.process(timeseries_process.process_ses("point", lambda i: i.point, alpha=0.45), + filter_fn=locked_2d) + frame_meta.process_deltas(timeseries_process.calculate_speeds(), skip=packets_per_second * 3, + filter_fn=locked_2d) + frame_meta.process(timeseries_process.calculate_odo(), filter_fn=locked_2d) + frame_meta.process_deltas(timeseries_process.calculate_gradient(), skip=packets_per_second * 3, + filter_fn=locked_3d) # hack + frame_meta.process(timeseries_process.process_kalman("speed", lambda e: e.speed)) + frame_meta.process(timeseries_process.filter_locked()) + + # privacy zone applies everywhere, not just at start, so might not always be suitable... + if args.privacy: + lat, lon, km = args.privacy.split(",") + privacy_zone = PrivacyZone( + Point(float(lat), float(lon)), + units.Quantity(float(km), units.km) + ) + else: + privacy_zone = NoPrivacyZone() + + with MapRenderer( + cache_dir=cache_dir, + styler=MapStyler( + api_key_finder=api_key_finder(config_loader, args) ) + ).open(args.map_style) as renderer: - if overlap == 0: - fatal( - "Video file and GPX/FIT file don't overlap in time - See " - "https://github.com/time4tea/gopro-dashboard-overlay/tree/main/docs/bin#create-a-movie" - "-from-gpx-and-video-not-created-with-gopro") - - log(f"GPX/FIT Timeseries has {len(fit_or_gpx_timeseries)} data points.. merging...") - merge_gpx_with_gopro(fit_or_gpx_timeseries, frame_meta) - - if args.overlay_size: - dimensions = dimension_from(args.overlay_size) - - if len(frame_meta) < 1: - fatal(f"Unable to load GoPro metadata from {inputpath}. Use --debug-metadata to see more information") - - log(f"Generating overlay at {dimensions}") - log(f"Timeseries has {len(frame_meta)} data points") - log("Processing....") - - with timers.timer("processing"): - locked_2d = lambda e: e.gpsfix in GPS_FIXED_VALUES - locked_3d = lambda e: e.gpsfix == GPSFix.LOCK_3D.value - - frame_meta.process(timeseries_process.process_ses("point", lambda i: i.point, alpha=0.45), - filter_fn=locked_2d) - frame_meta.process_deltas(timeseries_process.calculate_speeds(), skip=packets_per_second * 3, - filter_fn=locked_2d) - frame_meta.process(timeseries_process.calculate_odo(), filter_fn=locked_2d) - frame_meta.process_deltas(timeseries_process.calculate_gradient(), skip=packets_per_second * 3, - filter_fn=locked_3d) # hack - frame_meta.process(timeseries_process.process_kalman("speed", lambda e: e.speed)) - frame_meta.process(timeseries_process.filter_locked()) - - # privacy zone applies everywhere, not just at start, so might not always be suitable... - if args.privacy: - lat, lon, km = args.privacy.split(",") - privacy_zone = PrivacyZone( - Point(float(lat), float(lon)), - units.Quantity(float(km), units.km) - ) - else: - privacy_zone = NoPrivacyZone() + if args.profiler: + profiler = WidgetProfiler() + else: + profiler = None - with MapRenderer( - cache_dir=cache_dir, - styler=MapStyler( - api_key_finder=api_key_finder(config_loader, args) - ) - ).open(args.map_style) as renderer: + if args.profile: + ffmpeg_options = load_ffmpeg_profile(config_loader, args.profile) + else: + ffmpeg_options = None - if args.profiler: - profiler = WidgetProfiler() - else: - profiler = None + if args.show_ffmpeg: + redirect = None + else: + redirect = temp_file_name(suffix=".txt") + log(f"FFMPEG Output is in {redirect}") + + execution = InProcessExecution(redirect=redirect) + + output: Path = args.output + + if generate == "none": + ffmpeg = FFMPEGNull() + elif generate == "overlay": + output.unlink(missing_ok=True) + ffmpeg = FFMPEGOverlay( + ffmpeg=ffmpeg_exe, + output=output, + options=ffmpeg_options, + overlay_size=dimensions, + execution=execution + ) + else: + output.unlink(missing_ok=True) + ffmpeg = FFMPEGOverlayVideo( + ffmpeg=ffmpeg_exe, + input=inputpath, + output=output, + options=ffmpeg_options, + overlay_size=dimensions, + execution=execution + ) - if args.profile: - ffmpeg_options = load_ffmpeg_profile(config_loader, args.profile) - else: - ffmpeg_options = None + draw_timer = PoorTimer("drawing frames") - if args.show_ffmpeg: - redirect = None - else: - redirect = temp_file_name(suffix=".txt") - log(f"FFMPEG Output is in {redirect}") - - execution = InProcessExecution(redirect=redirect) - - output: Path = args.output - - if generate == "none": - ffmpeg = FFMPEGNull() - elif generate == "overlay": - output.unlink(missing_ok=True) - ffmpeg = FFMPEGOverlay( - ffmpeg=ffmpeg_exe, - output=output, - options=ffmpeg_options, - overlay_size=dimensions, - execution=execution + # Draw an overlay frame every 0.1 seconds of video + timelapse_correction = frame_meta.duration() / video_duration + log(f"Timelapse Factor = {timelapse_correction:.3f}") + stepper = frame_meta.stepper(timeunits(seconds=0.1 * timelapse_correction)) + progress = ProgressBarProgress("Render") + + unit_converters = Converters( + speed_unit=args.units_speed, + distance_unit=args.units_distance, + altitude_unit=args.units_altitude, + temperature_unit=args.units_temperature, ) - else: - output.unlink(missing_ok=True) - ffmpeg = FFMPEGOverlayVideo( - ffmpeg=ffmpeg_exe, - input=inputpath, - output=output, - options=ffmpeg_options, - overlay_size=dimensions, - execution=execution + + layout_creator = create_desired_layout( + layout=args.layout, + layout_xml=args.layout_xml, + dimensions=dimensions, + include=args.include, + exclude=args.exclude, + renderer=renderer, + timeseries=frame_meta, + font=font, + privacy_zone=privacy_zone, + profiler=profiler, + converters=unit_converters ) - draw_timer = PoorTimer("drawing frames") + overlay = Overlay(framemeta=frame_meta, create_widgets=layout_creator) - # Draw an overlay frame every 0.1 seconds of video - timelapse_correction = frame_meta.duration() / video_duration - log(f"Timelapse Factor = {timelapse_correction:.3f}") - stepper = frame_meta.stepper(timeunits(seconds=0.1 * timelapse_correction)) - progress = ProgressBarProgress("Render") + try: + progress.start(len(stepper)) + with ffmpeg.generate() as writer: - unit_converters = Converters( - speed_unit=args.units_speed, - distance_unit=args.units_distance, - altitude_unit=args.units_altitude, - temperature_unit=args.units_temperature, - ) + if args.double_buffer: + log("*** NOTE: Double Buffer mode is experimental. It is believed to work fine on Linux. " + "Please raise issues if you see it working or not-working. Thanks ***") + buffer = DoubleBuffer(dimensions, args.bg, writer) + else: + buffer = SingleBuffer(dimensions, args.bg, writer) - layout_creator = create_desired_layout( - layout=args.layout, - layout_xml=args.layout_xml, - dimensions=dimensions, - include=args.include, - exclude=args.exclude, - renderer=renderer, - timeseries=frame_meta, - font=font, - privacy_zone=privacy_zone, - profiler=profiler, - converters=unit_converters - ) + with buffer: + for index, dt in enumerate(stepper.steps()): + progress.update(index) + draw_timer.time(lambda: buffer.draw(lambda frame: overlay.draw(dt, frame))) - overlay = Overlay(framemeta=frame_meta, create_widgets=layout_creator) + log("Finished drawing frames. waiting for ffmpeg to catch up") + progress.complete() - try: - progress.start(len(stepper)) - with ffmpeg.generate() as writer: + finally: + for t in [draw_timer]: + log(t) - if args.double_buffer: - log("*** NOTE: Double Buffer mode is experimental. It is believed to work fine on Linux. " - "Please raise issues if you see it working or not-working. Thanks ***") - buffer = DoubleBuffer(dimensions, args.bg, writer) - else: - buffer = SingleBuffer(dimensions, args.bg, writer) - - with buffer: - for index, dt in enumerate(stepper.steps()): - progress.update(index) - draw_timer.time(lambda: buffer.draw(lambda frame: overlay.draw(dt, frame))) - - log("Finished drawing frames. waiting for ffmpeg to catch up") - progress.complete() - - except KeyboardInterrupt: - log("...Stopping...") - finally: - for t in [draw_timer]: - log(t) - - if profiler: - log("\n\n*** Widget Timings ***") - profiler.print() - log("***\n\n") + if profiler: + log("\n\n*** Widget Timings ***") + profiler.print() + log("***\n\n") + + except KeyboardInterrupt: + log("User interrupted...") diff --git a/gopro_overlay/ffmpeg_profile.py b/gopro_overlay/ffmpeg_profile.py index cf8dc3b..ad7b506 100644 --- a/gopro_overlay/ffmpeg_profile.py +++ b/gopro_overlay/ffmpeg_profile.py @@ -1,9 +1,12 @@ from typing import Mapping -from gopro_overlay.config import Config -from gopro_overlay.ffmpeg_overlay import FFMPEGOptions +from .config import Config +from .ffmpeg_overlay import FFMPEGOptions +from .log import log -builtin_profiles = {} +builtin_profiles = { + +} class FFMPEGProfiles: @@ -16,12 +19,14 @@ def load_profile(self, name: str) -> FFMPEGOptions: if config_file.exists(): if name in config_file.content: + log(f"Using *user-defined* profile: {name}") try: return self.load_profile_content(config_file.content, name) except ValueError as e: raise ValueError(f"{config_file.location}: {e}") from None if name in builtin_profiles: + log(f"Using *built-in* profile: {name}") profile = builtin_profiles[name] return FFMPEGOptions(input=profile["input"], output=profile["output"], filter_spec=profile["filter"])