VariaReEncoder / garmin_varia_transcode.sh
Newer Older
1558 lines | 48.301kb
Bogdan Timofte authored a month ago
1
#!/usr/bin/env bash
2
set -euo pipefail
3

            
4
# Garmin Varia batch transcoder (macOS/Linux)
5
#
6
# Apple compatibility note:
7
# For HEVC outputs we explicitly set -tag:v hvc1 (not hev1), which is required
8
# for reliable QuickTime / Photos compatibility.
9
#
10
# Encoding strategy note:
11
# Do not derive output bitrate mechanically from fixed Garmin source sizes.
12
# Prefer CRF (software) or constrained VBR (hardware), as used below.
13

            
Bogdan Timofte authored a month ago
14
TOOL_NAME="ffmpeg"
Bogdan Timofte authored a month ago
15

            
16
DEFAULT_SOURCE="."
17
DEFAULT_MODE="hardware"
18
DEFAULT_EXTENSIONS="mp4,mov,avi,m4v"
19
DEFAULT_CRF_HEVC=20
20
DEFAULT_CRF_H264=19
21

            
Bogdan Timofte authored a month ago
22
# Garmin publicly declares a 140-degree field of view.
23
# We infer practical focal metadata from this single spec.
24
# 35mm-equivalent focal length is integer-only in EXIF, so we round to 7 mm.
25
GARMIN_DECLARED_FOV_DEG=140
26
GARMIN_INFERRED_FOCAL_MM="6.5"
27
GARMIN_INFERRED_FOCAL_35MM_MM="7"
28

            
Bogdan Timofte authored a month ago
29
SOURCE_DIR="$DEFAULT_SOURCE"
30
DEST_DIR="$DEFAULT_SOURCE"
Bogdan Timofte authored a month ago
31
STAGING_DIR=""
Bogdan Timofte authored a month ago
32
MODE="$DEFAULT_MODE"
33
EXTENSIONS_CSV="$DEFAULT_EXTENSIONS"
34
CRF_OVERRIDE=""
35
OVERWRITE=true
36
DRY_RUN=false
37
RECURSIVE=true
38
SINGLE_FILE=""
39
VERBOSE=false
40
MOVE_SOURCE=false
41
SOURCE_PROVIDED=false
42
DEST_PROVIDED=false
Bogdan Timofte authored a month ago
43
STAGING_PROVIDED=false
Bogdan Timofte authored a month ago
44
FAIL_FAST=true
45
SOURCE_READABLE_MODE="normal"
46
APPLE_REPACK_FALLBACK=true
47
REPACKED_SOURCE_PATH=""
Bogdan Timofte authored a month ago
48

            
49
HAS_VIDEOTOOLBOX=false
50
HAS_LIBX265=false
51
HAS_LIBX264=false
Bogdan Timofte authored a month ago
52
HAS_AVCONVERT=false
Bogdan Timofte authored a month ago
53

            
54
ENCODER_KIND=""
55
VIDEO_CODEC=""
56
VIDEO_CRF=""
57
VIDEO_ARGS=()
58
FIND_EXT_EXPR=()
Bogdan Timofte authored a month ago
59
EXT_LIST=()
60
VIDEO_FILES=()
Bogdan Timofte authored a month ago
61

            
62
VIDEOS_PROCESSED=0
63
VIDEOS_SKIPPED=0
64
JSON_COPIED=0
65
JSON_SKIPPED=0
66
ERRORS=0
Bogdan Timofte authored a month ago
67
INVALID_SOURCES_SKIPPED=0
68
DESTINATION_FAILURES=0
Bogdan Timofte authored a month ago
69
TOTAL_VIDEO_TIME_SEC=0
Bogdan Timofte authored a month ago
70
TOTAL_FILE_REAL_TIME_SEC=0
Bogdan Timofte authored a month ago
71

            
72
DURATION_TOLERANCE_SEC=1.0
Bogdan Timofte authored a month ago
73
STOP_AFTER_CURRENT=false
74
INTERRUPT_COUNT=0
75
CURRENT_FFMPEG_PID=""
76
PROGRESS_LINE_OPEN=false
Bogdan Timofte authored a month ago
77

            
78
usage() {
79
  cat <<'EOF'
80
Usage:
81
  garmin_varia_transcode.sh [options]
82

            
83
Options:
84
  -s, --source, --input DIR       Source directory or single file (default: current directory)
85
  -d, --destination, --output DIR Destination directory (default: current directory)
Bogdan Timofte authored a month ago
86
  --staging-dir DIR               Temporary staging directory for intermediate output files
87
                                  (falls back to destination if staging cannot be used)
Bogdan Timofte authored a month ago
88
  --mode MODE                     Encoding mode (default: hardware); see Encoding Modes below
89
  --crf N                         CRF value for quality/compat modes (lower = better; default: 20/19)
90
  --no-overwrite                  Skip files that already exist at destination (default: overwrite)
91
  --dry-run                       Print actions without writing files
92
  --no-recursive                  Process only the top-level source directory (default: recursive)
93
  --extensions LIST               Comma-separated video extensions (default: mp4,mov,avi,m4v)
94
  --verbose                       Log each operation with timestamp; show ffmpeg/ffprobe output
Bogdan Timofte authored a month ago
95
  --delete-source                 Delete source file only after strict post-encode validation
96
  --keep-going                    Continue after source-file failures (default: stop)
97
  --unattended                    Preset for long runs: --delete-source + --keep-going
98
  --no-apple-repack-fallback      Disable macOS avconvert fallback for unreadable MP4/MOV sources
99
  --apple-repack-fallback         Enable macOS avconvert fallback (default)
Bogdan Timofte authored a month ago
100
  -h, --help                      Show this help
101

            
102
Encoding Modes:
103
  hardware   Uses hevc_videotoolbox (Apple Silicon / Intel Mac GPU). macOS only.
104
             ~4-5s per 30s clip, ~35W (measured on Apple Silicon MacBook Pro).
105
             Best choice when running on battery or transcoding large libraries.
106
             Output quality is good for dashcam/action footage.
107
             Falls back to an error on Linux or if videotoolbox is absent.
108

            
109
  auto       Like hardware on macOS with videotoolbox, otherwise falls back to quality,
110
             then compat. Safe cross-platform default when the machine is unknown.
111

            
112
  quality    Uses libx265 (software HEVC, CRF 20). Platform-independent.
113
             ~50s per 30s clip, ~80W (measured on Apple Silicon MacBook Pro).
114
             Best compression ratio and quality for archival, high-resolution, or visually
115
             complex sources (e.g. cinema, screen recordings).
116
             Overkill for dashcam footage; prefer hardware or auto for those.
117

            
118
  compat     Uses libx264 (software H.264, CRF 19). Maximum player compatibility.
119
             Use when the destination player cannot decode HEVC (older TVs, Android 4.x,
120
             web browsers without HEVC support). Larger files than HEVC modes.
121

            
122
Behavior:
123
  - Output is always .mp4 (H.264 or HEVC depending on mode, always Apple-compatible)
124
  - HEVC outputs are tagged hvc1 for QuickTime / Apple Photos compatibility
Bogdan Timofte authored a month ago
125
  - Metadata is restored from source to output with exiftool
126
  - Garmin camera model is also mapped to standard Make/Model tags for compatibility
127
  - Inferred lens metadata is written from Garmin's declared 140-degree FOV
128
  - Transcoding encoder/mode is written in metadata (Software)
Bogdan Timofte authored a month ago
129
  - Original directory structure is preserved under destination
130
  - When --source points to a file, only that file is processed
131
  - JSON sidecar files found in source are copied 1:1 to destination
132
  - telemetry_manifest.json is created in destination as a placeholder contract
133

            
134
Examples:
135
  ./garmin_varia_transcode.sh -s SampleFootage -d /Volumes/Archive
136
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode auto
137
  ./garmin_varia_transcode.sh -s clip.mp4 -d encoded --dry-run --verbose
138
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode quality --crf 18
139
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode compat
Bogdan Timofte authored a month ago
140
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --delete-source
141
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --unattended
Bogdan Timofte authored a month ago
142
EOF
143
}
144

            
145
log_msg() {
146
  local level="$1"
147
  shift
148
  local ts
149
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
150
  echo "[$ts] [$level] $*"
151
}
152

            
153
# Verbose-only log: suppressed in default quiet mode
154
vlog_msg() {
155
  [[ "$VERBOSE" == true ]] && log_msg "$@"
156
  return 0
157
}
158

            
Bogdan Timofte authored a month ago
159
# Quiet-mode per-file progress lines
160
log_progress_start() {
Bogdan Timofte authored a month ago
161
  local input_file="$1"
Bogdan Timofte authored a month ago
162
  local ts
163
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
164
  printf '%s : Transcoding %s ...' "$ts" "$input_file"
165
  PROGRESS_LINE_OPEN=true
166
}
167

            
168
log_progress_done() {
Bogdan Timofte authored a month ago
169
  local real_elapsed_sec="$2"
170
  local encode_elapsed_sec="$3"
Bogdan Timofte authored a month ago
171
  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
172
    printf ' done in %ss (encode %ss)\n' "$real_elapsed_sec" "$encode_elapsed_sec"
173
    PROGRESS_LINE_OPEN=false
174
    return
175
  fi
176

            
177
  local input_file="$1"
Bogdan Timofte authored a month ago
178
  local ts
179
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
Bogdan Timofte authored a month ago
180
  echo "$ts : Transcoding $input_file ... done in ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s)"
Bogdan Timofte authored a month ago
181
}
182

            
Bogdan Timofte authored a month ago
183
log_progress_failed() {
184
  local input_file="$1"
185
  local real_elapsed_sec="$2"
186
  local encode_elapsed_sec="$3"
187
  local ffmpeg_rc="$4"
188
  local ffmpeg_log="$5"
189

            
190
  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
191
    printf ' FAILED after %ss (encode %ss, rc=%s, log=%s)\n' "$real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "${ffmpeg_log:-n/a}"
192
    PROGRESS_LINE_OPEN=false
193
    return
194
  fi
195

            
196
  local ts
197
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
198
  echo "$ts : Transcoding $input_file ... FAILED after ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s, rc=$ffmpeg_rc, log=${ffmpeg_log:-n/a})"
199
}
200

            
201
log_progress_skipped_unreadable() {
202
  local input_file="$1"
203

            
204
  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
205
    printf ' SKIPPED (unreadable/corrupted source)\n'
206
    PROGRESS_LINE_OPEN=false
207
    return
208
  fi
209

            
210
  local ts
211
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
212
  echo "$ts : Transcoding $input_file ... SKIPPED (unreadable/corrupted source)"
213
}
214

            
Bogdan Timofte authored a month ago
215
die() {
216
  log_msg "ERROR" "$*"
217
  exit 1
218
}
219

            
Bogdan Timofte authored a month ago
220
handle_interrupt() {
221
  INTERRUPT_COUNT=$((INTERRUPT_COUNT + 1))
222
  STOP_AFTER_CURRENT=true
223

            
224
  if [[ "$INTERRUPT_COUNT" -eq 1 ]]; then
225
    if [[ -n "$CURRENT_FFMPEG_PID" ]]; then
226
      log_msg "WARN" "Stop requested. Will stop after current file. Press Ctrl+C again to abort current encode immediately."
227
    else
228
      log_msg "WARN" "Stop requested. Exiting before next file."
229
    fi
230
    return
231
  fi
232

            
233
  if [[ -n "$CURRENT_FFMPEG_PID" ]]; then
234
    log_msg "WARN" "Force-stopping current encode (pid=$CURRENT_FFMPEG_PID)."
235
    kill -INT "$CURRENT_FFMPEG_PID" 2>/dev/null || true
236
  fi
237
}
238

            
239
run_ffmpeg_with_signal_guard() {
240
  (
241
    trap '' INT TERM
242
    exec "$@"
243
  ) &
244

            
245
  CURRENT_FFMPEG_PID=$!
246
  if wait "$CURRENT_FFMPEG_PID"; then
247
    CURRENT_FFMPEG_PID=""
248
    return 0
249
  fi
250

            
251
  local rc=$?
252
  CURRENT_FFMPEG_PID=""
253
  return "$rc"
254
}
255

            
Bogdan Timofte authored a month ago
256
format_seconds() {
257
  local sec="$1"
258
  local h m s
259
  h=$((sec / 3600))
260
  m=$(((sec % 3600) / 60))
261
  s=$((sec % 60))
262
  printf '%02d:%02d:%02d' "$h" "$m" "$s"
263
}
264

            
Bogdan Timofte authored a month ago
265
print_final_report() {
266
  local total_elapsed_sec="$1"
267
  local total_elapsed_fmt="$2"
268
  local file_post_total_sec="$3"
269
  local file_post_total_fmt="$4"
270
  local run_non_file_overhead_sec="$5"
271
  local run_non_file_overhead_fmt="$6"
272
  local avg_file_real_time_sec="$7"
273
  local avg_file_real_time_fmt="$8"
274
  local avg_video_time_sec="$9"
275
  local avg_video_time_fmt="${10}"
276

            
277
  printf '\n'
278
  printf 'Run Summary\n'
279
  printf '+---------------------------+-------+\n'
280
  printf '| %-25s | %-5s |\n' "Metric" "Value"
281
  printf '+---------------------------+-------+\n'
282
  printf '| %-25s | %5s |\n' "Videos processed" "$VIDEOS_PROCESSED"
283
  printf '| %-25s | %5s |\n' "Videos skipped" "$VIDEOS_SKIPPED"
284
  printf '| %-25s | %5s |\n' "Invalid sources skipped" "$INVALID_SOURCES_SKIPPED"
285
  printf '| %-25s | %5s |\n' "Destination failures" "$DESTINATION_FAILURES"
286
  printf '| %-25s | %5s |\n' "JSON copied" "$JSON_COPIED"
287
  printf '| %-25s | %5s |\n' "JSON skipped" "$JSON_SKIPPED"
288
  printf '| %-25s | %5s |\n' "Errors" "$ERRORS"
289
  printf '+---------------------------+-------+\n'
290

            
291
  printf '\nTimings\n'
292
  printf '+---------------------------+---------+----------+\n'
293
  printf '| %-25s | %-7s | %-8s |\n' "Metric" "Seconds" "Elapsed"
294
  printf '+---------------------------+---------+----------+\n'
295
  printf '| %-25s | %7s | %-8s |\n' "Total run time" "${total_elapsed_sec}s" "$total_elapsed_fmt"
296
  printf '| %-25s | %7s | %-8s |\n' "File wall time total" "${TOTAL_FILE_REAL_TIME_SEC}s" "$(format_seconds "$TOTAL_FILE_REAL_TIME_SEC")"
297
  printf '| %-25s | %7s | %-8s |\n' "Encode time total" "${TOTAL_VIDEO_TIME_SEC}s" "$(format_seconds "$TOTAL_VIDEO_TIME_SEC")"
298
  printf '| %-25s | %7s | %-8s |\n' "Post-processing total" "${file_post_total_sec}s" "$file_post_total_fmt"
299
  printf '| %-25s | %7s | %-8s |\n' "Non-file overhead" "${run_non_file_overhead_sec}s" "$run_non_file_overhead_fmt"
300
  printf '+---------------------------+---------+----------+\n'
301

            
302
  printf '\nPer-File Averages\n'
303
  printf '+---------------------------+---------+----------+\n'
304
  printf '| %-25s | %-7s | %-8s |\n' "Metric" "Seconds" "Elapsed"
305
  printf '+---------------------------+---------+----------+\n'
306
  printf '| %-25s | %7s | %-8s |\n' "Average file wall time" "${avg_file_real_time_sec}s" "$avg_file_real_time_fmt"
307
  printf '| %-25s | %7s | %-8s |\n' "Average encode time" "${avg_video_time_sec}s" "$avg_video_time_fmt"
308
  printf '+---------------------------+---------+----------+\n'
309
}
310

            
Bogdan Timofte authored a month ago
311
make_temp_log_file() {
312
  local temp_path
313
  local base_tmp="${TMPDIR:-/tmp}"
314
  base_tmp="${base_tmp%/}"
315

            
316
  temp_path="$(mktemp "$base_tmp/varia_ffmpeg.XXXXXX.log" 2>/dev/null || true)"
317
  if [[ -z "$temp_path" ]]; then
318
    temp_path="$(mktemp -t varia_ffmpeg 2>/dev/null || true)"
319
  fi
320

            
321
  printf '%s\n' "$temp_path"
322
}
323

            
Bogdan Timofte authored a month ago
324
make_temp_output_file() {
325
  local target_file="$1"
Bogdan Timofte authored a month ago
326
  local preferred_dir="${2:-}"
327
  local target_dir target_base target_noext temp_path seed_path work_dir
Bogdan Timofte authored a month ago
328

            
329
  target_dir="$(dirname "$target_file")"
330
  target_base="$(basename "$target_file")"
331
  target_noext="${target_base%.*}"
Bogdan Timofte authored a month ago
332
  work_dir="$target_dir"
333

            
334
  if [[ -n "$preferred_dir" ]]; then
335
    work_dir="$preferred_dir"
336
  fi
337

            
338
  seed_path="$(mktemp "$work_dir/.varia_tmp.${target_noext}.XXXXXX" 2>/dev/null || true)"
339
  if [[ -z "$seed_path" && "$work_dir" != "$target_dir" ]]; then
340
    seed_path="$(mktemp "$target_dir/.varia_tmp.${target_noext}.XXXXXX" 2>/dev/null || true)"
341
  fi
Bogdan Timofte authored a month ago
342

            
343
  if [[ -n "$seed_path" ]]; then
344
    rm -f -- "$seed_path" 2>/dev/null || true
345
    temp_path="${seed_path}.mp4"
346
  else
Bogdan Timofte authored a month ago
347
    temp_path="$work_dir/.varia_tmp.${target_noext}.$$.$RANDOM.mp4"
Bogdan Timofte authored a month ago
348
    if ! touch "$temp_path" >/dev/null 2>&1; then
Bogdan Timofte authored a month ago
349
      if [[ "$work_dir" != "$target_dir" ]]; then
350
        temp_path="$target_dir/.varia_tmp.${target_noext}.$$.$RANDOM.mp4"
351
        if ! touch "$temp_path" >/dev/null 2>&1; then
352
          temp_path=""
353
        else
354
          rm -f -- "$temp_path" 2>/dev/null || true
355
        fi
356
      else
357
        temp_path=""
358
      fi
Bogdan Timofte authored a month ago
359
    else
360
      rm -f -- "$temp_path" 2>/dev/null || true
361
    fi
362
  fi
363

            
364
  printf '%s\n' "$temp_path"
365
}
366

            
367
cleanup_transcode_artifacts() {
368
  local temp_output="$1"
369
  local final_output="$2"
Bogdan Timofte authored a month ago
370
  local exif_backup_default="${temp_output}_original"
371
  local exif_backup_alt="${temp_output}.exif_original"
Bogdan Timofte authored a month ago
372

            
373
  rm -f -- "$temp_output" 2>/dev/null || true
Bogdan Timofte authored a month ago
374
  rm -f -- "$exif_backup_default" "$exif_backup_alt" 2>/dev/null || true
Bogdan Timofte authored a month ago
375

            
376
  # Defensive cleanup for previously failed non-atomic runs.
377
  if [[ -f "$final_output" && ! -s "$final_output" ]]; then
378
    rm -f -- "$final_output" 2>/dev/null || true
379
  fi
380
}
381

            
382
is_apple_noise_file() {
383
  local file_path="$1"
384
  local base
385
  base="$(basename "$file_path")"
386

            
387
  case "$base" in
388
    ._*|.DS_Store|.AppleDouble|._.DS_Store)
