diff --git a/app/build.gradle b/app/build.gradle index 828a45e..3567447 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,7 +89,7 @@ dependencies { implementation 'dev.mobile:dadb:1.2.8' implementation files('libs/common-release.aar') // implementation project(path: ':dadb') - + implementation 'org.videolan.android:libvlc-all:3.4.5' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/java/com/xypower/mpremote/StreamActivity.java b/app/src/main/java/com/xypower/mpremote/StreamActivity.java index ac8fe2f..4b62087 100644 --- a/app/src/main/java/com/xypower/mpremote/StreamActivity.java +++ b/app/src/main/java/com/xypower/mpremote/StreamActivity.java @@ -2,7 +2,6 @@ package com.xypower.mpremote; import androidx.annotation.NonNull; import androidx.annotation.OptIn; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; @@ -18,26 +17,25 @@ import androidx.media3.ui.PlayerView; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; -import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; import android.view.View; import android.widget.Toast; -import com.xypower.common.FilesUtils; -import com.xypower.mpremote.databinding.ActivityMainBinding; import com.xypower.mpremote.databinding.ActivityStreamBinding; import com.xypower.mpremote.utils.AdbUtils; -import com.xypower.mpremote.zlmediakit.ZLMediaKit; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; +import org.videolan.libvlc.LibVLC; +import org.videolan.libvlc.Media; +import org.videolan.libvlc.MediaPlayer; + +import java.util.ArrayList; -import dadb.AdbKeyPair; import dadb.AdbShellResponse; import dadb.Dadb; @@ -47,9 +45,9 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis private static final String TAG = "STRM"; private static final int MAX_RETRIES = 5; - private ExoPlayer exoPlayer; + private ExoPlayer player; private String mDeviceIp; - private Handler mHandler; + private Handler retryHandler = new Handler(); @NonNull private com.xypower.mpremote.databinding.ActivityStreamBinding binding; private String localIp; @@ -58,8 +56,18 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis private int rotation; private int netCamera; private int vendor; - private PlayerView playerView; private int retryCount; + private static final long INITIAL_RETRY_DELAY_MS = 1000; // 初始重试延迟 + private static final long MAX_RETRY_DELAY_MS = 15000; // 最大重试延迟 + private static final float RETRY_BACKOFF_MULTIPLIER = 1.5f; // 退避乘数 + private long currentRetryDelay = INITIAL_RETRY_DELAY_MS; + private boolean isBuffering = false; + private long bufferingStartTime = 0; + private static final long BUFFERING_TIMEOUT_MS = 10000; // 10秒缓冲超时 + private String RTMP_URL; + private SurfaceView playerView; + private MediaPlayer mediaPlayer; + private LibVLC libVLC; @Override protected void onCreate(Bundle savedInstanceState) { @@ -73,7 +81,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis } private void initEvent() { - String url = "rtmp://" + mDeviceIp + "/live/0"; + RTMP_URL = "rtmp://" + mDeviceIp + "/live/0"; String cmd = "am start -n com.xypower.mplive/com.xypower.mplive.MainActivity" + " --ei cameraId " + Integer.toString(cameraId) @@ -81,7 +89,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis + " --ei netCamera " + Integer.toString(netCamera) + " --ei vendor " + Integer.toString(vendor) + " --ei autoStart 1" - + " --es url \"" + url + "\""; + + " --es url \"" + RTMP_URL + "\""; Runnable runnable = new Runnable() { @Override public void run() { @@ -89,10 +97,22 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis } }; startStreaming(cmd, runnable); + + + } private void initHandler() { - mHandler = new Handler(); +// mHandler = new Handler(); + + // 配置 VLC 参数 + ArrayList options = new ArrayList<>(); + options.add("--aout=opensles"); // 优化音频延迟‌:ml-citation{ref="6" data="citationList"} + options.add("--audio-time-stretch"); + + libVLC = new LibVLC(this, options); + mediaPlayer = new MediaPlayer(libVLC); + } private void initIntent() { @@ -111,6 +131,25 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis binding.toolbar.back.setOnClickListener(this); binding.toolbar.refresh.setOnClickListener(this); playerView = binding.playerView; + SurfaceHolder holder = playerView.getHolder(); + holder.addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + Surface surface = holder.getSurface(); + mediaPlayer.getVLCVout().setVideoSurface(surface,holder); + mediaPlayer.getVLCVout().attachViews(); // 绑定视图‌:ml-citation{ref="8" data="citationList"} + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + + } + }); } private void startStreaming(final String cmd, final Runnable runnable) { @@ -124,7 +163,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis adb = AdbManager.getAdb(mDeviceIp, AdbUtils.DEFAULT_ADB_PORT); Log.i(TAG, "Finish connecting " + mDeviceIp); if (adb == null) { - mHandler.postDelayed(new Runnable() { + retryHandler.postDelayed(new Runnable() { @Override public void run() { Toast.makeText(StreamActivity.this, R.string.err_dev_failed_to_connect, Toast.LENGTH_LONG).show(); @@ -147,7 +186,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis if (adbShellResponse != null) { if (adbShellResponse.getExitCode() == 0) { try { - Thread.sleep(100); + Thread.sleep(5000); } catch (Exception ex) { } runOnUiThread(runnable); @@ -201,7 +240,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis th.start(); } - private void initializePlayer() { + private void initializePlayer2() { // // 创建重试策略 // DefaultLoadControl loadControl = new DefaultLoadControl.Builder() // .setBufferDurationsMs( @@ -210,52 +249,154 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis // 500, // bufferForPlaybackMs // 500 // bufferForPlaybackAfterRebufferMs // ).build(); - exoPlayer = new ExoPlayer.Builder(this).build(); - playerView.setPlayer(exoPlayer); - exoPlayer.addListener(new Player.Listener() { - @Override - public void onPlayerError(PlaybackException error) { - Log.e(TAG, "播放错误: " + error.getMessage()); +// player = new ExoPlayer.Builder(this).build(); +// playerView.setPlayer(player); +// player.addListener(new Player.Listener() { +// @Override +// public void onPlayerError(PlaybackException error) { +// Log.e(TAG, "播放错误: " + error.getMessage()); +// handlePlaybackError(); +// } +// +// @Override +// public void onPlaybackStateChanged(int state) { +// if (state == Player.STATE_READY) { +// // 重置重试计数器当成功连接时 +// retryCount = 0; +// } +// } +// }); +// startPlayback(); + } + +// private void handlePlaybackError2() { +// if (retryCount < MAX_RETRIES) { +// retryCount++; +// Log.d(TAG, "准备重试 (" + retryCount + "/" + MAX_RETRIES + ")..."); +// mHandler.postDelayed(() -> { +// if (player != null) { +// startPlayback(); +// } +// }, 3000); +// } else { +// Log.e(TAG, "达到最大重试次数,停止尝试"); +// // 这里可以添加UI提示或执行其他错误处理 +// } +// } + + private void startPlayback2() { + if (player == null) return; + Log.d(TAG, "开始播放,重试次数: " + retryCount); + MediaItem mediaItem = MediaItem.fromUri("rtmp://" + mDeviceIp + "/live/0"); + ProgressiveMediaSource videoSource = new ProgressiveMediaSource.Factory(new RtmpDataSource.Factory()) + .createMediaSource(mediaItem); + player.setMediaSource(videoSource); + player.prepare(); + player.setPlayWhenReady(true); + } + + + private void initializePlayer() { +// if (player == null) { +// player = new ExoPlayer.Builder(this) +// .setLoadControl(new DefaultLoadControl.Builder() +// .setBufferDurationsMs( +// 5000, // minBufferMs +// 10000, // maxBufferMs +// 500, // bufferForPlaybackMs +// 500 // bufferForPlaybackAfterRebufferMs +// ).setPrioritizeTimeOverSizeThresholds(true) +// .build()) +// .build(); +// +//// playerView.setPlayer(player); +// +// player.addListener(new Player.Listener() { +// @Override +// public void onPlayerError(PlaybackException error) { +// Log.e(TAG, "播放错误: " + error.getMessage()); +// handlePlaybackError(); +// } +// +// @Override +// public void onPlaybackStateChanged(int state) { +// if (state == Player.STATE_BUFFERING) { +// if (!isBuffering) { +// isBuffering = true; +// bufferingStartTime = System.currentTimeMillis(); +// // 启动缓冲超时检查 +// retryHandler.postDelayed(bufferingTimeoutRunnable, BUFFERING_TIMEOUT_MS); +// } +// } else if (state == Player.STATE_READY) { +// isBuffering = false; +// retryHandler.removeCallbacks(bufferingTimeoutRunnable); +// retryCount = 0; +// currentRetryDelay = INITIAL_RETRY_DELAY_MS; +// Log.d(TAG, "播放器准备就绪"); +// } +// } +// }); +// } +// startPlayback(); + Media media = new Media(libVLC, Uri.parse(RTMP_URL)); + mediaPlayer.setMedia(media); + mediaPlayer.play(); + + } + + private Runnable bufferingTimeoutRunnable = new Runnable() { + @Override + public void run() { + if (isBuffering) { + Log.w(TAG, "缓冲超时,触发重连"); handlePlaybackError(); } + } + }; - @Override - public void onPlaybackStateChanged(int state) { - if (state == Player.STATE_READY) { - // 重置重试计数器当成功连接时 - retryCount = 0; - } - } - }); - startPlayback(); + private void startPlayback() { + if (player == null) return; + + Log.d(TAG, "开始播放,重试次数: " + retryCount); +// MediaItem mediaItem = MediaItem.fromUri(Uri.parse(RTMP_URL)); + + MediaItem mediaItem = MediaItem.fromUri(RTMP_URL); + ProgressiveMediaSource videoSource = new ProgressiveMediaSource.Factory(new RtmpDataSource.Factory()) + .createMediaSource(mediaItem); + player.setMediaSource(videoSource); +// player.setMediaItem(mediaItem); + player.prepare(); + player.setPlayWhenReady(true); } private void handlePlaybackError() { + releasePlayerInternal(); + if (retryCount < MAX_RETRIES) { retryCount++; - Log.d(TAG, "准备重试 (" + retryCount + "/" + MAX_RETRIES + ")..."); - mHandler.postDelayed(() -> { - if (exoPlayer != null) { - startPlayback(); - } - }, 3000); - } else { - Log.e(TAG, "达到最大重试次数,停止尝试"); - // 这里可以添加UI提示或执行其他错误处理 + Log.d(TAG, "准备重试 (" + retryCount + "), 延迟: " + currentRetryDelay + "ms"); + + retryHandler.postDelayed(() -> { + initializePlayer(); + // 增加下次重试的延迟时间(使用退避算法) + currentRetryDelay = Math.min((long)(currentRetryDelay * RETRY_BACKOFF_MULTIPLIER), MAX_RETRY_DELAY_MS); + }, currentRetryDelay); } } - private void startPlayback() { - if (exoPlayer == null) return; - Log.d(TAG, "开始播放,重试次数: " + retryCount); - MediaItem mediaItem = MediaItem.fromUri("rtmp://" + mDeviceIp + "/live/0"); - ProgressiveMediaSource videoSource = new ProgressiveMediaSource.Factory(new RtmpDataSource.Factory()) - .createMediaSource(mediaItem); - exoPlayer.setMediaSource(videoSource); - exoPlayer.prepare(); - exoPlayer.setPlayWhenReady(true); + private void releasePlayerInternal() { + if (player != null) { + player.release(); + player = null; + isBuffering = false; + retryHandler.removeCallbacks(bufferingTimeoutRunnable); + } } + + + + @Override protected void onDestroy() { super.onDestroy(); @@ -281,9 +422,9 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis } private void releasePlayer() { - if (exoPlayer != null) { - exoPlayer.release(); - exoPlayer = null; + if (player != null) { + player.release(); + player = null; } } diff --git a/app/src/main/res/layout/activity_stream.xml b/app/src/main/res/layout/activity_stream.xml index 1275d37..43b0de9 100644 --- a/app/src/main/res/layout/activity_stream.xml +++ b/app/src/main/res/layout/activity_stream.xml @@ -11,17 +11,24 @@ android:id="@+id/toolbar" layout="@layout/toolbar" /> - + app:layout_constraintTop_toBottomOf="@+id/toolbar" /> + + + + + + + + + + + +