Skip to content

Commit

Permalink
Refactor logic to allow video track number selection to make it easie…
Browse files Browse the repository at this point in the history
…r to extract Video Track 1003 from .ubv files
  • Loading branch information
petergeneric committed Aug 6, 2024
1 parent d900315 commit 1251bcb
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 64 deletions.
10 changes: 5 additions & 5 deletions demux/demux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"ubvremux/ubv"
)

func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, audioFilename string, partition *ubv.UbvPartition) {
func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, videoTrackNum int, audioFilename 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)
Expand Down Expand Up @@ -48,11 +48,11 @@ func DemuxSinglePartitionToNewFiles(ubvFilename string, videoFilename string, au
audioFile = nil
}

DemuxSinglePartition(ubvFilename, partition, videoFile, ubvFile, audioFile)
DemuxSinglePartition(ubvFilename, partition, videoFile, videoTrackNum, ubvFile, audioFile)
}

// 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, videoTrackNum int, ubvFile *os.File, audioFile *bufio.Writer) {
// Allocate a buffer large enough for the largest frame
var buffer []byte
{
Expand All @@ -75,7 +75,7 @@ func DemuxSinglePartition(ubvFilename string, partition *ubv.UbvPartition, video
}

for _, frame := range partition.Frames {
if frame.TrackNumber == 7 && videoFile != nil {
if frame.TrackNumber == videoTrackNum && videoFile != nil {
// Video packet - contains one or more length-prefixed NALs
frameDataRead := 0

Expand Down Expand Up @@ -114,7 +114,7 @@ func DemuxSinglePartition(ubvFilename string, partition *ubv.UbvPartition, video
}
}

} else if frame.TrackNumber == 1000 && audioFile != nil {
} else if frame.TrackNumber == ubv.TrackAudio && audioFile != nil {
// Audio packet - contains raw AAC bitstream

// Seek
Expand Down
48 changes: 24 additions & 24 deletions ffmpegutil/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"ubvremux/ubv"
)

func MuxVideoOnly(partition *ubv.UbvPartition, h264File string, mp4File string) {
videoTrack := partition.Tracks[7]
func MuxVideoOnly(partition *ubv.UbvPartition, h264File string, videoTrackNum int, mp4File string) {
videoTrack := partition.Tracks[videoTrackNum]

if videoTrack.FrameCount <= 0 {
log.Println("Video stream contained zero frames! Skipping this output file: ", mp4File)
Expand All @@ -21,14 +21,14 @@ func MuxVideoOnly(partition *ubv.UbvPartition, h264File string, mp4File string)
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)
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)
}
Expand All @@ -39,17 +39,17 @@ func MuxAudioOnly(partition *ubv.UbvPartition, aacFile string, mp4File string) {
runFFmpeg(cmd)
}

func MuxAudioAndVideo(partition *ubv.UbvPartition, h264File string, aacFile string, mp4File string) {
func MuxAudioAndVideo(partition *ubv.UbvPartition, h264File string, videoTrackNum int, 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)
MuxVideoOnly(partition, h264File, videoTrackNum, mp4File)
return
} else if len(h264File) <= 0 {
MuxAudioOnly(partition, aacFile, mp4File)
}

videoTrack := partition.Tracks[7]
audioTrack := partition.Tracks[1000]
videoTrack := partition.Tracks[videoTrackNum]
audioTrack := partition.Tracks[ubv.TrackAudio]

if videoTrack.FrameCount <= 0 || audioTrack.FrameCount <= 0 {
log.Println("Audio/Video stream contained zero frames! Skipping this output file: ", mp4File)
Expand All @@ -63,17 +63,17 @@ func MuxAudioAndVideo(partition *ubv.UbvPartition, h264File string, aacFile stri
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),
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",
"-y",
"-loglevel", "warning",
mp4File)

runFFmpeg(cmd)
Expand Down
29 changes: 18 additions & 11 deletions remux.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ func main() {
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")
versionPtr := flag.Bool("version", false, "Display version and quit")
videoTrackNumPtr := flag.Int("video-track", ubv.TrackVideo, "Video track number to extract (supported: 7, 1003)")

flag.Parse()

// Perform some argument combo validation
if *versionPtr {
println("UBV Remux Tool")
println("Copyright (c) Peter Wright 2020-2021")
println("Copyright (c) Peter Wright 2020-2024")
println("https://github.com/petergeneric/unifi-protect-remux")
println("")

Expand All @@ -58,21 +59,27 @@ func main() {
os.Exit(1)
}

RemuxCLI(flag.Args(), *includeAudioPtr, *includeVideoPtr, *forceRatePtr, *remuxPtr, *outputFolder)
RemuxCLI(flag.Args(), *includeAudioPtr, *includeVideoPtr, *videoTrackNumPtr, *forceRatePtr, *remuxPtr, *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) {
func RemuxCLI(files []string, extractAudio bool, extractVideo bool, videoTrackNum int, forceRate int, createMP4 bool, outputFolder string) {
for _, ubvFile := range files {
log.Println("Analysing ", ubvFile)
info := ubv.Analyse(ubvFile, extractAudio)
info := ubv.Analyse(ubvFile, extractAudio, videoTrackNum)

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))

for _, track := range info.Partitions[0].Tracks {
if track.IsVideo || info.Partitions[0].VideoTrackCount == 0 {
log.Printf("\tStart Timecode: %s", track.StartTimecode.Format(time.RFC3339))
break
}
}
}

log.Printf("\n\nExtracting %d partitions", len(info.Partitions))
Expand Down Expand Up @@ -109,7 +116,7 @@ func RemuxCLI(files []string, extractAudio bool, extractVideo bool, forceRate in
baseFilename = baseFilename[0:strings.LastIndex(baseFilename, "_")]
}

basename := outputFolder + "/" + baseFilename + "_" + strings.ReplaceAll(getStartTimecode(partition).Format(time.RFC3339), ":", ".")
basename := outputFolder + "/" + baseFilename + "_" + strings.ReplaceAll(getStartTimecode(partition, videoTrackNum).Format(time.RFC3339), ":", ".")

if extractVideo && partition.VideoTrackCount > 0 {
videoFile = basename + ".h264"
Expand All @@ -124,14 +131,14 @@ func RemuxCLI(files []string, extractAudio bool, extractVideo bool, forceRate in
}
}

demux.DemuxSinglePartitionToNewFiles(ubvFile, videoFile, audioFile, partition)
// Demux .ubv into .h264 (and optionally .aac) atomic streams
demux.DemuxSinglePartitionToNewFiles(ubvFile, videoFile, videoTrackNum, audioFile, partition)

if createMP4 {
log.Println("\nWriting MP4 ", mp4, "...")

// 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)
ffmpegutil.MuxAudioAndVideo(partition, videoFile, videoTrackNum, audioFile, mp4)

// Delete
if len(videoFile) > 0 {
Expand All @@ -149,9 +156,9 @@ func RemuxCLI(files []string, extractAudio bool, extractVideo bool, forceRate in
}
}

func getStartTimecode(partition *ubv.UbvPartition) time.Time {
func getStartTimecode(partition *ubv.UbvPartition, videoTrackNum int) time.Time {
for _, track := range partition.Tracks {
if partition.VideoTrackCount == 0 || track.IsVideo {
if partition.VideoTrackCount == 0 || (track.IsVideo && track.TrackNumber == videoTrackNum) {
return track.StartTimecode
}
}
Expand Down
20 changes: 10 additions & 10 deletions ubv/ubvfile.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package ubv

import (
"fmt"
"log"
"strconv"
"time"
"fmt"
)

const (
Expand All @@ -29,7 +29,7 @@ const (
)

type UbvFrame struct {
//The track ID; only two observed values are 7 for the main video, and 1000 for main audio (AAC)
//The track ID; observed values are 7 for the main video, 1003 for some hevc alt video, and 1000 for main audio (AAC)
TrackNumber int
Offset int
Size int
Expand All @@ -52,7 +52,7 @@ type UbvTrack struct {

// For Video tracks, holds a window of rate estimations per-frame
// This is populated and used to determine Rate
RateProbeWindow [32]int
RateProbeWindow [32]int
RateProbeLastFrameWC int64

// The date+time of the last frame in this partition
Expand Down Expand Up @@ -111,7 +111,7 @@ func extractTimecodeAndRate(fields []string, line string, track *UbvTrack) {
} else if track.Rate == 0 && track.IsVideo {
if track.FrameCount < len(track.RateProbeWindow) {
// Compute rate based on current+last frame time
track.RateProbeWindow[track.FrameCount] = int(tbc / ((wc - track.RateProbeLastFrameWC)))
track.RateProbeWindow[track.FrameCount] = int(tbc / (wc - track.RateProbeLastFrameWC))
track.RateProbeLastFrameWC = wc
} else {
// Find the most frequent rate in the probe window
Expand All @@ -124,10 +124,10 @@ func extractTimecodeAndRate(fields []string, line string, track *UbvTrack) {

log.Println("Video Rate Probe: File appears to be", track.Rate, "fps. Use -force-rate if incorrect.")
} else if rate == 0 {
log.Println("Video Rate Probe: WARNING probed rate was",rate, "fps. Assuming timelapse file and using 1fps")
log.Println("Video Rate Probe: WARNING probed rate was", rate, "fps. Assuming timelapse file and using 1fps")
track.Rate = 1
} else {
log.Fatal("Video Rate Probe: WARNING probed rate was", rate , "fps. Assuming invalid. Please use -force-rate ## (e.g. -force-rate 25) based on your camera's frame rate")
log.Fatal("Video Rate Probe: WARNING probed rate was", rate, "fps. Assuming invalid. Please use -force-rate ## (e.g. -force-rate 25) based on your camera's frame rate")
panic("Could not determine sensible video framerate based on data stored in .ubv")
}
}
Expand Down Expand Up @@ -159,18 +159,18 @@ func guessVideoRate(durations [32]int) int {
* Generates a timecode string from a StartTimecode object and framerate.
* The timecode is set as the wall clock time (so a clip starting at 03:45 pm and 13 seconds will have a timestamp of 03:45:13)
* Additionally, the nanosecond time value is rounded to the nearest frame index based on the framerate,
* so a 13.50000 second time is frame 16 on a 30 fps clip (frames are indexed from 1 onwards).
* so a 13.50000 second time is frame 16 on a 30 fps clip (frames are indexed from 1 onwards).
* So the clip will have a full timestamp of 03:34:13.16
*
* @param startTimecode The StartTimecode object to generate a timecode string from
* @param framerate The framerate of the video
* @return The timecode string
*/
func GenerateTimecode(startTimecode time.Time, framerate int) string {
func GenerateTimecode(startTimecode time.Time, framerate int) string {

var timecode string
// calculate timecode ( HH:MM:SS.FF ) from seconds and nanoseconds for frame part
timecode = startTimecode.Format("15:04:05") + "." + fmt.Sprintf("%02.0f", ((float32(startTimecode.Nanosecond()) / float32(1000000000.0) * float32(framerate)) + 1) )
timecode = startTimecode.Format("15:04:05") + "." + fmt.Sprintf("%02.0f", ((float32(startTimecode.Nanosecond())/float32(1000000000.0)*float32(framerate))+1))
// log.Println("Timecode: ", timecode)
// log.Printf("Date/Time: %s", videoTrack.StartTimecode)
return timecode
Expand Down
19 changes: 8 additions & 11 deletions ubv/ubvinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ const ubntUbvInfoPath2 = "/usr/share/unifi-protect/app/node_modules/.bin/ubnt_ub

const TrackAudio = 1000
const TrackVideo = 7
const TrackUnknown = 1003
const TrackVideoHevcUnknown = 1003

// Analyse a .ubv file (picking between ubnt_ubvinfo or a pre-prepared .txt file as appropriate)
func Analyse(ubvFile string, includeAudio bool) UbvFile {
func Analyse(ubvFile string, includeAudio bool, videoTrackNum int) UbvFile {
cachedUbvInfoFile := ubvFile + ".txt"

if _, err := os.Stat(cachedUbvInfoFile); err != nil {
// No existing analysis, must run ubnt_ubvinfo
return runUbvInfo(ubvFile, includeAudio)
return runUbvInfo(ubvFile, includeAudio, videoTrackNum)
} else {
// Analysis file exists, read that instead of re-running ubnt_ubvinfo
return parseUbvInfoFile(ubvFile, cachedUbvInfoFile)
Expand All @@ -50,13 +50,13 @@ func getUbvInfoCommand() string {
return paths[0]
}

func runUbvInfo(ubvFile string, includeAudio bool) UbvFile {
func runUbvInfo(ubvFile string, includeAudio bool, videoTrackNum int) UbvFile {
ubntUbvinfo := getUbvInfoCommand()
cmd := exec.Command(ubntUbvinfo, "-P", "-f", ubvFile)

// Optimise video-only extraction to speed ubnt_ubvinfo part of process
if !includeAudio {
cmd = exec.Command(ubntUbvinfo, "-t", "7", "-P", "-f", ubvFile)
cmd = exec.Command(ubntUbvinfo, "-t", strconv.Itoa(videoTrackNum), "-P", "-f", ubvFile)
}

// Parse stdout in the background
Expand Down Expand Up @@ -152,14 +152,11 @@ func parseUbvInfo(ubvFile string, scanner *bufio.Scanner) UbvFile {
log.Fatal("Error parsing frame size!", err)
}

// Ignore Track 1003 (unknown purpose, first discovered in #44)
if frame.TrackNumber == TrackUnknown {
continue
}
isRecognisedVideoTrack := frame.TrackNumber == TrackVideo || frame.TrackNumber == TrackVideoHevcUnknown

// Bail if we encounter an unexpected track number
// We could silently ignore it, but it seems more useful to know about new cases
if frame.TrackNumber != TrackVideo && frame.TrackNumber != TrackAudio {
if !isRecognisedVideoTrack && frame.TrackNumber != TrackAudio {
log.Fatal("Encountered unrecognisdd track number, please report this. Track Number: ", frame.TrackNumber)
}

Expand All @@ -168,7 +165,7 @@ func parseUbvInfo(ubvFile string, scanner *bufio.Scanner) UbvFile {
if !ok {
track = &UbvTrack{
// TODO should really test field FIELD_TRACK_TYPE holds (A or V)
IsVideo: frame.TrackNumber == TrackVideo,
IsVideo: isRecognisedVideoTrack,
TrackNumber: frame.TrackNumber,
FrameCount: 0,
}
Expand Down
14 changes: 11 additions & 3 deletions ubvfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"ubvremux/ubv"
)

func TestGenerateTimecode(t *testing.T) {
func TestGenerateTimecode(t *testing.T) {
timecode := ubv.GenerateTimecode(time.Date(2023, time.Month(5), 16, 11, 58, 26, 500000000, time.UTC), 30)
log.Printf("Timecode Generated")
if timecode != "11:58:26.16" {
Expand All @@ -18,7 +18,7 @@ func TestGenerateTimecode(t *testing.T) {
func TestCopyFrames(t *testing.T) {
ubvFile := "samples/FCECDA1F0A63_0_rotating_1597425468956.ubv"

info := ubv.Analyse(ubvFile, true)
info := ubv.Analyse(ubvFile, true, ubv.TrackVideo)

log.Printf("\n\n*** Parsing complete! ***\n\n")
log.Printf("Number of partitions: %d", len(info.Partitions))
Expand All @@ -27,7 +27,15 @@ func TestCopyFrames(t *testing.T) {
log.Printf("Partition %d", info.Partitions[0].Index)
log.Printf("Tracks: %d", len(info.Partitions[0].Tracks))
log.Printf("Frames: %d", len(info.Partitions[0].Frames))
log.Printf("Start Timecode: %s", info.Partitions[0].Tracks[7].StartTimecode.Format(time.RFC3339))

mainVideoTrack := info.Partitions[0].Tracks[ubv.TrackVideo]
altVideoTrack := info.Partitions[0].Tracks[ubv.TrackVideoHevcUnknown]
if mainVideoTrack != nil {
log.Printf("Start Timecode: %s", mainVideoTrack.StartTimecode.Format(time.RFC3339))
}
if altVideoTrack != nil {
log.Printf("Start Timecode (Alt Video): %s", altVideoTrack.StartTimecode.Format(time.RFC3339))
}
}

//
Expand Down

0 comments on commit 1251bcb

Please sign in to comment.