Skip to content

Commit 054bad8

Browse files
committed
Fix Surface thread-safety, decoder lock, AudioTrack.Builder, single-pass SDP
1 parent f063f53 commit 054bad8

1 file changed

Lines changed: 139 additions & 108 deletions

File tree

app/src/main/java/com/openipc/decoder/Decoder.java

Lines changed: 139 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
import android.content.pm.PackageManager;
1717
import android.graphics.Color;
1818
import android.graphics.drawable.GradientDrawable;
19+
import android.media.AudioAttributes;
1920
import android.media.AudioFormat;
20-
import android.media.AudioManager;
2121
import android.media.AudioTrack;
2222
import android.media.MediaCodec;
2323
import android.media.MediaFormat;
@@ -35,6 +35,7 @@
3535
import android.util.TypedValue;
3636
import android.view.Gravity;
3737
import android.view.Surface;
38+
import android.view.SurfaceHolder;
3839
import android.view.SurfaceView;
3940
import android.view.View;
4041
import 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

Comments
 (0)