An art project of mine; showing 30 second video clips of calm places & moments I've enjoyed being in. stream.place/byjp.me
video streaming art

Ensure audio, and validate output

This prevents the audio lag I was seeing on stream place

+54 -3
+54 -3
main.go
··· 32 32 33 33 func check(err error) { 34 34 if err != nil { 35 - panic(err) 36 35 log.Fatal(err) 37 36 } 38 37 } ··· 51 50 start := (videoLength - length) / 2 52 51 53 52 outPath, err := multiEncode(in, start, created) 54 - check(err) 53 + if err != nil { 54 + log.Println(err) 55 + continue 56 + } 55 57 56 58 fmt.Println("Encoded", outPath) 57 59 } ··· 94 96 name := strings.TrimSuffix(base, ext) 95 97 lenMinus3 := len(name) - 3 96 98 noAudio := name[lenMinus3:] == ".na" 97 - audioFilter := "[0:a]adelay=1000|1000,afade=in:st=1:d=1,afade=out:st=30:d=1,apad=whole_dur=32,asetpts=PTS-STARTPTS[outa]" 99 + audioFilter := "anullsrc=r=44100:cl=stereo:d=32[silence];[0:a]adelay=1000|1000,afade=in:st=1:d=1,afade=out:st=30:d=1,apad=whole_dur=32[delayed];[silence][delayed]amix=inputs=2:duration=first:normalize=0,asetpts=PTS-STARTPTS[outa]" 98 100 if noAudio { 99 101 name = name[0:lenMinus3] 100 102 audioFilter = "anullsrc=r=44100:cl=stereo:d=32,asetpts=PTS-STARTPTS[outa]" ··· 138 140 return "", fmt.Errorf("ffmpeg error %s: %w", strings.Join(cmd.Args, " "), cmdErr) 139 141 } 140 142 143 + // Validate output before moving source to done 144 + if err := validateOutput(outPath); err != nil { 145 + _ = os.Remove(outPath) 146 + return "", fmt.Errorf("validation failed for %s: %w", path, err) 147 + } 148 + 141 149 movePath := filepath.Join(filepath.Dir(path), "done", filepath.Base(path)) 142 150 _ = os.Rename(path, movePath) 143 151 144 152 return outPath, nil 153 + } 154 + 155 + const ( 156 + expectedVideoFrames = 800 157 + expectedAACFrames = 1380 // 44100 Hz × 32 seconds ÷ 1024 samples/frame ≈ 1378, rounded up 158 + ) 159 + 160 + func validateOutput(path string) error { 161 + // Count video frames 162 + videoCmd := exec.Command("/opt/homebrew/bin/ffprobe", 163 + "-v", "error", "-select_streams", "v:0", "-count_frames", 164 + "-show_entries", "stream=nb_read_frames", "-of", "csv=p=0", path) 165 + videoOut, err := videoCmd.Output() 166 + if err != nil { 167 + return fmt.Errorf("failed to count video frames: %w", err) 168 + } 169 + videoFrames, err := strconv.Atoi(strings.Split(strings.TrimSpace(string(videoOut)), "\n")[0]) 170 + if err != nil { 171 + return fmt.Errorf("failed to parse video frame count: %w", err) 172 + } 173 + 174 + // Count AAC frames 175 + audioCmd := exec.Command("/opt/homebrew/bin/ffprobe", 176 + "-v", "error", "-select_streams", "a:0", "-count_frames", 177 + "-show_entries", "stream=nb_read_frames", "-of", "csv=p=0", path) 178 + audioOut, err := audioCmd.Output() 179 + if err != nil { 180 + return fmt.Errorf("failed to count audio frames: %w", err) 181 + } 182 + aacFrames, err := strconv.Atoi(strings.Split(strings.TrimSpace(string(audioOut)), "\n")[0]) 183 + if err != nil { 184 + return fmt.Errorf("failed to parse audio frame count: %w", err) 185 + } 186 + 187 + // Validate 188 + if videoFrames != expectedVideoFrames { 189 + return fmt.Errorf("invalid video frame count: got %d, expected %d", videoFrames, expectedVideoFrames) 190 + } 191 + if aacFrames < expectedAACFrames-5 || aacFrames > expectedAACFrames+5 { 192 + return fmt.Errorf("invalid audio frame count: got %d, expected ~%d", aacFrames, expectedAACFrames) 193 + } 194 + 195 + return nil 145 196 } 146 197 147 198 func listVideos() (videos []string, err error) {