389
      return 0
390
      ;;
391
  esac
392

            
393
  return 1
394
}
395

            
Bogdan Timofte authored a month ago
396
require_value() {
397
  local flag="$1"
398
  local value="${2:-}"
399
  if [[ -z "$value" ]]; then
400
    die "Missing value for $flag"
401
  fi
402
}
403

            
404
to_abs_path() {
405
  local p="$1"
406
  if [[ "$p" = /* ]]; then
407
    printf '%s\n' "$p"
408
  else
409
    printf '%s\n' "$PWD/$p"
410
  fi
411
}
412

            
413
path_is_within() {
414
  local child="$1"
415
  local parent="$2"
416
  [[ "$child" == "$parent" || "$child" == "$parent"/* ]]
417
}
418

            
419
join_cmd_for_log() {
420
  local out=""
421
  local arg
422
  for arg in "$@"; do
423
    out+="$(printf '%q' "$arg") "
424
  done
425
  printf '%s\n' "${out% }"
426
}
427

            
428
parse_args() {
429
  while [[ $# -gt 0 ]]; do
430
    case "$1" in
431
      -s|--source|--input)
432
        require_value "$1" "${2:-}"
433
        SOURCE_DIR="$2"
434
        SOURCE_PROVIDED=true
435
        shift 2
436
        ;;
437
      -d|--destination|--output)
438
        require_value "$1" "${2:-}"
439
        DEST_DIR="$2"
440
        DEST_PROVIDED=true
441
        shift 2
442
        ;;
Bogdan Timofte authored a month ago
443
      --staging-dir)
444
        require_value "$1" "${2:-}"
445
        STAGING_DIR="$2"
446
        STAGING_PROVIDED=true
447
        shift 2
448
        ;;
Bogdan Timofte authored a month ago
449
      --mode)
450
        require_value "$1" "${2:-}"
451
        MODE="$2"
452
        shift 2
453
        ;;
454
      --crf)
455
        require_value "$1" "${2:-}"
456
        CRF_OVERRIDE="$2"
457
        shift 2
458
        ;;
459
      --overwrite)
460
        OVERWRITE=true
461
        shift
462
        ;;
463
      --no-overwrite)
464
        OVERWRITE=false
465
        shift
466
        ;;
467
      --dry-run)
468
        DRY_RUN=true
469
        shift
470
        ;;
471
      --recursive)
472
        RECURSIVE=true
473
        shift
474
        ;;
475
      --no-recursive)
476
        RECURSIVE=false
477
        shift
478
        ;;
479
      --extensions)
480
        require_value "$1" "${2:-}"
481
        EXTENSIONS_CSV="$2"
482
        shift 2
483
        ;;
484
      --single)
485
        require_value "$1" "${2:-}"
486
        SINGLE_FILE="$2"
487
        shift 2
488
        ;;
489
      --verbose)
490
        VERBOSE=true
491
        shift
492
        ;;
Bogdan Timofte authored a month ago
493
      --delete-source)
494
        MOVE_SOURCE=true
495
        shift
496
        ;;
497
      --keep-going)
498
        FAIL_FAST=false
499
        shift
500
        ;;
501
      --unattended)
502
        MOVE_SOURCE=true
503
        FAIL_FAST=false
504
        shift
505
        ;;
506
      --no-apple-repack-fallback)
507
        APPLE_REPACK_FALLBACK=false
508
        shift
509
        ;;
510
      --apple-repack-fallback)
511
        APPLE_REPACK_FALLBACK=true
512
        shift
513
        ;;
Bogdan Timofte authored a month ago
514
      --move-source)
515
        MOVE_SOURCE=true
516
        shift
517
        ;;
Bogdan Timofte authored a month ago
518
      --continue-on-error)
519
        FAIL_FAST=false
520
        shift
521
        ;;
Bogdan Timofte authored a month ago
522
      -h|--help)
523
        usage
524
        exit 0
525
        ;;
526
      *)
527
        die "Unknown argument: $1"
528
        ;;
529
    esac
530
  done
531

            
532
  case "$MODE" in
533
    auto|hardware|quality|compat) ;;
534
    *) die "Invalid --mode '$MODE'. Use auto|hardware|quality|compat." ;;
535
  esac
536

            
537
  if [[ "$SOURCE_PROVIDED" != true && "$DEST_PROVIDED" != true ]]; then
538
    die "At least one of --source or --destination must be provided"
539
  fi
540

            
541
  if [[ -n "$CRF_OVERRIDE" && ! "$CRF_OVERRIDE" =~ ^[0-9]+$ ]]; then
542
    die "--crf must be an integer"
543
  fi
544
}
545

            
546
check_tools() {
547
  command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found in PATH"
548
  command -v ffprobe >/dev/null 2>&1 || die "ffprobe not found in PATH"
Bogdan Timofte authored a month ago
549
  command -v exiftool >/dev/null 2>&1 || die "exiftool not found in PATH"
Bogdan Timofte authored a month ago
550

            
551
  HAS_AVCONVERT=false
552
  if [[ "$(uname -s)" == "Darwin" ]] && command -v avconvert >/dev/null 2>&1; then
553
    HAS_AVCONVERT=true
554
  fi
555
}
556

            
557
make_temp_repack_file() {
558
  local base_tmp="${TMPDIR:-/tmp}"
559
  local seed_path temp_path
560

            
561
  base_tmp="${base_tmp%/}"
562
  seed_path="$(mktemp "$base_tmp/varia_repack.XXXXXX" 2>/dev/null || true)"
563
  if [[ -n "$seed_path" ]]; then
564
    rm -f -- "$seed_path" 2>/dev/null || true
565
    temp_path="${seed_path}.mp4"
566
    printf '%s\n' "$temp_path"
567
    return
568
  fi
569

            
570
  temp_path="$base_tmp/varia_repack.$$.$RANDOM.mp4"
571
  printf '%s\n' "$temp_path"
572
}
573

            
574
cleanup_repacked_input() {
575
  local repacked_file="$1"
576
  if [[ -n "$repacked_file" ]]; then
577
    rm -f -- "$repacked_file" 2>/dev/null || true
578
  fi
579
}
580

            
581
try_apple_repack_for_unreadable() {
582
  local source_file="$1"
583
  local repacked_file=""
584

            
585
  REPACKED_SOURCE_PATH=""
586

            
587
  if [[ "$APPLE_REPACK_FALLBACK" != true || "$HAS_AVCONVERT" != true || "$(uname -s)" != "Darwin" ]]; then
588
    return 1
589
  fi
590

            
591
  repacked_file="$(make_temp_repack_file)"
592
  if [[ -z "$repacked_file" ]]; then
593
    return 1
594
  fi
595

            
596
  if [[ "$VERBOSE" == true ]]; then
597
    log_msg "WARN" "Trying avconvert passthrough repack fallback: $source_file"
598
    if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace; then
599
      cleanup_repacked_input "$repacked_file"
600
      return 1
601
    fi
602
  else
603
    if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace >/dev/null 2>&1; then
604
      cleanup_repacked_input "$repacked_file"
605
      return 1
606
    fi
607
  fi
608

            
609
  if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$repacked_file" >/dev/null 2>&1; then
610
    REPACKED_SOURCE_PATH="$repacked_file"
611
    return 0
612
  fi
613

            
614
  cleanup_repacked_input "$repacked_file"
615
  return 1
Bogdan Timofte authored a month ago
616
}
617

            
618
restore_metadata_with_exiftool() {
619
  local input_file="$1"
620
  local output_file="$2"
621

            
622
  vlog_msg "CHECKPOINT" "metadata_start: $output_file"
623

            
624
  if [[ "$VERBOSE" == true ]]; then
625
    if exiftool -overwrite_original -m -TagsFromFile "$input_file" -all:all -unsafe "$output_file"; then
626
      vlog_msg "CHECKPOINT" "metadata_end: $output_file"
627
      return 0
628
    fi
629
  else
630
    if exiftool -overwrite_original -m -q -q -TagsFromFile "$input_file" -all:all -unsafe "$output_file" >/dev/null 2>&1; then
631
      vlog_msg "CHECKPOINT" "metadata_end: $output_file"
632
      return 0
633
    fi
634
  fi
635

            
Bogdan Timofte authored a month ago
636
  if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
637
    log_msg "INFO" "Metadata restore interrupted by user while stopping: $output_file"
638
    return 1
639
  fi
640

            
Bogdan Timofte authored a month ago
641
  log_msg "ERROR" "Failed to restore metadata with exiftool: $output_file"
642
  return 1
643
}
644

            
645
map_garmin_model_to_standard_tags() {
646
  local input_file="$1"
647
  local output_file="$2"
648
  local garmin_model
649

            
650
  garmin_model="$(exiftool -s3 -UserData:GarminModel "$input_file" 2>/dev/null | head -n1 || true)"
651
  if [[ -z "$garmin_model" ]]; then
652
    vlog_msg "CHECKPOINT" "model_map_skip: no GarminModel in source: $input_file"
653
    return 0
654
  fi
655

            
Bogdan Timofte authored a month ago
656
  # garmin_model confirms this is a Garmin Varia source; fixed metadata values are
657
  # used below because Garmin does not embed a parseable model string we can map.
Bogdan Timofte authored a month ago
658
  vlog_msg "CHECKPOINT" "model_map_start: $output_file (GarminModel=$garmin_model)"
659

            
660
  if [[ "$VERBOSE" == true ]]; then
661
    if exiftool -overwrite_original -m \
662
      -UserData:Make="Garmin" -UserData:Model="Varia RCT715" \
663
      -Keys:Make="Garmin" -Keys:Model="Varia RCT715" \
Bogdan Timofte authored a month ago
664
      -Keys:CompatibleBrands="isom, iso2, mp41" \
665
      -Keys:MajorBrand="isom" \
Bogdan Timofte authored a month ago
666
      -Make="Garmin" -Model="Varia RCT715" \
667
      -VideoKeys:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
668
      -VideoKeys:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
669
      -XMP-exif:FocalLength="${GARMIN_INFERRED_FOCAL_MM} mm" \
670
      -XMP-exif:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
671
      -XMP-exifEX:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
672
      "$output_file"; then
673
      vlog_msg "CHECKPOINT" "model_map_end: $output_file"
674
      return 0
675
    fi
676
  else
677
    if exiftool -overwrite_original -m -q -q \
678
      -UserData:Make="Garmin" -UserData:Model="Varia RCT715" \
679
      -Keys:Make="Garmin" -Keys:Model="Varia RCT715" \
Bogdan Timofte authored a month ago
680
      -Keys:CompatibleBrands="isom, iso2, mp41" \
681
      -Keys:MajorBrand="isom" \
Bogdan Timofte authored a month ago
682
      -Make="Garmin" -Model="Varia RCT715" \
683
      -VideoKeys:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
684
      -VideoKeys:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
685
      -XMP-exif:FocalLength="${GARMIN_INFERRED_FOCAL_MM} mm" \
686
      -XMP-exif:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
687
      -XMP-exifEX:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
688
      "$output_file" >/dev/null 2>&1; then
689
      vlog_msg "CHECKPOINT" "model_map_end: $output_file"
690
      return 0
691
    fi
692
  fi
693

            
694
  log_msg "ERROR" "Failed to map GarminModel to standard Make/Model tags: $output_file"
695
  return 1
696
}
697

            
698
write_transcode_encoder_metadata() {
699
  local output_file="$1"
700
  local encoder_meta
701

            
702
  encoder_meta="$TOOL_NAME mode=$ENCODER_KIND codec=$VIDEO_CODEC"
703

            
704
  vlog_msg "CHECKPOINT" "encoder_meta_start: $output_file ($encoder_meta)"
705

            
706
  if [[ "$VERBOSE" == true ]]; then
707
    if exiftool -overwrite_original -m \
708
      -Software="$encoder_meta" \
709
      -UserData:Software="$encoder_meta" \
710
      "$output_file"; then
711
      vlog_msg "CHECKPOINT" "encoder_meta_end: $output_file"
712
      return 0
713
    fi
714
  else
715
    if exiftool -overwrite_original -m -q -q \
716
      -Software="$encoder_meta" \
717
      -UserData:Software="$encoder_meta" \
718
      "$output_file" >/dev/null 2>&1; then
719
      vlog_msg "CHECKPOINT" "encoder_meta_end: $output_file"
720
      return 0
721
    fi
722
  fi
723

            
724
  log_msg "ERROR" "Failed to write encoder metadata: $output_file"
725
  return 1
Bogdan Timofte authored a month ago
726
}
727

            
728
detect_encoders() {
729
  local encoders
730
  encoders="$(ffmpeg -hide_banner -encoders 2>&1 || true)"
731

            
732
  if echo "$encoders" | grep -Eq '(^|[[:space:]])hevc_videotoolbox([[:space:]]|$)'; then
733
    HAS_VIDEOTOOLBOX=true
734
  fi
735
  if echo "$encoders" | grep -Eq '(^|[[:space:]])libx265([[:space:]]|$)'; then
736
    HAS_LIBX265=true
737
  fi
738
  if echo "$encoders" | grep -Eq '(^|[[:space:]])libx264([[:space:]]|$)'; then
739
    HAS_LIBX264=true
740
  fi
741

            
742
  if [[ "$VERBOSE" == true ]]; then
743
    log_msg "INFO" "Encoders: hevc_videotoolbox=$HAS_VIDEOTOOLBOX libx265=$HAS_LIBX265 libx264=$HAS_LIBX264"
744
  fi
745
}
746

            
747
resolve_encoder() {
748
  local os_name
749
  os_name="$(uname -s)"
750

            
751
  case "$MODE" in
752
    auto)
753
      if [[ "$os_name" == "Darwin" && "$HAS_VIDEOTOOLBOX" == true ]]; then
754
        ENCODER_KIND="hardware"
755
      elif [[ "$HAS_LIBX265" == true ]]; then
756
        ENCODER_KIND="quality"
757
      elif [[ "$HAS_LIBX264" == true ]]; then
758
        ENCODER_KIND="compat"
759
      else
760
        die "No suitable encoder found. Need one of hevc_videotoolbox, libx265, libx264."
761
      fi
762
      ;;
763
    hardware)
764
      [[ "$os_name" == "Darwin" ]] || die "--mode hardware is only supported on macOS (hevc_videotoolbox)"
765
      [[ "$HAS_VIDEOTOOLBOX" == true ]] || die "hevc_videotoolbox not available in ffmpeg"
766
      ENCODER_KIND="hardware"
767
      ;;
768
    quality)
769
      [[ "$HAS_LIBX265" == true ]] || die "libx265 not available in ffmpeg"
770
      ENCODER_KIND="quality"
771
      ;;
772
    compat)
773
      [[ "$HAS_LIBX264" == true ]] || die "libx264 not available in ffmpeg"
774
      ENCODER_KIND="compat"
775
      ;;
776
  esac
777

            
778
  case "$ENCODER_KIND" in
779
    hardware)
780
      VIDEO_CODEC="hevc_videotoolbox"
781
      VIDEO_CRF=""
782
      ;;
783
    quality)
784
      VIDEO_CODEC="libx265"
785
      VIDEO_CRF="${CRF_OVERRIDE:-$DEFAULT_CRF_HEVC}"
786
      ;;
787
    compat)
788
      VIDEO_CODEC="libx264"
789
      VIDEO_CRF="${CRF_OVERRIDE:-$DEFAULT_CRF_H264}"
790
      ;;
791
  esac
792

            
793
  vlog_msg "INFO" "Selected mode: $MODE (effective: $ENCODER_KIND, codec: $VIDEO_CODEC${VIDEO_CRF:+, crf: $VIDEO_CRF})"
794
}
795

            
796
probe_has_audio() {
797
  local input_file="$1"
798
  local out
799
  out="$(ffprobe -v error -select_streams a -show_entries stream=codec_type -of csv=p=0 "$input_file" || true)"
800
  [[ -n "$out" ]]
801
}
802

            
803
print_verbose_probe() {
804
  local input_file="$1"
805
  ffprobe -v error \
806
    -show_entries format=filename,format_name,duration,bit_rate,start_time:stream=index,codec_type,codec_name,profile,pix_fmt,width,height,avg_frame_rate,r_frame_rate,bit_rate \
807
    -of default=noprint_wrappers=1:nokey=0 "$input_file" || true
808
}
809

            
810
ffprobe_duration_or_empty() {
811
  local file_path="$1"
812
  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" 2>/dev/null | head -n1
813
}
814

            
815
ffprobe_video_codec_or_empty() {
816
  local file_path="$1"
817
  ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file_path" 2>/dev/null | head -n1
818
}
819

            
Bogdan Timofte authored a month ago
820
source_video_is_readable() {
821
  local file_path="$1"
822
  REPACKED_SOURCE_PATH=""
823

            
824
  if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then
825
    SOURCE_READABLE_MODE="normal"
826
    return 0
827
  fi
828

            
829
  if [[ "$VERBOSE" == true ]]; then
830
    log_msg "WARN" "Source probe failed, trying tolerant mode: $file_path"
831
  fi
832

            
833
  if ffprobe -v error -fflags +genpts -err_detect ignore_err -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then
834
    SOURCE_READABLE_MODE="tolerant"
835
    vlog_msg "INFO" "Source readable in tolerant mode: $file_path"
836
    return 0
837
  fi
838

            
839
  if try_apple_repack_for_unreadable "$file_path"; then
840
    SOURCE_READABLE_MODE="repacked"
841
    log_msg "WARN" "Using avconvert repack fallback for unreadable source: $file_path"
842
    return 0
843
  fi
844

            
845
  SOURCE_READABLE_MODE="normal"
846
  return 1
847
}
848

            
849
source_error_from_ffmpeg_log() {
850
  local ffmpeg_log="$1"
851
  [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]] || return 1
852
  grep -Eiq 'invalid data found when processing input|error reading header|moov atom not found|corrupt|truncated|end of file|contradictionary STSC and STCO' "$ffmpeg_log"
853
}
854

            
855
destination_cannot_accept_file() {
856
  local input_file="$1"
857
  local out_dir="$2"
858
  local ffmpeg_log="$3"
859
  local probe_path=""
860
  local avail_kb=""
861
  local avail_bytes=0
862

            
863
  if [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]]; then
864
    if grep -Eiq 'no space left on device|disk quota exceeded|read-only file system|permission denied|file too large|input/output error|stale file handle' "$ffmpeg_log"; then
865
      return 0
866
    fi
867
  fi
868

            
869
  probe_path="$(mktemp "$out_dir/.varia_write_probe.XXXXXX" 2>/dev/null || true)"
870
  if [[ -z "$probe_path" ]]; then
871
    return 0
872
  fi
873
  rm -f -- "$probe_path" 2>/dev/null || true
874

            
875
  avail_kb="$(df -Pk "$out_dir" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
876
  if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then
877
    avail_bytes=$((avail_kb * 1024))
878
    if [[ "$avail_bytes" -lt 67108864 ]]; then
879
      return 0
880
    fi
881
  fi
882

            
883
  return 1
884
}
885

            
Bogdan Timofte authored a month ago
886
validate_transcoded_output() {
887
  local input_file="$1"
888
  local output_file="$2"
889

            
890
  if [[ ! -f "$output_file" ]]; then
891
    log_msg "ERROR" "Validation failed: destination file missing: $output_file"
892
    return 1
893
  fi
894

            
895
  local expected_codec actual_codec
896
  case "$ENCODER_KIND" in
897
    hardware|quality) expected_codec="hevc" ;;
898
    compat) expected_codec="h264" ;;
899
    *)
900
      log_msg "ERROR" "Validation failed: unknown encoder kind '$ENCODER_KIND'"
901
      return 1
902
      ;;
903
  esac
904

            
905
  actual_codec="$(ffprobe_video_codec_or_empty "$output_file")"
906
  if [[ -z "$actual_codec" ]]; then
907
    log_msg "ERROR" "Validation failed: ffprobe could not read output codec: $output_file"
908
    return 1
909
  fi
910
  if [[ "$actual_codec" != "$expected_codec" ]]; then
911
    log_msg "ERROR" "Validation failed: codec mismatch for $output_file (expected=$expected_codec actual=$actual_codec)"
912
    return 1
913
  fi
914

            
915
  local in_duration out_duration duration_delta
916
  in_duration="$(ffprobe_duration_or_empty "$input_file")"
917
  out_duration="$(ffprobe_duration_or_empty "$output_file")"
918

            
919
  if [[ -z "$in_duration" || -z "$out_duration" ]]; then
920
    log_msg "ERROR" "Validation failed: missing duration probe data (input='$in_duration' output='$out_duration')"
921
    return 1
922
  fi
923

            
924
  duration_delta="$(awk -v a="$in_duration" -v b="$out_duration" 'BEGIN{d=a-b; if (d<0) d=-d; printf "%.3f", d}')"
925
  if ! awk -v d="$duration_delta" -v t="$DURATION_TOLERANCE_SEC" 'BEGIN{exit !(d<=t)}'; then
926
    log_msg "ERROR" "Validation failed: duration delta too large for $output_file (input=${in_duration}s output=${out_duration}s delta=${duration_delta}s tolerance=${DURATION_TOLERANCE_SEC}s)"
927
    return 1
928
  fi
929

            
930
  vlog_msg "INFO" "Validation OK: $output_file (codec=$actual_codec input_dur=${in_duration}s output_dur=${out_duration}s delta=${duration_delta}s)"
931
  return 0
932
}
933

            
934
build_video_args() {
935
  VIDEO_ARGS=()
936

            
937
  case "$ENCODER_KIND" in
938
    hardware)
939
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -b:v 8M -maxrate 16M -bufsize 24M -tag:v hvc1 )
940
      ;;
941
    quality)
942
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -crf "$VIDEO_CRF" -preset slow -tag:v hvc1 )
943
      ;;
944
    compat)
945
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -crf "$VIDEO_CRF" -preset slow -pix_fmt yuv420p )
946
      ;;
947
  esac
948
}
949

            
950
normalize_source_dir() {
951
  SOURCE_DIR="$(to_abs_path "$SOURCE_DIR")"
952
  [[ -d "$SOURCE_DIR" ]] || die "Source directory not found: $SOURCE_DIR"
953
  SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
954
}
955

            
956
normalize_dest_dir() {
957
  DEST_DIR="$(to_abs_path "$DEST_DIR")"
958
}
959

            
Bogdan Timofte authored a month ago
960
normalize_staging_dir() {
961
  if [[ "$STAGING_PROVIDED" != true ]]; then
962
    STAGING_DIR=""
963
    return
964
  fi
965

            
966
  STAGING_DIR="$(to_abs_path "$STAGING_DIR")"
967
  [[ -d "$STAGING_DIR" ]] || die "Staging directory not found: $STAGING_DIR"
968
  STAGING_DIR="$(cd "$STAGING_DIR" && pwd)"
969
  [[ -w "$STAGING_DIR" ]] || die "Staging directory not writable: $STAGING_DIR"
970

            
971
  if path_is_within "$STAGING_DIR" "$SOURCE_DIR"; then
972
    die "Staging directory must not be inside source: staging=$STAGING_DIR source=$SOURCE_DIR"
973
  fi
974
}
975

            
Bogdan Timofte authored a month ago
976
collect_extensions() {
977
  local raw="$EXTENSIONS_CSV"
978
  local token
979
  EXT_LIST=()
980

            
981
  IFS=',' read -r -a tokens <<< "$raw"
982
  for token in "${tokens[@]}"; do
983
    token="$(printf '%s' "$token" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
984
    token="${token#.}"
985
    token="$(printf '%s' "$token" | tr '[:upper:]' '[:lower:]')"
986
    [[ -n "$token" ]] && EXT_LIST+=("$token")
987
  done
988

            
989
  if [[ ${#EXT_LIST[@]} -eq 0 ]]; then
990
    die "No valid extensions after parsing --extensions"
991
  fi
992
}
993

            
994
build_find_expr_for_extensions() {
995
  FIND_EXT_EXPR=()
996
  local ext
997
  for ext in "${EXT_LIST[@]}"; do
998
    FIND_EXT_EXPR+=( -iname "*.${ext}" -o )
999
  done
1000
  if [[ ${#FIND_EXT_EXPR[@]} -gt 0 ]]; then
1001
    unset 'FIND_EXT_EXPR[${#FIND_EXT_EXPR[@]}-1]'
1002
  fi
1003
}
1004

            
1005
rel_path_from_source() {
1006
  local abs_file="$1"
1007
  if path_is_within "$abs_file" "$SOURCE_DIR"; then
1008
    printf '%s\n' "${abs_file#$SOURCE_DIR/}"
1009
  else
1010
    printf '%s\n' "$(basename "$abs_file")"
1011
  fi
1012
}
1013

            
1014
collect_video_files() {
1015
  VIDEO_FILES=()
1016

            
1017
  if [[ -n "$SINGLE_FILE" ]]; then
1018
    local single_abs
1019
    single_abs="$(to_abs_path "$SINGLE_FILE")"
1020
    [[ -f "$single_abs" ]] || die "--single file not found: $single_abs"
1021
    VIDEO_FILES+=("$single_abs")
1022
    return
1023
  fi
1024

            
1025
  build_find_expr_for_extensions
1026

            
1027
  while IFS= read -r -d '' file; do
Bogdan Timofte authored a month ago
1028
    if is_apple_noise_file "$file"; then
1029
      vlog_msg "SKIP" "Ignoring Apple artifact: $file"
1030
      continue
1031
    fi
Bogdan Timofte authored a month ago
1032
    VIDEO_FILES+=("$file")
1033
  done < <(
1034
    if [[ "$RECURSIVE" == true ]]; then
1035
      find "$SOURCE_DIR" \
1036
        -path "$DEST_DIR" -prune -o \
1037
        -type f \( "${FIND_EXT_EXPR[@]}" \) -print0
1038
    else
1039
      find "$SOURCE_DIR" \
1040
        -maxdepth 1 -type f \( "${FIND_EXT_EXPR[@]}" \) -print0
1041
    fi
1042
  )
1043
}
1044

            
1045
process_video_file() {
1046
  local input_file="$1"
Bogdan Timofte authored a month ago
1047
  local rel_path output_file temp_output_file out_dir
1048
  local display_path
1049
  local encode_input_file
1050
  local repacked_input_file=""
Bogdan Timofte authored a month ago
1051
  local file_started_at file_ended_at file_real_elapsed_sec
1052
  local encode_started_at encode_ended_at encode_elapsed_sec=0
1053
  local post_elapsed_sec
1054

            
1055
  file_started_at="$(date +%s)"
1056
  vlog_msg "CHECKPOINT" "file_start: $input_file"
Bogdan Timofte authored a month ago
1057

            
1058
  rel_path="$(rel_path_from_source "$input_file")"
1059
  output_file="$DEST_DIR/${rel_path%.*}.mp4"
1060
  out_dir="$(dirname "$output_file")"
Bogdan Timofte authored a month ago
1061
  display_path="${input_file#$PWD/}"
1062
  encode_input_file="$input_file"
Bogdan Timofte authored a month ago
1063

            
1064
  mkdir -p "$out_dir"
1065

            
Bogdan Timofte authored a month ago
1066
  if ! source_video_is_readable "$input_file"; then
1067
    ERRORS=$((ERRORS + 1))
1068
    INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
1069
    if [[ "$VERBOSE" == true ]]; then
1070
      log_msg "ERROR" "Skipping unreadable/corrupted source video: $input_file"
1071
    else
1072
      log_progress_start "$display_path"
1073
      log_progress_skipped_unreadable "$display_path"
1074
    fi
1075
    return 2
1076
  fi
1077

            
1078
  if [[ "$SOURCE_READABLE_MODE" == "repacked" && -n "$REPACKED_SOURCE_PATH" ]]; then
1079
    repacked_input_file="$REPACKED_SOURCE_PATH"
1080
    encode_input_file="$REPACKED_SOURCE_PATH"
1081
    vlog_msg "INFO" "Encoding from avconvert repacked source: $repacked_input_file"
1082
  fi
1083

            
Bogdan Timofte authored a month ago
1084
  if [[ -f "$output_file" && "$OVERWRITE" != true ]]; then
1085
    vlog_msg "SKIP" "Video exists: $output_file"
1086
    VIDEOS_SKIPPED=$((VIDEOS_SKIPPED + 1))
Bogdan Timofte authored a month ago
1087
    cleanup_repacked_input "$repacked_input_file"
Bogdan Timofte authored a month ago
1088
    return
1089
  fi
1090

            
1091
  local has_audio=false
Bogdan Timofte authored a month ago
1092
  if probe_has_audio "$encode_input_file"; then
Bogdan Timofte authored a month ago
1093
    has_audio=true
1094
  fi
1095

            
1096
  if [[ "$VERBOSE" == true ]]; then
1097
    log_msg "INFO" "ffprobe summary: $input_file"
1098
    print_verbose_probe "$input_file"
1099
    log_msg "INFO" "Audio detected: $has_audio"
1100
  fi
1101

            
1102
  build_video_args
1103

            
Bogdan Timofte authored a month ago
1104
  temp_output_file="$(make_temp_output_file "$output_file" "$STAGING_DIR")"
Bogdan Timofte authored a month ago
1105
  if [[ -z "$temp_output_file" ]]; then
1106
    ERRORS=$((ERRORS + 1))
1107
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1108
    log_msg "ERROR" "Could not create temporary output file: $output_file"
1109
    return 3
1110
  fi
Bogdan Timofte authored a month ago
1111
  if [[ "$STAGING_PROVIDED" == true ]] && ! path_is_within "$temp_output_file" "$STAGING_DIR"; then
1112
    log_msg "WARN" "Staging unavailable for temp output; using destination directory: $temp_output_file"
1113
  fi
Bogdan Timofte authored a month ago
1114

            
Bogdan Timofte authored a month ago
1115
  local cmd=(ffmpeg -hide_banner)
Bogdan Timofte authored a month ago
1116
  if [[ "$SOURCE_READABLE_MODE" == "tolerant" ]]; then
1117
    cmd+=( -fflags +genpts -err_detect ignore_err )
1118
  fi
Bogdan Timofte authored a month ago
1119
  if [[ "$OVERWRITE" == true ]]; then
1120
    cmd+=( -y )
1121
  else
1122
    cmd+=( -n )
1123
  fi
1124

            
Bogdan Timofte authored a month ago
1125
  cmd+=( -i "$encode_input_file" -map 0:v:0 )
Bogdan Timofte authored a month ago
1126

            
1127
  if [[ "$has_audio" == true ]]; then
1128
    cmd+=( -map 0:a? -c:a aac -b:a 128k )
1129
  fi
1130

            
Bogdan Timofte authored a month ago
1131
  cmd+=( "${VIDEO_ARGS[@]}" -map_metadata 0 -movflags +faststart "$temp_output_file" )
Bogdan Timofte authored a month ago
1132

            
1133
  if [[ "$VERBOSE" == true ]]; then
1134
    log_msg "INFO" "Command: $(join_cmd_for_log "${cmd[@]}")"
1135
  fi
1136

            
1137
  if [[ "$DRY_RUN" == true ]]; then
1138
    log_msg "DRY-RUN" "Would transcode: $input_file -> $output_file"
1139
    return 0
1140
  fi
1141

            
Bogdan Timofte authored a month ago
1142
  if [[ "$VERBOSE" != true ]]; then
1143
    log_progress_start "$display_path"
1144
  fi
1145

            
1146
  vlog_msg "INFO" "Encoding: $input_file -> $output_file (temp: $temp_output_file)"
Bogdan Timofte authored a month ago
1147
  encode_started_at="$(date +%s)"
1148
  vlog_msg "CHECKPOINT" "encode_start: $input_file"
Bogdan Timofte authored a month ago
1149

            
1150
  local ffmpeg_rc=0
Bogdan Timofte authored a month ago
1151
  local ffmpeg_log=""
Bogdan Timofte authored a month ago
1152
  if [[ "$VERBOSE" == true ]]; then
1153
    # Verbose: show ffmpeg output directly
Bogdan Timofte authored a month ago
1154
    if run_ffmpeg_with_signal_guard "${cmd[@]}"; then
Bogdan Timofte authored a month ago
1155
      :
1156
    else
1157
      ffmpeg_rc=$?
1158
    fi
1159
  else
1160
    # Quiet (default): redirect ffmpeg output; keep log on failure
1161
    ffmpeg_log="$(make_temp_log_file)"
1162
    if [[ -z "$ffmpeg_log" ]]; then
1163
      ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1164
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
Bogdan Timofte authored a month ago
1165
      log_msg "ERROR" "Could not create temporary ffmpeg log file"
Bogdan Timofte authored a month ago
1166
      return 3
Bogdan Timofte authored a month ago
1167
    fi
Bogdan Timofte authored a month ago
1168
    if run_ffmpeg_with_signal_guard "${cmd[@]}" >"$ffmpeg_log" 2>&1; then
Bogdan Timofte authored a month ago
1169
      rm -f "$ffmpeg_log"
1170
    else
1171
      ffmpeg_rc=$?
1172
    fi
1173
  fi
1174

            
Bogdan Timofte authored a month ago
1175
  encode_ended_at="$(date +%s)"
1176
  encode_elapsed_sec=$((encode_ended_at - encode_started_at))
1177
  vlog_msg "CHECKPOINT" "encode_end: $input_file (encode_elapsed=${encode_elapsed_sec}s)"
Bogdan Timofte authored a month ago
1178

            
1179
  if [[ "$ffmpeg_rc" -ne 0 ]]; then
Bogdan Timofte authored a month ago
1180
    local failure_rc=1
1181
    if destination_cannot_accept_file "$input_file" "$out_dir" "$ffmpeg_log"; then
1182
      failure_rc=3
1183
    elif source_error_from_ffmpeg_log "$ffmpeg_log"; then
1184
      failure_rc=2
1185
    fi
1186

            
1187
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
Bogdan Timofte authored a month ago
1188
    file_ended_at="$(date +%s)"
1189
    file_real_elapsed_sec=$((file_ended_at - file_started_at))
1190
    local encode_elapsed_fmt
1191
    local real_elapsed_fmt
1192
    encode_elapsed_fmt="$(format_seconds "$encode_elapsed_sec")"
1193
    real_elapsed_fmt="$(format_seconds "$file_real_elapsed_sec")"
1194

            
Bogdan Timofte authored a month ago
1195
    ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1196
    if [[ "$failure_rc" -eq 2 ]]; then
1197
      INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
1198
    elif [[ "$failure_rc" -eq 3 ]]; then
1199
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1200
    fi
Bogdan Timofte authored a month ago
1201
    if [[ "$VERBOSE" == true ]]; then
Bogdan Timofte authored a month ago
1202
      log_msg "ERROR" "ffmpeg failed: $input_file (real=${file_real_elapsed_sec}s / $real_elapsed_fmt, encode=${encode_elapsed_sec}s / $encode_elapsed_fmt, rc=$ffmpeg_rc)"
Bogdan Timofte authored a month ago
1203
    else
Bogdan Timofte authored a month ago
1204
      log_progress_failed "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "$ffmpeg_log"
Bogdan Timofte authored a month ago
1205
    fi
Bogdan Timofte authored a month ago
1206
    if [[ "$failure_rc" -eq 3 ]]; then
1207
      log_msg "ERROR" "Destination cannot accept more output data: $out_dir"
1208
    fi
1209
    cleanup_repacked_input "$repacked_input_file"
1210
    return "$failure_rc"
Bogdan Timofte authored a month ago
1211
  fi
1212

            
Bogdan Timofte authored a month ago
1213
  TOTAL_VIDEO_TIME_SEC=$((TOTAL_VIDEO_TIME_SEC + encode_elapsed_sec))
1214

            
Bogdan Timofte authored a month ago
1215
  if ! restore_metadata_with_exiftool "$input_file" "$temp_output_file"; then
1216
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1217
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
1218
      cleanup_repacked_input "$repacked_input_file"
1219
      return 4
1220
    fi
Bogdan Timofte authored a month ago
1221
    ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1222
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1223
    cleanup_repacked_input "$repacked_input_file"
1224
    return 3
Bogdan Timofte authored a month ago
1225
  fi
1226

            
Bogdan Timofte authored a month ago
1227
  if ! map_garmin_model_to_standard_tags "$input_file" "$temp_output_file"; then
1228
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1229
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
1230
      cleanup_repacked_input "$repacked_input_file"
1231
      return 4
1232
    fi
Bogdan Timofte authored a month ago
1233
    ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1234
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1235
    cleanup_repacked_input "$repacked_input_file"
1236
    return 3
Bogdan Timofte authored a month ago
1237
  fi
1238

            
Bogdan Timofte authored a month ago
1239
  if ! write_transcode_encoder_metadata "$temp_output_file"; then
1240
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1241
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
1242
      cleanup_repacked_input "$repacked_input_file"
1243
      return 4
1244
    fi
Bogdan Timofte authored a month ago
1245
    ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1246
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1247
    cleanup_repacked_input "$repacked_input_file"
1248
    return 3
Bogdan Timofte authored a month ago
1249
  fi
Bogdan Timofte authored a month ago
1250

            
1251
  if [[ "$MOVE_SOURCE" == true ]]; then
Bogdan Timofte authored a month ago
1252
    if ! validate_transcoded_output "$encode_input_file" "$temp_output_file"; then
1253
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1254
      if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
1255
        cleanup_repacked_input "$repacked_input_file"
1256
        return 4
1257
      fi
Bogdan Timofte authored a month ago
1258
      ERRORS=$((ERRORS + 1))
Bogdan Timofte authored a month ago
1259
      if destination_cannot_accept_file "$input_file" "$out_dir" ""; then
1260
        DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1261
        cleanup_repacked_input "$repacked_input_file"
1262
        return 3
1263
      fi
1264
      # Validation failed but destination is healthy: the source or encoder produced
1265
      # a corrupt/truncated output. Treat as source error so --keep-going can skip it.
1266
      log_msg "ERROR" "Encode produced invalid output (source may be corrupt): $input_file"
Bogdan Timofte authored a month ago
1267
      cleanup_repacked_input "$repacked_input_file"
Bogdan Timofte authored a month ago
1268
      return 2
Bogdan Timofte authored a month ago
1269
    fi
1270
  fi
1271

            
Bogdan Timofte authored a month ago
1272
  touch -r "$input_file" "$temp_output_file" || true
1273

            
1274
  if [[ "$OVERWRITE" == true ]]; then
1275
    if ! mv -f "$temp_output_file" "$output_file"; then
1276
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1277
      ERRORS=$((ERRORS + 1))
1278
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1279
      log_msg "ERROR" "Failed to move completed output into place: $output_file"
1280
      cleanup_repacked_input "$repacked_input_file"
1281
      return 3
1282
    fi
1283
  else
1284
    if ! mv -n "$temp_output_file" "$output_file"; then
1285
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
1286
      ERRORS=$((ERRORS + 1))
1287
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
1288
      log_msg "ERROR" "Failed to move completed output into place: $output_file"
1289
      cleanup_repacked_input "$repacked_input_file"
1290
      return 3
1291
    fi
1292
  fi
Bogdan Timofte authored a month ago
1293

            
1294
  if [[ "$MOVE_SOURCE" == true ]]; then
1295
    if rm -f "$input_file"; then
1296
      vlog_msg "INFO" "Removed source after successful validation: $input_file"
1297
    else
1298
      ERRORS=$((ERRORS + 1))
1299
      log_msg "ERROR" "Failed to remove source after validation: $input_file"
Bogdan Timofte authored a month ago
1300
      cleanup_repacked_input "$repacked_input_file"
Bogdan Timofte authored a month ago
1301
      return 1
1302
    fi
1303
  fi
1304

            
Bogdan Timofte authored a month ago
1305
  file_ended_at="$(date +%s)"
1306
  file_real_elapsed_sec=$((file_ended_at - file_started_at))
1307
  post_elapsed_sec=$((file_real_elapsed_sec - encode_elapsed_sec))
1308
  if [[ "$post_elapsed_sec" -lt 0 ]]; then
1309
    post_elapsed_sec=0
1310
  fi
1311
  TOTAL_FILE_REAL_TIME_SEC=$((TOTAL_FILE_REAL_TIME_SEC + file_real_elapsed_sec))
1312
  vlog_msg "CHECKPOINT" "file_done: $input_file (real=${file_real_elapsed_sec}s encode=${encode_elapsed_sec}s post=${post_elapsed_sec}s)"
1313

            
Bogdan Timofte authored a month ago
1314
  VIDEOS_PROCESSED=$((VIDEOS_PROCESSED + 1))
1315

            
1316
  if [[ "$VERBOSE" == true ]]; then
Bogdan Timofte authored a month ago
1317
    log_msg "INFO" "Transcoded: $output_file (real=${file_real_elapsed_sec}s / $(format_seconds "$file_real_elapsed_sec"), encode=${encode_elapsed_sec}s / $(format_seconds "$encode_elapsed_sec"), post=${post_elapsed_sec}s / $(format_seconds "$post_elapsed_sec"))"
Bogdan Timofte authored a month ago
1318
  else
Bogdan Timofte authored a month ago
1319
    log_progress_done "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec"
Bogdan Timofte authored a month ago
1320
  fi
1321

            
Bogdan Timofte authored a month ago
1322
  cleanup_repacked_input "$repacked_input_file"
1323

            
Bogdan Timofte authored a month ago
1324
  return 0
1325
}
1326

            
1327
copy_one_json() {
1328
  local json_file="$1"
1329
  local rel_json dst_json dst_dir
1330

            
1331
  rel_json="$(rel_path_from_source "$json_file")"
1332
  dst_json="$DEST_DIR/$rel_json"
1333
  dst_dir="$(dirname "$dst_json")"
1334

            
1335
  mkdir -p "$dst_dir"
1336

            
1337
  if [[ -f "$dst_json" && "$OVERWRITE" != true ]]; then
1338
    JSON_SKIPPED=$((JSON_SKIPPED + 1))
1339
    vlog_msg "SKIP" "JSON exists: $dst_json"
1340
    return
1341
  fi
1342

            
1343
  if [[ "$DRY_RUN" == true ]]; then
1344
    JSON_COPIED=$((JSON_COPIED + 1))
1345
    log_msg "DRY-RUN" "Would copy JSON: $json_file -> $dst_json"
1346
    return
1347
  fi
1348

            
1349
  if cp -f "$json_file" "$dst_json"; then
1350
    touch -r "$json_file" "$dst_json" || true
1351
    JSON_COPIED=$((JSON_COPIED + 1))
1352
    vlog_msg "INFO" "Copied JSON: $dst_json"
1353
  else
1354
    ERRORS=$((ERRORS + 1))
1355
    log_msg "ERROR" "Failed to copy JSON: $json_file"
1356
  fi
1357
}
1358

            
1359
copy_sidecars_json() {
1360
  local json_files=()
1361

            
1362
  if [[ -n "$SINGLE_FILE" ]]; then
1363
    local single_abs rel_path candidate
1364
    single_abs="$(to_abs_path "$SINGLE_FILE")"
1365
    rel_path="$(rel_path_from_source "$single_abs")"
1366
    candidate="$SOURCE_DIR/${rel_path%.*}.json"
1367
    if [[ -f "$candidate" ]]; then
1368
      json_files+=("$candidate")
1369
    fi
1370
  else
1371
    while IFS= read -r -d '' jf; do
1372
      json_files+=("$jf")
1373
    done < <(
1374
      if [[ "$RECURSIVE" == true ]]; then
1375
        find "$SOURCE_DIR" -path "$DEST_DIR" -prune -o -type f -iname '*.json' -print0
1376
      else
1377
        find "$SOURCE_DIR" -maxdepth 1 -type f -iname '*.json' -print0
1378
      fi
1379
    )
1380
  fi
1381

            
1382
  if [[ ${#json_files[@]} -eq 0 ]]; then
1383
    vlog_msg "INFO" "No JSON sidecars found to copy"
1384
    return
1385
  fi
1386

            
1387
  local jf
1388
  for jf in "${json_files[@]}"; do
Bogdan Timofte authored a month ago
1389
    if is_apple_noise_file "$jf"; then
1390
      vlog_msg "SKIP" "Ignoring Apple artifact JSON: $jf"
1391
      continue
1392
    fi
Bogdan Timofte authored a month ago
1393
    copy_one_json "$jf"
1394
  done
1395
}
1396

            
1397
write_manifest() {
1398
  local manifest_path="$DEST_DIR/telemetry_manifest.json"
1399

            
1400
  if [[ -f "$manifest_path" && "$OVERWRITE" != true ]]; then
1401
    vlog_msg "SKIP" "Manifest exists: $manifest_path"
1402
    return
1403
  fi
1404

            
1405
  if [[ "$DRY_RUN" == true ]]; then
1406
    log_msg "DRY-RUN" "Would write manifest: $manifest_path"
1407
    return
1408
  fi
1409

            
1410
  mkdir -p "$DEST_DIR"
1411

            
1412
  cat > "$manifest_path" <<EOF
1413
{
1414
  "schema_version": "0.1-draft",
1415
  "purpose": "placeholder contract for future FIT-to-sidecar sync pipeline",
1416
  "fields_target": [
1417
    "power_w",
1418
    "speed_kmh",
1419
    "heart_rate_bpm",
1420
    "cadence_rpm",
1421
    "gps"
1422
  ],
1423
  "sync_methods": [
1424
    "auto_timestamp_plus_offset",
1425
    "manual_offset_ms"
1426
  ],
1427
  "notes": "Current release copies existing JSON sidecars only; FIT parsing is not implemented yet."
1428
}
1429
EOF
1430

            
1431
  vlog_msg "INFO" "Wrote manifest: $manifest_path"
1432
}
1433

            
1434
main() {
1435
  local total_started_at total_ended_at total_elapsed_sec total_elapsed_fmt
1436
  total_started_at="$(date +%s)"
1437

            
Bogdan Timofte authored a month ago
1438
  trap 'handle_interrupt' INT TERM
1439

            
Bogdan Timofte authored a month ago
1440
  parse_args "$@"
1441
  check_tools
1442

            
1443
  # Auto single-file detection: if --source points to a file, treat it as single-file mode
1444
  if [[ -z "$SINGLE_FILE" && "$SOURCE_PROVIDED" == true && -f "$SOURCE_DIR" ]]; then
1445
    SINGLE_FILE="$SOURCE_DIR"
1446
    SOURCE_DIR="$(dirname "$SINGLE_FILE")"
1447
  fi
1448

            
1449
  normalize_source_dir
1450
  normalize_dest_dir
Bogdan Timofte authored a month ago
1451
  normalize_staging_dir
Bogdan Timofte authored a month ago
1452
  collect_extensions
1453

            
1454
  if path_is_within "$DEST_DIR" "$SOURCE_DIR"; then
1455
    die "Destination must not be inside source. Choose a destination outside source: source=$SOURCE_DIR destination=$DEST_DIR"
1456
  fi
1457

            
1458
  detect_encoders
1459
  resolve_encoder
1460

            
1461
  collect_video_files
Bogdan Timofte authored a month ago
1462
  if [[ -z "${VIDEO_FILES[*]-}" ]]; then
Bogdan Timofte authored a month ago
1463
    log_msg "INFO" "No video files found for extensions: $EXTENSIONS_CSV"
1464
  fi
1465

            
1466
  local f
Bogdan Timofte authored a month ago
1467
  for f in ${VIDEO_FILES+"${VIDEO_FILES[@]}"}; do
1468
    if [[ "$STOP_AFTER_CURRENT" == true ]]; then
1469
      log_msg "INFO" "Stop requested; ending before next file"
Bogdan Timofte authored a month ago
1470
      break
1471
    fi
Bogdan Timofte authored a month ago
1472

            
1473
    local process_rc=0
1474
    if process_video_file "$f"; then
1475
      continue
1476
    else
1477
      process_rc=$?
1478
    fi
1479

            
1480
    case "$process_rc" in
1481
      2)
1482
        log_msg "INFO" "Continuing after unreadable/corrupted source file"
1483
        continue
1484
        ;;
1485
      3)
1486
        log_msg "ERROR" "Stopping encoding chain because destination is not writable or is out of space"
1487
        break
1488
        ;;
1489
      4)
1490
        log_msg "INFO" "Stopped by user after current file"
1491
        break
1492
        ;;
1493
      *)
1494
        if [[ "$FAIL_FAST" == true ]]; then
1495
          log_msg "ERROR" "Stopping encoding chain due to ffmpeg failure/interruption"
1496
          break
1497
        fi
1498
        log_msg "ERROR" "Continuing after ffmpeg failure because --keep-going is enabled"
1499
        continue
1500
        ;;
1501
    esac
Bogdan Timofte authored a month ago
1502
  done
1503

            
Bogdan Timofte authored a month ago
1504
  if [[ "$STOP_AFTER_CURRENT" == true ]]; then
1505
    log_msg "INFO" "Skipping sidecar copy and manifest because run was stopped by user"
1506
  elif [[ "$ERRORS" -eq 0 ]]; then
Bogdan Timofte authored a month ago
1507
    copy_sidecars_json
1508
    write_manifest
1509
  else
1510
    log_msg "INFO" "Skipping sidecar copy and manifest because encoding ended with errors"
1511
  fi
1512

            
1513
  total_ended_at="$(date +%s)"
1514
  total_elapsed_sec=$((total_ended_at - total_started_at))
1515
  total_elapsed_fmt="$(format_seconds "$total_elapsed_sec")"
1516

            
1517
  local avg_video_time_sec=0 avg_video_time_fmt="00:00:00"
Bogdan Timofte authored a month ago
1518
  local avg_file_real_time_sec=0 avg_file_real_time_fmt="00:00:00"
1519
  local file_post_total_sec=0 file_post_total_fmt="00:00:00"
1520
  local run_non_file_overhead_sec=0 run_non_file_overhead_fmt="00:00:00"
1521

            
Bogdan Timofte authored a month ago
1522
  if [[ "$VIDEOS_PROCESSED" -gt 0 ]]; then
1523
    avg_video_time_sec=$((TOTAL_VIDEO_TIME_SEC / VIDEOS_PROCESSED))
1524
    avg_video_time_fmt="$(format_seconds "$avg_video_time_sec")"
Bogdan Timofte authored a month ago
1525
    avg_file_real_time_sec=$((TOTAL_FILE_REAL_TIME_SEC / VIDEOS_PROCESSED))
1526
    avg_file_real_time_fmt="$(format_seconds "$avg_file_real_time_sec")"
1527
  fi
1528

            
1529
  file_post_total_sec=$((TOTAL_FILE_REAL_TIME_SEC - TOTAL_VIDEO_TIME_SEC))
1530
  if [[ "$file_post_total_sec" -lt 0 ]]; then
1531
    file_post_total_sec=0
1532
  fi
1533
  file_post_total_fmt="$(format_seconds "$file_post_total_sec")"
1534

            
1535
  run_non_file_overhead_sec=$((total_elapsed_sec - TOTAL_FILE_REAL_TIME_SEC))
1536
  if [[ "$run_non_file_overhead_sec" -lt 0 ]]; then
1537
    run_non_file_overhead_sec=0
Bogdan Timofte authored a month ago
1538
  fi
Bogdan Timofte authored a month ago
1539
  run_non_file_overhead_fmt="$(format_seconds "$run_non_file_overhead_sec")"
Bogdan Timofte authored a month ago
1540

            
Bogdan Timofte authored a month ago
1541
  print_final_report \
1542
    "$total_elapsed_sec" \
1543
    "$total_elapsed_fmt" \
1544
    "$file_post_total_sec" \
1545
    "$file_post_total_fmt" \
1546
    "$run_non_file_overhead_sec" \
1547
    "$run_non_file_overhead_fmt" \
1548
    "$avg_file_real_time_sec" \
1549
    "$avg_file_real_time_fmt" \
1550
    "$avg_video_time_sec" \
1551
    "$avg_video_time_fmt"
Bogdan Timofte authored a month ago
1552

            
1553
  if [[ "$ERRORS" -gt 0 ]]; then
1554
    exit 1
1555
  fi
1556
}
1557

            
1558
main "$@"