diff --git a/demux/demux.go b/demux/demux.go index 7d517bb..4e365ef 100644 --- a/demux/demux.go +++ b/demux/demux.go @@ -9,7 +9,7 @@ import ( "ubvremux/ubv" ) -func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, audioFilename string, partition *ubv.UbvPartition) { +func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, partition *ubv.UbvPartition) { // The input media file; N.B. we do not use a buffered reader for this because we will be seeking heavily ubvFile, err := os.OpenFile(ubvFilename, os.O_RDONLY, 0) @@ -33,26 +33,11 @@ func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, au videoFile = nil } - // Optionally write audio - var audioFile *bufio.Writer - if len(audioFilename) > 0 && partition.AudioTrackCount > 0 { - audioFileRaw, err := os.Create(audioFilename) - if err != nil { - log.Fatal("Error opening audio bitstream output", err) - } - - defer audioFileRaw.Close() - - audioFile = bufio.NewWriter(audioFileRaw) - } else { - audioFile = nil - } - - DemuxSinglePartition(ubvFilename, partition, videoFile, ubvFile, audioFile) + DemuxSinglePartition(ubvFilename, partition, videoFile, ubvFile) } // Extract video and audio data from a given partition of a .ubv file into raw .H264 bitstream and/or raw .AAC bitstream file -func DemuxSinglePartition(ubvFilename string, partition *ubv.UbvPartition, videoFile *bufio.Writer, ubvFile *os.File, audioFile *bufio.Writer) { +func DemuxSinglePartition(ubvFilename string, partition *ubv.UbvPartition, videoFile *bufio.Writer, ubvFile *os.File) { // Allocate a buffer large enough for the largest frame var buffer []byte { @@ -113,34 +98,12 @@ func DemuxSinglePartition(ubvFilename string, partition *ubv.UbvPartition, video log.Fatal("Failed to write output NAL Separator! Only wrote ", bytesWritten, " bytes. Error:", err) } } - - } else if frame.TrackNumber == 1000 && audioFile != nil { - // Audio packet - contains raw AAC bitstream - - // Seek - if _, err := ubvFile.Seek(int64(frame.Offset), io.SeekStart); err != nil { - log.Fatal("Failed to seek to ", frame.Offset, "in ", ubvFilename, err) - } - - // Read - if _, err := io.ReadFull(ubvFile, buffer[0:frame.Size]); err != nil { - log.Fatal("Failed to read ", frame.Size, " bytes at ", frame.Offset, err) - } - - if bytesWritten, err := audioFile.Write(buffer[0:frame.Size]); err != nil { - log.Fatal("Failed to write output audio data! Only wrote ", bytesWritten, ". Error:", err) - } } else { continue } } // Flush all buffered output data - - if audioFile != nil { - audioFile.Flush() - } - if videoFile != nil { videoFile.Flush() } diff --git a/ffmpegutil/ffmpeg.go b/ffmpegutil/ffmpeg.go deleted file mode 100644 index 4fcefb1..0000000 --- a/ffmpegutil/ffmpeg.go +++ /dev/null @@ -1,115 +0,0 @@ -package ffmpegutil - -import ( - "log" - "os" - "os/exec" - "strconv" - "ubvremux/ubv" -) - -func MuxVideoOnly(partition *ubv.UbvPartition, h264File string, mp4File string) { - videoTrack := partition.Tracks[7] - - if videoTrack.FrameCount <= 0 { - log.Println("Video stream contained zero frames! Skipping this output file: ", mp4File) - return - } - - if videoTrack.Rate <= 0 { - log.Println("Invalid guessed Video framerate of ", videoTrack.Rate, " for ", mp4File, ". Setting to 1") - videoTrack.Rate = 1 - } - - cmd := exec.Command(getFfmpegCommand(), - "-i", h264File, - "-c", "copy", - "-r", strconv.Itoa(videoTrack.Rate), - "-timecode", ubv.GenerateTimecode(videoTrack.StartTimecode, videoTrack.Rate), - "-y", - "-loglevel", "warning", - mp4File) - - runFFmpeg(cmd) -} - -func MuxAudioOnly(partition *ubv.UbvPartition, aacFile string, mp4File string) { - cmd := exec.Command(getFfmpegCommand(), "-i", aacFile, "-c", "copy", "-y", "-loglevel", "warning", mp4File) - - runFFmpeg(cmd) -} - -func MuxAudioAndVideo(partition *ubv.UbvPartition, h264File string, aacFile string, mp4File string) { - // If there is no audio file, fall back to the video-only mux operation - if len(aacFile) <= 0 { - MuxVideoOnly(partition, h264File, mp4File) - return - } else if len(h264File) <= 0 { - MuxAudioOnly(partition, aacFile, mp4File) - } - - videoTrack := partition.Tracks[7] - audioTrack := partition.Tracks[1000] - - if videoTrack.FrameCount <= 0 || audioTrack.FrameCount <= 0 { - log.Println("Audio/Video stream contained zero frames! Skipping this output file: ", mp4File) - return - } - - audioDelaySec := float64(videoTrack.StartTimecode.UnixNano()-audioTrack.StartTimecode.UnixNano()) / 1000000000.0 - - if videoTrack.Rate <= 0 { - log.Println("Invalid guessed Video framerate of ", videoTrack.Rate, " for ", mp4File, ". Setting to 1") - videoTrack.Rate = 1 - } - - cmd := exec.Command(getFfmpegCommand(), - "-i", h264File, - "-itsoffset", strconv.FormatFloat(audioDelaySec, 'f', -1, 32), - "-i", aacFile, - "-map", "0:v", - "-map", "1:a", - "-c", "copy", - "-r", strconv.Itoa(videoTrack.Rate), - "-timecode", ubv.GenerateTimecode(videoTrack.StartTimecode, videoTrack.Rate), - "-y", - "-loglevel", "warning", - mp4File) - - runFFmpeg(cmd) -} - -func runFFmpeg(cmd *exec.Cmd) { - log.Println("Running: ", cmd.Args) - - // Pass through stdout and stderr - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Fatal("FFmpeg command failed! Error: ", err) - } -} - -const ( - FFMPEG_LOC_1 = "ffmpeg" - FFMPEG_LOC_2 = "/root/ffmpeg" - FFMPEG_LOC_3 = "/root/ffmpeg-4.3.1-arm64-static/ffmpeg" -) - -// Looks for ubnt_ubvinfo on the path and in the default Protect install location -func getFfmpegCommand() string { - paths := [...]string{FFMPEG_LOC_1, FFMPEG_LOC_2, FFMPEG_LOC_3} - - for _, path := range paths { - if _, err := exec.LookPath(path); err == nil { - return path - } - } - - log.Fatal("FFmpeg not on PATH, nor in any default search locations!") - - // Keep compiler happy (log.Fatal above exits) - return paths[0] -} diff --git a/remux.go b/remux.go index 26cb129..4bf351e 100644 --- a/remux.go +++ b/remux.go @@ -8,7 +8,6 @@ import ( "strings" "time" "ubvremux/demux" - "ubvremux/ffmpegutil" "ubvremux/ubv" ) @@ -20,79 +19,44 @@ var GitCommit string // Parses and validates commandline options and passes them to RemuxCLI func main() { - includeAudioPtr := flag.Bool("with-audio", false, "If true, extract audio") - includeVideoPtr := flag.Bool("with-video", true, "If true, extract video") - forceRatePtr := flag.Int("force-rate", 0, "If non-zero, adds a -r argument to FFmpeg invocations") - outputFolder := flag.String("output-folder", "./", "The path to output remuxed files to. \"SRC-FOLDER\" to put alongside .ubv files") - remuxPtr := flag.Bool("mp4", true, "If true, will create an MP4 as output") + outputFolder := flag.String("output-folder", "./", "The path to output demuxed files to. \"SRC-FOLDER\" to put alongside .ubv files") versionPtr := flag.Bool("version", false, "Display version and quit") flag.Parse() // Perform some argument combo validation if *versionPtr { - println("UBV Remux Tool") - println("Copyright (c) Peter Wright 2020-2021") + println("UBV Demux Tool") + println("Copyright (c) Peter Wright 2020-2023") println("https://github.com/petergeneric/unifi-protect-remux") println("") - // If there's a release version specified, use that. Otherwise print the git revision - if len(ReleaseVersion) > 0 { - println("\tVersion: ", ReleaseVersion) - } else { - println("\tGit commit: ", GitCommit) - } + println("\tVersion: (custom build 1)") + println("\tGit commit: ", GitCommit) os.Exit(0) } else if len(flag.Args()) == 0 { // Terminate immediately if no .ubv files were provided println("Expected at least one .ubv file as input!\n") - flag.Usage() - os.Exit(1) - } else if !*includeAudioPtr && !*includeVideoPtr { - // Fail if extracting neither audio nor video - println("Must enable extraction of at least one of: audio, video!\n") - flag.Usage() os.Exit(1) } - RemuxCLI(flag.Args(), *includeAudioPtr, *includeVideoPtr, *forceRatePtr, *remuxPtr, *outputFolder) + RemuxCLI(flag.Args(), *outputFolder) } -// Takes parsed commandline args and performs the remux tasks across the set of input files -func RemuxCLI(files []string, extractAudio bool, extractVideo bool, forceRate int, createMP4 bool, outputFolder string) { +// RemuxCLI Takes parsed commandline args and performs the remux tasks across the set of input files +func RemuxCLI(files []string, outputFolder string) { for _, ubvFile := range files { log.Println("Analysing ", ubvFile) - info := ubv.Analyse(ubvFile, extractAudio) - - log.Printf("\n\nAnalysis complete!\n") - if len(info.Partitions) > 0 { - log.Printf("First Partition:") - log.Printf("\tTracks: %d", len(info.Partitions[0].Tracks)) - log.Printf("\tFrames: %d", len(info.Partitions[0].Frames)) - log.Printf("\tStart Timecode: %s", info.Partitions[0].Tracks[7].StartTimecode.Format(time.RFC3339)) - } + info := ubv.Analyse(ubvFile, false) - log.Printf("\n\nExtracting %d partitions", len(info.Partitions)) + log.Printf("\n\nAnalysis complete!") + log.Printf("Extracting %d partitions...\n", len(info.Partitions)) - // Optionally apply the user's forced framerate - if forceRate > 0 { - log.Println("\nFramerate forced by user instruction: using ", forceRate, " fps") - for _, partition := range info.Partitions { - for _, track := range partition.Tracks { - if track.IsVideo { - track.Rate = forceRate - } - } - } - } - - for _, partition := range info.Partitions { + for i, partition := range info.Partitions { var videoFile string - var audioFile string - var mp4 string { outputFolder := strings.TrimSuffix(outputFolder, "/") @@ -111,40 +75,16 @@ func RemuxCLI(files []string, extractAudio bool, extractVideo bool, forceRate in basename := outputFolder + "/" + baseFilename + "_" + strings.ReplaceAll(getStartTimecode(partition).Format(time.RFC3339), ":", ".") - if extractVideo && partition.VideoTrackCount > 0 { - videoFile = basename + ".h264" - } - - if extractAudio && partition.AudioTrackCount > 0 { - audioFile = basename + ".aac" - } - - if createMP4 { - mp4 = basename + ".mp4" - } + videoFile = basename + ".h264" } - demux.DemuxSinglePartitionToNewFiles(ubvFile, videoFile, audioFile, partition) - - if createMP4 { - log.Println("\nWriting MP4 ", mp4, "...") + log.Printf("Partition %d:", i) + log.Printf("\tTracks: %d", len(partition.Tracks)) + log.Printf("\tFrames: %d", len(partition.Frames)) + log.Printf("\tStart Timecode: %s", partition.Tracks[7].StartTimecode.Format(time.RFC3339)) + log.Printf("\tOutput File: %s\n", videoFile) - // Spawn FFmpeg to remux - // TODO: could we generate an MP4 directly? Would require some analysis of the input bitstreams to build MOOV - ffmpegutil.MuxAudioAndVideo(partition, videoFile, audioFile, mp4) - - // Delete - if len(videoFile) > 0 { - if err := os.Remove(videoFile); err != nil { - log.Println("Warning: could not delete ", videoFile+": ", err) - } - } - if len(audioFile) > 0 { - if err := os.Remove(audioFile); err != nil { - log.Println("Warning: could not delete ", audioFile+": ", err) - } - } - } + demux.DemuxSinglePartitionToNewFiles(ubvFile, videoFile, partition) } } }