@@ -59,6 +59,7 @@ public class GLRecordingPipeline {
5959 private Surface pendingPreviewToApply = null ;
6060 private Runnable pendingPreviewApplyRunnable = null ;
6161 private final Object previewApplyLock = new Object ();
62+ private final Object timestampLock = new Object (); // Synchronization lock for timestamp fields
6263 private final String orientation ;
6364 private final int sensorOrientation ;
6465
@@ -83,50 +84,51 @@ public interface SegmentCallback {
8384 /**
8485 * Gets a synchronized timestamp for audio frames based on the video timeline.
8586 * This ensures audio and video timestamps are properly aligned.
87+ * Accounts for pause durations to maintain sync during camera switch.
8688 */
8789 private long getSynchronizedAudioTimestamp () {
88- synchronized (timestampLock ) {
89- if (recordingStartTimeNanos == -1 ) {
90- // First call - initialize the recording start time
91- recordingStartTimeNanos = System .nanoTime ();
92- return 0 ; // First audio frame starts at 0
93- }
94-
95- // Calculate elapsed time since recording started
96- long elapsedNanos = System .nanoTime () - recordingStartTimeNanos ;
97- return elapsedNanos / 1000L ; // Convert to microseconds
98- }
90+ // Audio timestamp is calculated based on sample count, which naturally
91+ // excludes pause periods (no samples recorded during pause)
92+ // So we don't need to do anything special here
93+ return -1 ; // Indicates: use the PTS calculated in audio thread
9994 }
10095
10196 private void initializeVideoTimestamp (long cameraTimestampNanos ) {
102- synchronized (timestampLock ) {
103- if (firstVideoTimestampNanos == -1 ) {
104- firstVideoTimestampNanos = cameraTimestampNanos ;
105- if (recordingStartTimeNanos == -1 ) {
106- recordingStartTimeNanos = System .nanoTime ();
107- }
108- // Log.d(TAG, "Video timestamp initialized: camera=" + cameraTimestampNanos +
109- // ", recording_start=" + recordingStartTimeNanos);
110- }
111- }
97+ // Not needed anymore - PTS is calculated from frame counter
98+ // Keeping this method for compatibility with existing code
11299 }
113100
114101 /**
115102 * Gets a synchronized timestamp for video frames that aligns with audio.
116- * This converts camera timestamps to recording timeline.
103+ * Returns -1 during pause to skip frames, preserving the existing timestamp logic.
104+ *
105+ * CRITICAL FIX: Use SYSTEM CLOCK elapsed time (same as audio thread),
106+ * not camera timestamps. This ensures audio and video use the SAME timing base.
117107 */
118108 public long getSynchronizedVideoTimestamp (long cameraTimestampNanos ) {
119- synchronized (timestampLock ) {
120- if (firstVideoTimestampNanos == -1 || recordingStartTimeNanos == -1 ) {
121- // Initialize if not done yet
122- initializeVideoTimestamp (cameraTimestampNanos );
123- return 0 ; // First video frame starts at 0
109+ // If paused, return -1 to signal frame skip in renderToEncoder
110+ if (isPaused ) {
111+ return -1 ; // Skip this frame
112+ }
113+
114+ // Use System.nanoTime() reference (same as audio thread for sync)
115+ // Initialize recording start time on first frame
116+ if (recordingStartSystemTimeNanos == -1 ) {
117+ synchronized (timestampLock ) {
118+ if (recordingStartSystemTimeNanos == -1 ) {
119+ recordingStartSystemTimeNanos = System .nanoTime ();
120+ Log .d (TAG , "[VIDEO_TIMESTAMP] Recording system time reference initialized: " +
121+ recordingStartSystemTimeNanos + " nanos" );
122+ }
124123 }
125-
126- // Calculate offset from first video frame
127- long videoOffsetNanos = cameraTimestampNanos - firstVideoTimestampNanos ;
128- return videoOffsetNanos / 1000L ; // Convert to microseconds
129124 }
125+
126+ // Calculate relative timestamp from system time reference (same as audio!)
127+ // This ensures both audio and video use the exact same timing base
128+ long elapsedNanos = System .nanoTime () - recordingStartSystemTimeNanos - totalPauseDurationNanos ;
129+ long ptsUs = Math .max (0 , elapsedNanos / 1000L ); // Convert to microseconds, ensure non-negative
130+
131+ return ptsUs ;
130132 }
131133
132134 private long maxFileSizeBytes = Long .MAX_VALUE ;
@@ -188,9 +190,16 @@ public long getSynchronizedVideoTimestamp(long cameraTimestampNanos) {
188190 private Float locationLongitude = null ;
189191
190192 // Timestamp synchronization fields
191- private long recordingStartTimeNanos = -1 ;
192- private long firstVideoTimestampNanos = -1 ;
193- private final Object timestampLock = new Object ();
193+ private long recordingStartTimeNanos = -1 ; // System.nanoTime() when recording starts (VIDEO reference)
194+ private long recordingStartSystemTimeNanos = -1 ; // System.nanoTime() for audio thread reference (same as video!)
195+ private volatile boolean isPaused = false ; // Track pause state for audio thread
196+
197+ // Pause/resume tracking fields
198+ private long pauseStartTimeNanos = -1 ; // System.nanoTime() when pause starts
199+ private long totalPauseDurationNanos = 0 ; // Accumulated pause duration
200+ private long lastVideoPtsBeforePauseUs = 0 ; // Last video PTS before pause
201+ private long lastAudioPtsBeforePauseUs = 0 ; // Last audio PTS before pause
202+ private boolean isCameraSwitchPause = false ; // Flag to indicate if pause is due to camera switch
194203
195204 // scheduler-----------
196205 // Update watermark on a low-frequency handler to avoid per-frame overhead and
@@ -454,6 +463,11 @@ public void startRecording() {
454463 com .fadcam .Log .d (TAG , "Starting recording pipeline" );
455464 } catch (Throwable ignore ) {
456465 }
466+
467+ // Reset timestamp tracking at recording start
468+ recordingStartTimeNanos = -1 ; // Will be initialized on first VIDEO frame
469+ recordingStartSystemTimeNanos = System .nanoTime (); // Initialize for AUDIO thread reference NOW
470+ Log .i (TAG , "[RECORDING_START] Audio/Video timing reference initialized: " + recordingStartSystemTimeNanos );
457471
458472 // Make sure we have a valid renderer and surfaces
459473 if (glRenderer == null || encoderInputSurface == null ) {
@@ -1107,7 +1121,7 @@ private void setupMuxer() throws IOException {
11071121 if (segmentNumber == 1 ) {
11081122 synchronized (timestampLock ) {
11091123 recordingStartTimeNanos = -1 ;
1110- firstVideoTimestampNanos = -1 ;
1124+ recordingStartSystemTimeNanos = -1 ;
11111125 }
11121126 }
11131127
@@ -1331,9 +1345,11 @@ private void drainEncoder() {
13311345
13321346 videoSamplesWritten ++;
13331347 lastVideoPts = bufferInfo .presentationTimeUs ;
1334- if (videoSamplesWritten % 60 == 0 ) {
1335- Log .d (TAG , String .format ("VIDEO: #%d, %.1fs, %db" ,
1336- videoSamplesWritten , bufferInfo .presentationTimeUs / 1000000.0 , bufferInfo .size ));
1348+ // Log every frame for debugging (can reduce frequency later)
1349+ if (videoSamplesWritten % 30 == 0 ) {
1350+ Log .i (TAG , String .format ("[VIDEO_WRITE] Sample #%d, PTS=%.3fs (%dus), size=%db, keyframe=%s" ,
1351+ videoSamplesWritten , bufferInfo .presentationTimeUs / 1000000.0 ,
1352+ bufferInfo .presentationTimeUs , bufferInfo .size , isKeyframe ));
13371353 }
13381354
13391355 segmentBytesWritten += bufferInfo .size ;
@@ -1740,7 +1756,7 @@ public void stopRecording() {
17401756 isRecording = false ;
17411757 // Clear retry/time bases
17421758 recordingStartTimeNanos = -1 ;
1743- firstVideoTimestampNanos = -1 ;
1759+ recordingStartSystemTimeNanos = -1 ;
17441760 }
17451761
17461762 /**
@@ -1771,17 +1787,36 @@ public Surface getCameraInputSurface() {
17711787 }
17721788
17731789 /**
1774- * Pauses the recording pipeline (no-op, for API compatibility).
1790+ * Pauses the recording pipeline with proper timestamp tracking.
1791+ * During pause, we record the last known PTS values to maintain
1792+ * timeline continuity when resuming (especially important for camera switch).
17751793 */
17761794 public void pauseRecording () {
17771795 if (!isRecording || isStopped ) {
17781796 Log .w (TAG , "Cannot pause recording - recording is not active" );
17791797 return ;
17801798 }
17811799
1782- Log .d (TAG , "Pausing recording" );
1800+ Log .i (TAG , "========== PAUSE RECORDING ===========" );
1801+
1802+ synchronized (timestampLock ) {
1803+ // Record pause start time for duration tracking
1804+ pauseStartTimeNanos = System .nanoTime ();
1805+ isPaused = true ;
1806+
1807+ // Save last known PTS values before pause
1808+ lastVideoPtsBeforePauseUs = lastVideoPts ;
1809+ lastAudioPtsBeforePauseUs = lastAudioPts ;
1810+
1811+ Log .i (TAG , "[PAUSE] Pause started at " + (pauseStartTimeNanos / 1_000_000L ) + "ms" );
1812+ Log .i (TAG , "[PAUSE] Last video PTS: " + lastVideoPtsBeforePauseUs + "us (" + (lastVideoPtsBeforePauseUs / 1000.0 ) + "ms)" );
1813+ Log .i (TAG , "[PAUSE] Last audio PTS: " + lastAudioPtsBeforePauseUs + "us (" + (lastAudioPtsBeforePauseUs / 1000.0 ) + "ms)" );
1814+ Log .i (TAG , "[PAUSE] Total pause duration so far: " + (totalPauseDurationNanos / 1_000_000L ) + "ms" );
1815+ Log .i (TAG , "[PAUSE] Video samples written: " + videoSamplesWritten );
1816+ Log .i (TAG , "[PAUSE] Audio samples written: " + audioSamplesWritten );
1817+ }
17831818
1784- // Simply set the recording flag to false to stop encoding new frames
1819+ // Set recording flag to false to stop encoding new frames
17851820 isRecording = false ;
17861821
17871822 // Pause audio recording if enabled
@@ -1798,6 +1833,20 @@ public void pauseRecording() {
17981833
17991834 Log .d (TAG , "Recording paused successfully" );
18001835 }
1836+
1837+ /**
1838+ * Prepares for a camera switch by setting the appropriate flags.
1839+ * Call this before pauseRecording() when switching cameras.
1840+ */
1841+ public void prepareCameraSwitch () {
1842+ synchronized (timestampLock ) {
1843+ isCameraSwitchPause = true ;
1844+ Log .i (TAG , "========== PREPARE CAMERA SWITCH ===========" );
1845+ Log .i (TAG , "[CAMERA_SWITCH] Prepared - timestamps will be adjusted on resume" );
1846+ Log .i (TAG , "[CAMERA_SWITCH] Current video PTS: " + lastVideoPts + "us" );
1847+ Log .i (TAG , "[CAMERA_SWITCH] Current audio PTS: " + lastAudioPts + "us" );
1848+ }
1849+ }
18011850
18021851 /**
18031852 * Resumes the recording pipeline (no-op, for API compatibility).
@@ -1810,6 +1859,21 @@ public void resumeRecording() {
18101859
18111860 Log .d (TAG , "Resuming recording" );
18121861
1862+ Log .i (TAG , "========== RESUME RECORDING ===========" );
1863+
1864+ synchronized (timestampLock ) {
1865+ // Calculate pause duration for logging
1866+ if (pauseStartTimeNanos > 0 ) {
1867+ long pauseDuration = System .nanoTime () - pauseStartTimeNanos ;
1868+ totalPauseDurationNanos += pauseDuration ;
1869+ Log .i (TAG , "[RESUME] This pause duration: " + (pauseDuration / 1_000_000L ) + "ms" );
1870+ Log .i (TAG , "[RESUME] Total pause duration: " + (totalPauseDurationNanos / 1_000_000L ) + "ms" );
1871+ }
1872+
1873+ isPaused = false ;
1874+ pauseStartTimeNanos = -1 ;
1875+ }
1876+
18131877 // Resume audio recording if enabled
18141878 if (audioRecordingEnabled && audioRecord != null ) {
18151879 try {
@@ -2183,6 +2247,7 @@ private void setupAudio() {
21832247
21842248 /**
21852249 * Starts the audio thread to read PCM and feed the encoder.
2250+ * Handles pause/resume for camera switch scenarios.
21862251 */
21872252 private void startAudioThread () {
21882253 if (!audioRecordingEnabled || audioThreadRunning )
@@ -2196,13 +2261,35 @@ private void startAudioThread() {
21962261 final int aacFrameSize = 1024 * bytesPerFrame ; // 1024 PCM frames per AAC frame
21972262 final int readBufferSize = Math .max (aacFrameSize * 4 , 131072 ); // >= 128 KiB
21982263 byte [] readBuffer = new byte [readBufferSize ];
2199- long audioFramesWritten = 0L ; // PCM frames (not bytes)
2200- long lastPtsUs = 0L ;
2264+ long lastPtsUs = 0L ;
2265+
22012266 while (audioThreadRunning ) {
2267+ // Check if we're paused - wait for resume
2268+ if (isPaused ) {
2269+ // During pause, don't read audio - just wait
2270+ try {
2271+ Thread .sleep (10 );
2272+ } catch (InterruptedException e ) {
2273+ Thread .currentThread ().interrupt ();
2274+ }
2275+ continue ;
2276+ }
2277+
2278+ // Check if audioRecord is actually recording
2279+ if (audioRecord .getRecordingState () != android .media .AudioRecord .RECORDSTATE_RECORDING ) {
2280+ // AudioRecord is stopped (during pause), wait a bit
2281+ try {
2282+ Thread .sleep (10 );
2283+ } catch (InterruptedException e ) {
2284+ Thread .currentThread ().interrupt ();
2285+ }
2286+ continue ;
2287+ }
2288+
22022289 int read = audioRecord .read (readBuffer , 0 , readBuffer .length );
22032290 if (read > 0 ) {
22042291 int offset = 0 ;
2205- while (offset < read && audioThreadRunning ) {
2292+ while (offset < read && audioThreadRunning && ! isPaused ) {
22062293 int inputBufferIndex = audioEncoder .dequeueInputBuffer (10000 );
22072294 if (inputBufferIndex < 0 ) {
22082295 // Encoder busy; break and try next loop iteration
@@ -2215,13 +2302,33 @@ private void startAudioThread() {
22152302 codecInput .clear ();
22162303 int toCopy = Math .min (codecInput .remaining (), read - offset );
22172304 codecInput .put (readBuffer , offset , toCopy );
2218- // PTS based on frames written so far (stable, drift-free)
2219- long ptsUs = (audioFramesWritten * 1_000_000L ) / audioSampleRate ;
2220- audioEncoder .queueInputBuffer (inputBufferIndex , 0 , toCopy , ptsUs , 0 );
2221- lastPtsUs = ptsUs ;
2305+
2306+ // CRITICAL FIX: Audio PTS must sync with video PTS!
2307+ // Both use elapsed time from recordingStartSystemTimeNanos reference point
2308+ // This ensures they use the SAME timing base (system clock, not frame count)
2309+ synchronized (timestampLock ) {
2310+ long elapsedNanos = System .nanoTime () - recordingStartSystemTimeNanos - totalPauseDurationNanos ;
2311+ long ptsUs = Math .max (0 , elapsedNanos / 1000L ); // Convert to microseconds
2312+
2313+ // Ensure monotonically increasing PTS
2314+ if (ptsUs <= lastPtsUs && lastPtsUs > 0 ) {
2315+ ptsUs = lastPtsUs + 1 ; // Ensure at least 1us increment
2316+ }
2317+
2318+ audioEncoder .queueInputBuffer (inputBufferIndex , 0 , toCopy , ptsUs , 0 );
2319+ lastPtsUs = ptsUs ;
2320+ }
2321+
22222322 // Advance counters
22232323 offset += toCopy ;
2224- audioFramesWritten += (toCopy / bytesPerFrame );
2324+ }
2325+ } else if (read < 0 ) {
2326+ // Error or AudioRecord stopped
2327+ Log .w (TAG , "AudioRecord.read returned " + read + " - likely paused or error" );
2328+ try {
2329+ Thread .sleep (10 );
2330+ } catch (InterruptedException e ) {
2331+ Thread .currentThread ().interrupt ();
22252332 }
22262333 }
22272334 }
@@ -2233,14 +2340,16 @@ private void startAudioThread() {
22332340 while (eosRetries > 0 && !eosQueued ) {
22342341 int inputBufferIndex = audioEncoder .dequeueInputBuffer (50000 ); // 50ms timeout per retry
22352342 if (inputBufferIndex >= 0 ) {
2236- // Use lastPtsUs from our audio timeline to avoid duration jumps
2237- long eosPtsUs = (audioFramesWritten * 1_000_000L ) / audioSampleRate ;
2238- if (eosPtsUs < lastPtsUs )
2239- eosPtsUs = lastPtsUs ; // monotonic safeguard
2240- audioEncoder .queueInputBuffer (inputBufferIndex , 0 , 0 , eosPtsUs ,
2241- MediaCodec .BUFFER_FLAG_END_OF_STREAM );
2242- Log .d (TAG , "Audio EOS queued at PTS=" + eosPtsUs + "us (" + (eosPtsUs / 1000000.0 ) + "s)" );
2243- eosQueued = true ;
2343+ // Use system time for EOS PTS too, to match monotonic timeline
2344+ synchronized (timestampLock ) {
2345+ long eosPtsUs = Math .max (0 , (System .nanoTime () - recordingStartSystemTimeNanos - totalPauseDurationNanos ) / 1000L );
2346+ if (eosPtsUs < lastPtsUs )
2347+ eosPtsUs = lastPtsUs ; // monotonic safeguard
2348+ audioEncoder .queueInputBuffer (inputBufferIndex , 0 , 0 , eosPtsUs ,
2349+ MediaCodec .BUFFER_FLAG_END_OF_STREAM );
2350+ Log .d (TAG , "Audio EOS queued at PTS=" + eosPtsUs + "us (" + (eosPtsUs / 1000000.0 ) + "s)" );
2351+ eosQueued = true ;
2352+ }
22442353 } else {
22452354 eosRetries --;
22462355 Log .w (TAG , "Failed to get input buffer for audio EOS, retries left: " + eosRetries );
@@ -2360,9 +2469,11 @@ private void drainAudioEncoder() {
23602469
23612470 audioSamplesWritten ++;
23622471 lastAudioPts = bufferInfo .presentationTimeUs ;
2363- if (audioSamplesWritten % 100 == 0 ) {
2364- Log .d (TAG , String .format ("AUDIO: #%d, %.1fs, %db" ,
2365- audioSamplesWritten , bufferInfo .presentationTimeUs / 1000000.0 , bufferInfo .size ));
2472+ // Log every 50 samples for debugging
2473+ if (audioSamplesWritten % 50 == 0 ) {
2474+ Log .i (TAG , String .format ("[AUDIO_WRITE] Sample #%d, PTS=%.3fs (%dus), size=%db" ,
2475+ audioSamplesWritten , bufferInfo .presentationTimeUs / 1000000.0 ,
2476+ bufferInfo .presentationTimeUs , bufferInfo .size ));
23662477 }
23672478 // Detect silence pattern (constant 512-byte AAC frames)
23682479 if (bufferInfo .size == 512 ) {
0 commit comments