1616import android .content .pm .PackageManager ;
1717import android .graphics .Color ;
1818import android .graphics .drawable .GradientDrawable ;
19+ import android .media .AudioAttributes ;
1920import android .media .AudioFormat ;
20- import android .media .AudioManager ;
2121import android .media .AudioTrack ;
2222import android .media .MediaCodec ;
2323import android .media .MediaFormat ;
3535import android .util .TypedValue ;
3636import android .view .Gravity ;
3737import android .view .Surface ;
38+ import android .view .SurfaceHolder ;
3839import android .view .SurfaceView ;
3940import android .view .View ;
4041import android .view .Window ;
@@ -75,6 +76,13 @@ public class Decoder extends Activity {
7576
7677 private int nalSize ; // only touched on the network thread — no volatile needed
7778 private volatile MediaCodec mDecoder ;
79+ // Surface captured on the UI thread via SurfaceHolder.Callback; volatile so the
80+ // network thread can safely read it in createDecoder() without holding any UI lock
81+ private volatile Surface mVideoSurface ;
82+ // guards all MediaCodec lifecycle operations (create / use / release) so that
83+ // closeDecoder() called from the network thread never races with decodeFrame()
84+ // called from the video thread
85+ private final Object decoderLock = new Object ();
7886 private SurfaceView mSurface ;
7987 private volatile AudioTrack audioTrack ;
8088 private TextView mConnect ;
@@ -150,6 +158,13 @@ protected void onCreate(Bundle savedInstanceState) {
150158
151159 mSurface = findViewById (R .id .video_surface );
152160 mSurface .setKeepScreenOn (true );
161+ // capture the rendering Surface on the UI thread; network threads read mVideoSurface
162+ // from createDecoder() — SurfaceHolder.getSurface() is not thread-safe to call directly
163+ mSurface .getHolder ().addCallback (new SurfaceHolder .Callback () {
164+ @ Override public void surfaceCreated (SurfaceHolder h ) { mVideoSurface = h .getSurface (); }
165+ @ Override public void surfaceChanged (SurfaceHolder h , int f , int w , int hh ) { mVideoSurface = h .getSurface (); }
166+ @ Override public void surfaceDestroyed (SurfaceHolder h ) { mVideoSurface = null ; }
167+ });
153168 mConnect = findViewById (R .id .text_connect );
154169 mConnect .setTextColor (Color .LTGRAY );
155170
@@ -614,10 +629,20 @@ private void createAudio() {
614629 return ;
615630 }
616631
617- // build into a local first: assigning to the volatile field immediately would expose
618- // a partially-initialised AudioTrack to onPause() → closeAudio() on the UI thread
619- AudioTrack track = new AudioTrack (AudioManager .STREAM_MUSIC , sample , format ,
620- AudioFormat .ENCODING_PCM_16BIT , size , AudioTrack .MODE_STREAM );
632+ // AudioTrack.Builder replaces the deprecated stream-based constructor (API 21+)
633+ AudioTrack track = new AudioTrack .Builder ()
634+ .setAudioAttributes (new AudioAttributes .Builder ()
635+ .setUsage (AudioAttributes .USAGE_MEDIA )
636+ .setContentType (AudioAttributes .CONTENT_TYPE_MUSIC )
637+ .build ())
638+ .setAudioFormat (new AudioFormat .Builder ()
639+ .setEncoding (AudioFormat .ENCODING_PCM_16BIT )
640+ .setSampleRate (sample )
641+ .setChannelMask (AudioFormat .CHANNEL_OUT_MONO )
642+ .build ())
643+ .setBufferSizeInBytes (size )
644+ .setTransferMode (AudioTrack .MODE_STREAM )
645+ .build ();
621646 if (track .getState () != AudioTrack .STATE_INITIALIZED ) {
622647 // resource exhaustion or invalid config at the OS audio layer
623648 Log .e (TAG , "AudioTrack failed to initialize, releasing" );
@@ -657,14 +682,6 @@ private void closeAudio() {
657682 }
658683
659684 private void decodeFrame (Frame buffer ) {
660- // snapshot to a local: onPause() can null mDecoder from the UI thread at any moment,
661- // and a volatile read between null-check and method call would be a TOCTOU race
662- MediaCodec codec = mDecoder ;
663- if (codec == null ) {
664- if (!decoderFailed ) createDecoder (); // skip if codec proved unsupported
665- return ;
666- }
667-
668685 if (buffer .length () < 5 ) { // need at least 4-byte start code + 1 NAL type byte
669686 Log .w (TAG , "NAL frame too short: " + buffer .length ());
670687 return ;
@@ -682,56 +699,78 @@ private void decodeFrame(Frame buffer) {
682699 flag = MediaCodec .BUFFER_FLAG_CODEC_CONFIG ;
683700 }
684701
685- try {
686- int inputBufferId = codec .dequeueInputBuffer (0 );
687- if (inputBufferId >= 0 ) {
688- ByteBuffer inputBuffer = codec .getInputBuffer (inputBufferId );
689- if (inputBuffer != null ) {
690- inputBuffer .clear (); // reset position/limit — dequeue does not guarantee this
691- inputBuffer .put (buffer .data (), 0 , buffer .length ());
692- codec .queueInputBuffer (inputBufferId , 0 ,
693- buffer .length (), System .nanoTime () / 1000 , flag );
694- }
695- }
702+ // Hold decoderLock for the entire codec operation: closeDecoder() (called from the
703+ // network thread on codec-switch) must not call stop()/release() while we are still
704+ // feeding buffers or dequeuing output — MediaCodec is not thread-safe for that.
705+ boolean needCreate = false ;
706+ synchronized (decoderLock ) {
707+ MediaCodec codec = mDecoder ;
708+ if (codec == null ) {
709+ needCreate = !decoderFailed ; // createDecoder() acquires decoderLock itself
710+ } else {
711+ try {
712+ int inputBufferId = codec .dequeueInputBuffer (0 );
713+ if (inputBufferId >= 0 ) {
714+ ByteBuffer inputBuffer = codec .getInputBuffer (inputBufferId );
715+ if (inputBuffer != null ) {
716+ inputBuffer .clear (); // reset position/limit — dequeue does not guarantee this
717+ inputBuffer .put (buffer .data (), 0 , buffer .length ());
718+ codec .queueInputBuffer (inputBufferId , 0 ,
719+ buffer .length (), System .nanoTime () / 1000 , flag );
720+ }
721+ }
696722
697- MediaCodec .BufferInfo info = mBufferInfo ;
698- int outputBufferId = codec .dequeueOutputBuffer (info , 0 );
699-
700- if (outputBufferId == MediaCodec .INFO_OUTPUT_FORMAT_CHANGED ) {
701- // decoder signals a format change — read resolution from the canonical no-arg call
702- MediaFormat format = codec .getOutputFormat ();
703- int mWidth = format .getInteger (MediaFormat .KEY_WIDTH );
704- int mHeight = format .getInteger (MediaFormat .KEY_HEIGHT );
705- if (lastWidth != mWidth || lastHeight != mHeight ) {
706- lastWidth = mWidth ;
707- lastHeight = mHeight ;
708- updateResolution (lastWidth , lastHeight );
723+ MediaCodec .BufferInfo info = mBufferInfo ;
724+ int outputBufferId = codec .dequeueOutputBuffer (info , 0 );
725+
726+ if (outputBufferId == MediaCodec .INFO_OUTPUT_FORMAT_CHANGED ) {
727+ // decoder signals a format change — read resolution from the canonical no-arg call
728+ MediaFormat format = codec .getOutputFormat ();
729+ int mWidth = format .getInteger (MediaFormat .KEY_WIDTH );
730+ int mHeight = format .getInteger (MediaFormat .KEY_HEIGHT );
731+ if (lastWidth != mWidth || lastHeight != mHeight ) {
732+ lastWidth = mWidth ;
733+ lastHeight = mHeight ;
734+ updateResolution (lastWidth , lastHeight );
735+ }
736+ } else if (outputBufferId >= 0 ) {
737+ codec .releaseOutputBuffer (outputBufferId , true );
738+ }
739+ } catch (Exception e ) {
740+ Log .e (TAG , "Codec exception: " + e .getMessage ());
741+ // clear mDecoder under lock so the video thread won't retry before release
742+ mDecoder = null ;
743+ decoderFailed = false ;
744+ try { codec .stop (); } catch (Exception ignored ) {}
745+ try { codec .release (); } catch (Exception ignored ) {}
709746 }
710- } else if (outputBufferId >= 0 ) {
711- codec .releaseOutputBuffer (outputBufferId , true );
712747 }
713- } catch (Exception e ) {
714- Log .e (TAG , "Codec exception: " + e .getMessage ());
715- closeDecoder (); // stop() + release() to avoid native resource leak
716748 }
749+ // createDecoder() acquires decoderLock internally — must be called outside our block
750+ if (needCreate ) createDecoder ();
717751 }
718752
719753 private void createDecoder () {
720- if (mDecoder != null ) return ; // already running — avoid double-init and native leak
754+ // fast pre-check before allocating anything
755+ synchronized (decoderLock ) {
756+ if (mDecoder != null ) return ; // already running — avoid double-init and native leak
757+ }
721758
722- Surface mVideo = mSurface .getHolder ().getSurface ();
723- if (!mVideo .isValid ()) {
759+ // use the Surface captured on the UI thread — getSurface() is not thread-safe to call
760+ // from the network thread directly (the SurfaceHolder.Callback populates mVideoSurface)
761+ Surface mVideo = mVideoSurface ;
762+ if (mVideo == null || !mVideo .isValid ()) {
724763 return ;
725764 }
726765
727- MediaCodec local ;
728766 String type = codecH265 ? "video/hevc" : "video/avc" ;
729767
730768 MediaFormat format = MediaFormat .createVideoFormat (type , 1280 , 720 );
731769 // pre-declare the max input size to match our NAL buffer; prevents
732770 // BufferOverflowException when a large intra-frame arrives
733771 format .setInteger (MediaFormat .KEY_MAX_INPUT_SIZE , 1024 * 1024 );
734772
773+ MediaCodec local ;
735774 try {
736775 Log .i (TAG , "Start video decoder" );
737776 local = MediaCodec .createDecoderByType (type );
@@ -751,7 +790,14 @@ private void createDecoder() {
751790 return ;
752791 }
753792
754- mDecoder = local ;
793+ synchronized (decoderLock ) {
794+ if (mDecoder != null ) {
795+ // another thread created the decoder while we were outside the lock; discard ours
796+ local .release ();
797+ return ;
798+ }
799+ mDecoder = local ;
800+ }
755801 // reset the watchdog baseline: decodeFrame() only updates lastFrame after the codec
756802 // is ready, so the first call (codec==null path) returns early without touching it.
757803 // Without this, a keyframe interval > 3s would trigger a spurious stream disconnect.
@@ -760,18 +806,21 @@ private void createDecoder() {
760806 }
761807
762808 private void closeDecoder () {
763- MediaCodec codec = mDecoder ;
764- if (codec != null ) {
765- // null the field BEFORE stop/release: any concurrent decodeFrame() snapshot
766- // will then see null and skip, rather than operating on a stopping codec
809+ MediaCodec codec ;
810+ synchronized (decoderLock ) {
811+ codec = mDecoder ;
812+ if (codec == null ) { decoderFailed = false ; return ; }
813+ // null first so decodeFrame() sees null as soon as possible
767814 mDecoder = null ;
768- Log .i (TAG , "Close video decoder" );
769- try {
770- codec .stop ();
771- codec .release ();
772- } catch (Exception e ) {
773- Log .e (TAG , "Decoder close exception" , e );
774- }
815+ }
816+ Log .i (TAG , "Close video decoder" );
817+ // stop()/release() outside the lock so we don't block other threads while
818+ // the driver tears down the hardware codec pipeline
819+ try {
820+ codec .stop ();
821+ codec .release ();
822+ } catch (Exception e ) {
823+ Log .e (TAG , "Decoder close exception" , e );
775824 }
776825 decoderFailed = false ; // allow re-init on the next RTSP session
777826 }
@@ -831,39 +880,23 @@ private static String readRtspResponse(InputStream in, String targetHeader) thro
831880 }
832881
833882 /**
834- * Extracts the audio clock rate from an SDP body.
835- * Looks for "a=rtpmap:<RTP_PT_PCMU> <encoding>/<clock_rate>[/<channels>]"
836- * and stores the result in {@link #audioSampleRate}.
883+ * Parses the full SDP body in a single pass (RFC 4566), extracting:
884+ * <ul>
885+ * <li>audio clock rate → stored in {@link #audioSampleRate}</li>
886+ * <li>video codec (H.264 vs H.265) → stored in {@link #codecH265}</li>
887+ * <li>per-track Control URLs (RFC 2326 §C.1.1) → returned as [video, audio]</li>
888+ * </ul>
889+ * Absolute {@code a=control:} values are used as-is; relative values are resolved
890+ * against {@code baseUrl}. Falls back to "{@code baseUrl}/trackID=N" if absent.
837891 */
838- private void parseSdpAudioRate (String sdp ) {
839- String prefix = "a=rtpmap:" + RTP_PT_PCMU + " " ;
840- for (String line : sdp .split ("[\r \n ]+" )) {
841- if (line .startsWith (prefix )) {
842- int slash = line .indexOf ('/' , prefix .length ());
843- if (slash >= 0 ) {
844- int end = line .indexOf ('/' , slash + 1 );
845- String rateStr = (end >= 0 ? line .substring (slash + 1 , end ) : line .substring (slash + 1 )).trim ();
846- try {
847- int rate = Integer .parseInt (rateStr );
848- if (rate > 0 && rate <= 192000 ) {
849- audioSampleRate = rate ;
850- Log .d (TAG , "Audio sample rate from SDP: " + rate + " Hz" );
851- }
852- } catch (NumberFormatException ignored ) {}
853- }
854- break ;
855- }
856- }
857- }
892+ private String [] parseSdp (String sdp , String baseUrl ) {
893+ String [] controls = { baseUrl + "/trackID=0" , baseUrl + "/trackID=1" };
894+ String audioPtPrefix = "a=rtpmap:" + RTP_PT_PCMU + " " ;
895+ int track = -1 ; // -1 = session section, 0 = video, 1 = audio
858896
859- /**
860- * Detects the video codec from the SDP "m=video" line and updates {@link #codecH265}.
861- * Knowing the codec before the first RTP packet allows pre-warming the decoder immediately
862- * after PLAY, hiding the 200–500 ms MediaCodec startup cost behind network latency.
863- */
864- private void parseSdpVideoCodec (String sdp ) {
865897 for (String line : sdp .split ("[\r \n ]+" )) {
866898 if (line .startsWith ("m=video" )) {
899+ track = 0 ;
867900 // "m=video <port> RTP/AVP <pt> [...]" — first payload type determines codec
868901 String [] parts = line .split ("\\ s+" );
869902 if (parts .length >= 4 ) {
@@ -874,27 +907,27 @@ private void parseSdpVideoCodec(String sdp) {
874907 + " (PT=" + pt + ")" );
875908 } catch (NumberFormatException ignored ) {}
876909 }
877- break ;
878- }
879- }
880- }
881-
882- /**
883- * Parses per-track Control URLs from an SDP body (RFC 2326 §C.1.1).
884- * Returns a 2-element array: [videoControlUrl, audioControlUrl].
885- * Absolute "a=control:" values are used as-is; relative values are resolved
886- * against {@code baseUrl}. Falls back to "{@code baseUrl}/trackID=N" if absent.
887- */
888- private static String [] parseSdpControls (String sdp , String baseUrl ) {
889- String [] controls = { baseUrl + "/trackID=0" , baseUrl + "/trackID=1" };
890- int track = -1 ;
891- for (String line : sdp .split ("[\r \n ]+" )) {
892- if (line .startsWith ("m=video" )) track = 0 ;
893- else if (line .startsWith ("m=audio" )) track = 1 ;
894- else if (line .startsWith ("a=control:" ) && track >= 0 ) {
910+ } else if (line .startsWith ("m=audio" )) {
911+ track = 1 ;
912+ } else if (line .startsWith ("a=control:" ) && track >= 0 ) {
895913 String ctrl = line .substring ("a=control:" .length ()).trim ();
896914 // absolute URL used as-is; relative URL appended to base
897915 controls [track ] = ctrl .startsWith ("rtsp://" ) ? ctrl : baseUrl + "/" + ctrl ;
916+ } else if (line .startsWith (audioPtPrefix )) {
917+ int slash = line .indexOf ('/' , audioPtPrefix .length ());
918+ if (slash >= 0 ) {
919+ int end = line .indexOf ('/' , slash + 1 );
920+ String rateStr = (end >= 0
921+ ? line .substring (slash + 1 , end )
922+ : line .substring (slash + 1 )).trim ();
923+ try {
924+ int rate = Integer .parseInt (rateStr );
925+ if (rate > 0 && rate <= 192000 ) {
926+ audioSampleRate = rate ;
927+ Log .d (TAG , "Audio sample rate from SDP: " + rate + " Hz" );
928+ }
929+ } catch (NumberFormatException ignored ) {}
930+ }
898931 }
899932 }
900933 return controls ;
@@ -954,10 +987,8 @@ private void rtspConnect() throws Exception {
954987 sdp .append (new String (skipBuf , 0 , n , StandardCharsets .UTF_8 ));
955988 sdpBodyLen -= n ;
956989 }
957- parseSdpAudioRate (sdp .toString ());
958- parseSdpVideoCodec (sdp .toString ()); // detect codec early to pre-warm the decoder
959- // parse per-track Control URLs; used for SETUP requests below
960- String [] trackUrls = parseSdpControls (sdp .toString (), rtspUrl );
990+ // single-pass SDP parse: audio rate, video codec, and per-track Control URLs
991+ String [] trackUrls = parseSdp (sdp .toString (), rtspUrl );
961992
962993 seq ++;
963994 String type = mType ? "RTP/AVP/UDP;unicast;client_port=5000"
0 commit comments