diff --git a/app/build.gradle b/app/build.gradle
index 96206c0..de8b177 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,6 +10,7 @@ def AppVersionName = AppMajorVersion + "." + AppMinorVersion + "." + AppBuildNum
def AppVersionCode = AppMajorVersion * 100000 + AppMinorVersion * 1000 + AppBuildNumber
android {
+ namespace "com.xypower.mpremote"
compileSdk 33
defaultConfig {
@@ -78,20 +79,103 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10"
- implementation "androidx.media3:media3-exoplayer:1.1.1"
- implementation "androidx.media3:media3-exoplayer-dash:1.1.1"
- implementation "androidx.media3:media3-ui:1.1.1"
- implementation "androidx.media3:media3-datasource-rtmp:1.1.1"
- implementation "androidx.media3:media3-exoplayer-rtsp:1.1.1"
+
+// implementation 'io.vov.vitamio:vitamio:4.2.2'
+
+// implementation 'com.google.android.exoplayer:exoplayer:2.14.0' // 添加 ExoPlayer 库
+
+
+// // ExoPlayer 核心库
+// implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
+// implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
+// implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
+// implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'
+//
+//// RTSP 官方扩展
+// implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1'
+//
+//// RTMP 第三方扩展 (基于ExoPlayer 2.19.1)
+// implementation 'com.github.Piasy:ExoPlayerRtmp:v2.19.1.0'
+//
+//// H.265(HEVC) 和 AV1 解码器支持
+// implementation 'com.google.android.exoplayer:exoplayer-codec-av1:2.19.1'
+
+
+// implementation "androidx.media3:media3-exoplayer:1.1.1"
+// implementation "androidx.media3:media3-exoplayer-dash:1.1.1"
+// implementation "androidx.media3:media3-ui:1.1.1"
+// implementation "androidx.media3:media3-datasource-rtmp:1.1.1"
+// implementation ("androidx.media3:media3-exoplayer-rtsp:1.1.1"){
+// exclude group: 'com.google.android.exoplayer', module: 'exoplayer-core'
+// }
+// implementation 'androidx.media3:media3-decoder:1.1.1'
+
+
+
+ // Media3 统一版本
+// implementation "androidx.media3:media3-exoplayer:1.7.1"
+// implementation "androidx.media3:media3-exoplayer-rtsp:1.7.1"
+// implementation "androidx.media3:media3-ui:1.7.1"
+// implementation "androidx.media3:media3-datasource-rtmp:1.7.1"
+
+// // 排除所有旧版 ExoPlayer(关键!)
+// configurations.all {
+// exclude group: 'com.google.android.exoplayer', module: 'exoplayer-core'
+// exclude group: 'com.google.android.exoplayer', module: 'exoplayer-dash'
+// exclude group: 'com.google.android.exoplayer', module: 'exoplayer-ui'
+// }
+
+// implementation 'androidx.media3:media3-decoder-extension-ffmpeg:1.1.0'
+// implementation 'androidx.media3:media3-decoder-extension-ffmpeg-full:1.1.1'
+// 完整版(包含所有 FFmpeg 解码器,推荐用于 H.265/HEVC)
+// implementation 'androidx.media3:media3-decoder-extension-ffmpeg-full:1.1.1'
+
+
+
+// // 使用 BOM 管理 Media3 版本
+// implementation platform('androidx.media3:media3-bom:1.1.1')
+//
+// // 引入具体依赖(无需指定版本号)
+// implementation 'androidx.media3:media3-exoplayer'
+// implementation 'androidx.media3:media3-exoplayer-dash'
+// implementation 'androidx.media3:media3-ui'
+// implementation 'androidx.media3:media3-datasource-rtmp'
+// implementation 'androidx.media3:media3-exoplayer-rtsp'
+// implementation 'androidx.media3:media3-decoder'
+// implementation 'androidx.media3:media3-decoder-extension-ffmpeg-full'
+
+////// IJKPlayer相关库
+// implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
+// implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8' // ARMv7a 架构
+// implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8' // ARM64 架构
+// implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8' // x86 架构
+//
+// implementation 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.8'
+
+// implementation 'com.google.android.exoplayer:exoplayer-core:2.18.1'
+// implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.1' // HLS 支持
+// implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.1' // DASH 支持
+// RTSP 支持需额外添加扩展
+// implementation 'com.googlecode.rtsp-client:rtsp-client:2.1.0'
+// implementation 'com.github.videolan:vlc-android-sdk:3.3.10'
+// implementation 'com.github.videolan:libvlc-android:3.4.11'
+ // 使用更具体的ABI过滤,减少APK大小
+// implementation 'com.github.videolan:libvlc-android:3.4.11:arm64-v8a'
+// 或者包含所有ABI
+// implementation 'com.github.videolan:libvlc-android:3.4.11:all'
+
+// implementation 'org.videolan.android:libvlc-all:4.0.0-eap9'
+ implementation 'org.videolan.android:libvlc-all:3.6.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
// https://mvnrepository.com/artifact/dev.mobile/dadb
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'
+// 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'
+
}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index c566314..08bb8c4 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -20,4 +20,9 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class com.bumptech.glide.** { *; }
--dontwarn com.bumptech.glide.**
\ No newline at end of file
+-dontwarn com.bumptech.glide.**
+#-keep class tv.danmaku.ijk.media.player.** { *; }
+#-keep class tv.danmaku.ijk.media.widget.** { *; }
+# 保留 Media3 RTSP 相关类
+-keep class androidx.media3.datasource.rtsp.** { *; }
+-keep class androidx.media3.exoplayer.rtsp.** { *; }
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c529de..3c3d15b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,24 +13,27 @@
+
+
+ android:exported="false"
+ android:screenOrientation="portrait" />
{
+// initializePlayer();
+// // 增加下次重试的延迟时间(使用退避算法)
+// currentRetryDelay = Math.min((long) (currentRetryDelay * RETRY_BACKOFF_MULTIPLIER), MAX_RETRY_DELAY_MS);
+// }, currentRetryDelay);
+// }
+// }
+//
+// private void releasePlayerInternal() {
+// if (player != null) {
+// player.release();
+// player = null;
+// isBuffering = false;
+// retryHandler.removeCallbacks(bufferingTimeoutRunnable);
+// }
+// }
+//
+// @Override
+// protected void onDestroy() {
+// super.onDestroy();
+// AlertDialogUtils.dismiss(alertDialog);
+// releasePlayerInternal();
+// stopStreaming();
+// }
+//
+// @OptIn(markerClass = UnstableApi.class)
+// @Override
+// protected void onPause() {
+// super.onPause();
+// if (Util.SDK_INT < 24) {
+// releasePlayerInternal();
+// }
+// }
+//
+// @OptIn(markerClass = UnstableApi.class)
+// @Override
+// protected void onStop() {
+// super.onStop();
+// if (Util.SDK_INT >= 24) {
+// releasePlayerInternal();
+// }
+// }
+//
+// @Override
+// public boolean onCreateOptionsMenu(Menu menu) {
+// // Inflate the menu; this adds items to the action bar if it is present.
+// getMenuInflater().inflate(R.menu.stream_activity_actions, menu);
+// return true;
+// }
+//
+// @Override
+// public boolean onOptionsItemSelected(MenuItem item) {
+// int id = item.getItemId();
+// switch (id) {
+// case android.R.id.home: //返回键的id
+// initializePlayer();
+// return false;
+// default:
+// break;
+// }
+// return super.onOptionsItemSelected(item);
+// }
+//
+// @Override
+// public void onClick(View v) {
+// switch (v.getId()) {
+// case R.id.back:
+// finish();
+// break;
+// case R.id.refresh:
+// initEvent();
+// break;
+// }
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/mpremote/StreamActivity.java b/app/src/main/java/com/xypower/mpremote/StreamActivity.java
index 18c99d0..a83bef1 100644
--- a/app/src/main/java/com/xypower/mpremote/StreamActivity.java
+++ b/app/src/main/java/com/xypower/mpremote/StreamActivity.java
@@ -1,64 +1,64 @@
package com.xypower.mpremote;
import androidx.annotation.NonNull;
-import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.media3.common.MediaItem;
-import androidx.media3.common.PlaybackException;
-import androidx.media3.common.Player;
-import androidx.media3.common.util.UnstableApi;
-import androidx.media3.common.util.Util;
-import androidx.media3.datasource.rtmp.RtmpDataSource;
-import androidx.media3.exoplayer.DefaultLoadControl;
-import androidx.media3.exoplayer.ExoPlayer;
-import androidx.media3.exoplayer.source.ProgressiveMediaSource;
-import androidx.media3.ui.PlayerView;
import android.app.AlertDialog;
import android.content.Intent;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
import android.widget.Toast;
import com.xypower.mpremote.databinding.ActivityStreamBinding;
import com.xypower.mpremote.utils.AdbUtils;
import com.xypower.mpremote.utils.AlertDialogUtils;
+import org.videolan.libvlc.LibVLC;
+import org.videolan.libvlc.Media;
import org.videolan.libvlc.MediaPlayer;
+import org.videolan.libvlc.interfaces.IVLCVout;
-import dadb.AdbShellResponse;
-import dadb.Dadb;
+import java.util.ArrayList;
-public class StreamActivity extends AppCompatActivity implements View.OnClickListener {
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.view.TextureView;
- private static final String TAG = "STRM";
- private static final int MAX_RETRIES = 5;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
- private ExoPlayer player;
- private String mDeviceIp;
- private Handler retryHandler = new Handler();
- @NonNull
+import dadb.AdbShellResponse;
+import dadb.Dadb;
+
+public class StreamActivity extends AppCompatActivity implements IVLCVout.Callback, TextureView.SurfaceTextureListener {
+ private static final String TAG = "VLCPlayer";
+ private static final int PERMISSION_REQUEST_CODE = 1001;
+ private static String pushUrl = "";
+
+ private static final int RECONNECT_DELAY = 5000; // 重连延迟5秒
+ private int reconnectAttempts = 0;
+ private static final int MAX_RECONNECT_ATTEMPTS = 50; // 最大重连次数
+
+ private TextureView textureView;
+ private LibVLC libVLC;
+ private MediaPlayer mediaPlayer;
+ private SurfaceTexture surfaceTexture;
+ private Handler handler;
+ private boolean isSurfaceReady = false;
+ private boolean isPlayerReady = false;
private com.xypower.mpremote.databinding.ActivityStreamBinding binding;
- private String localIp;
+ private String mDeviceIp;
private int cameraId;
- private int anInt;
private int rotation;
private int netCamera;
private int vendor;
- 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 PlayerView playerView;
+ private int channel;
private AlertDialog alertDialog;
@Override
@@ -66,45 +66,72 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis
super.onCreate(savedInstanceState);
binding = ActivityStreamBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
- initHandler();
initIntent();
initView();
+ initHandler();
initEvent();
+ // 检查并请求权限
+ if (checkPermissions()) {
+// initializePlayer();
+ } else {
+ requestPermissions();
+ }
}
- private void initEvent() {
- RTMP_URL = "rtmp://" + mDeviceIp + "/live/0";
- String cmd = "am start -n com.xypower.mplive/com.xypower.mplive.MainActivity" + " --ei cameraId " + Integer.toString(cameraId) + " --ei rotation " + Integer.toString(rotation) + " --ei netCamera " + Integer.toString(netCamera) + " --ei vendor " + Integer.toString(vendor) + " --ei autoStart 1" + " --es url \"" + RTMP_URL + "\"";
- Runnable runnable = new Runnable() {
+ private void initHandler() {
+ handler = new Handler(Looper.getMainLooper()) {
@Override
- public void run() {
- initializePlayer();
+ public void handleMessage(@NonNull Message msg) {
+ switch (msg.what) {
+ case 1:
+ AlertDialogUtils.dismiss(alertDialog);
+ break;
+ }
}
};
- alertDialog = AlertDialogUtils.show(this, "视频加载中");
- startStreaming(cmd, runnable);
}
- private void initHandler() {
-// mHandler = new Handler();
+ private void initView() {
+ textureView = binding.textureView;
+ textureView.setSurfaceTextureListener(this);
+
+
}
+
private void initIntent() {
Intent intent = getIntent();
mDeviceIp = intent.getStringExtra("deviceIp");
- localIp = intent.getStringExtra("localIp");
cameraId = intent.getIntExtra("cameraId", 0);
- anInt = intent.getIntExtra("channel", 1);
+ channel = intent.getIntExtra("channel", 1);
rotation = intent.getIntExtra("rotation", -1);
netCamera = intent.getIntExtra("netCamera", 0);
vendor = intent.getIntExtra("vendor", 0);
}
- private void initView() {
- binding.toolbar.refresh.setVisibility(View.VISIBLE);
- binding.toolbar.back.setOnClickListener(this);
- binding.toolbar.refresh.setOnClickListener(this);
- playerView = binding.playerView;
+ private void initEvent() {
+ if (netCamera == 0) {
+ pushUrl = "rtmp://127.0.0.1/live/" + channel;
+ } else {
+ pushUrl = "rtsp://127.0.0.1:8554/live/" + channel;
+ }
+
+ if (TextUtils.isEmpty(pushUrl)) {
+ handler.post(() -> Toast.makeText(StreamActivity.this, "未找到推流路径", Toast.LENGTH_LONG).show());
+ } else {
+ String cmd = "am start -n com.xypower.mplive/com.xypower.mplive.MainActivity" + " --ei cameraId " + Integer.toString(cameraId) +" --ei channel " + Integer.toString(channel)+ " --ei rotation " + Integer.toString(rotation) + " --ei netCamera " + Integer.toString(netCamera) + " --ei vendor " + Integer.toString(vendor) + " --ei autoStart 1" + " --es url \"" + pushUrl + "\"";
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ initializePlayer();
+ }
+ };
+ alertDialog = AlertDialogUtils.show(this, "视频加载中");
+ startStreaming(cmd, runnable);
+// runOnUiThread(runnable);
+// new Thread(runnable).start();
+
+ }
}
private void startStreaming(final String cmd, final Runnable runnable) {
@@ -117,7 +144,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) {
- retryHandler.postDelayed(new Runnable() {
+ handler.postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(StreamActivity.this, R.string.err_dev_failed_to_connect, Toast.LENGTH_LONG).show();
@@ -140,7 +167,7 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis
if (adbShellResponse != null) {
if (adbShellResponse.getExitCode() == 0) {
try {
- Thread.sleep(5000);
+ Thread.sleep(150000);
} catch (Exception ex) {
}
runOnUiThread(runnable);
@@ -154,30 +181,26 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis
th.start();
}
+
private void stopStreaming() {
String cmd = "am force-stop com.xypower.mplive";
Thread th = new Thread(new Runnable() {
@Override
public void run() {
-
Dadb adb = null;
-
try {
Log.i(TAG, "Start connecting " + mDeviceIp);
adb = AdbManager.getAdb(mDeviceIp, AdbUtils.DEFAULT_ADB_PORT);
-
Log.i(TAG, "Finish connecting " + mDeviceIp);
if (adb == null) {
return;
}
-
AdbShellResponse adbShellResponse = null;
try {
adbShellResponse = adb.shell(cmd);
} catch (Exception ex) {
ex.printStackTrace();
}
-
if (adbShellResponse != null) {
if (adbShellResponse.getExitCode() == 0) {
}
@@ -188,154 +211,248 @@ public class StreamActivity extends AppCompatActivity implements View.OnClickLis
}
});
-
th.start();
}
- 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();
- }
+ private boolean checkPermissions() {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED;
+ }
- @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) {
- AlertDialogUtils.dismiss(alertDialog);
- isBuffering = false;
- retryHandler.removeCallbacks(bufferingTimeoutRunnable);
- retryCount = 0;
- currentRetryDelay = INITIAL_RETRY_DELAY_MS;
- Log.d(TAG, "播放器准备就绪");
- }
- }
- });
- }
- startPlayback();
+ private void requestPermissions() {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.INTERNET},
+ PERMISSION_REQUEST_CODE);
+ }
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == PERMISSION_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+// initializePlayer();
+ } else {
+ Toast.makeText(this, "需要存储权限才能播放视频", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
}
- private Runnable bufferingTimeoutRunnable = new Runnable() {
- @Override
- public void run() {
- if (isBuffering) {
- Log.w(TAG, "缓冲超时,触发重连");
- handlePlaybackError();
+ private void initializePlayer() {
+ // 初始化 LibVLC
+ ArrayList options = new ArrayList<>();
+ options.add("--aout=opensles");
+ options.add("--audio-time-stretch");
+// options.add("--avcodec-hw=none"); // 禁用硬件加速,强制软件解码
+ options.add("-vvv"); // 详细日志
+ // RTSP 特定参数
+ options.add("--rtsp-tcp"); // 使用 TCP 传输,提高稳定性
+ options.add("--network-caching=500"); // 网络缓存时间(毫秒)
+ options.add("--live-caching=500"); // 直播缓存时间
+ libVLC = new LibVLC(this, options);
+ mediaPlayer = new MediaPlayer(libVLC);
+
+ mediaPlayer.setEventListener(event -> {
+ switch (event.type) {
+ case MediaPlayer.Event.Playing:
+ Log.d(TAG, "播放中");
+ handler.sendMessage(handler.obtainMessage(1));
+ break;
+ case MediaPlayer.Event.Paused:
+ Log.d(TAG, "已暂停");
+ break;
+ case MediaPlayer.Event.Stopped:
+ Log.d(TAG, "已停止");
+ break;
+ case MediaPlayer.Event.EncounteredError:
+ Log.e(TAG, "播放错误: " + mediaPlayer.getMedia().getState());
+ handler.post(() -> Toast.makeText(StreamActivity.this, "播放失败: " + getErrorString(mediaPlayer.getMedia().getState()), Toast.LENGTH_LONG).show());
+ attemptReconnect();
+ break;
+ case MediaPlayer.Event.EndReached:
+ Log.d(TAG, "播放结束");
+ handler.post(() -> Toast.makeText(StreamActivity.this, "播放结束", Toast.LENGTH_SHORT).show());
+ attemptReconnect();
+ break;
}
+ });
+
+ isPlayerReady = true;
+ if (isSurfaceReady) {
+ loadMedia();
}
- };
-
- private void startPlayback() {
- if (player == null) return;
- Log.d(TAG, "开始播放,重试次数: " + retryCount);
- 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 + "), 延迟: " + currentRetryDelay + "ms");
- retryHandler.postDelayed(() -> {
+ private void attemptReconnect() {
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+ reconnectAttempts++;
+ Log.d(TAG, "尝试重连 (第" + reconnectAttempts + "次)");
+ handler.postDelayed(() -> {
+ releasePlayer();
initializePlayer();
- // 增加下次重试的延迟时间(使用退避算法)
- currentRetryDelay = Math.min((long) (currentRetryDelay * RETRY_BACKOFF_MULTIPLIER), MAX_RETRY_DELAY_MS);
- }, currentRetryDelay);
+ }, RECONNECT_DELAY);
+ } else {
+ Log.e(TAG, "达到最大重连次数,停止尝试");
+ handler.post(() -> Toast.makeText(StreamActivity.this, "无法连接到直播流,已达到最大重试次数", Toast.LENGTH_LONG).show());
}
}
- private void releasePlayerInternal() {
- if (player != null) {
- player.release();
- player = null;
- isBuffering = false;
- retryHandler.removeCallbacks(bufferingTimeoutRunnable);
+ private void loadMedia() {
+ if (mediaPlayer == null || surfaceTexture == null) return;
+
+ try {
+// String rtspUrl = "rtsp://61.169.135.146:1554/live/ab";
+// String rtspUrl = "rtsp://61.169.135.146:1554/live/abc";
+// String rtspUrl = "rtmp://61.169.135.146/live/cc";
+// String rtspUrl = "rtsp://192.168.43.1:8554/live/7";
+// String encodedUrl = java.net.URLEncoder.encode(rtspUrl, "UTF-8");
+// encodedUrl = encodedUrl.replaceAll("\\+", "%20"); // 将编码后的 + 替换为 %20
+
+ String rtspUrl = null;
+ if (netCamera == 0) {
+ rtspUrl = "rtmp://192.168.43.1/live/" + channel;
+ } else {
+ rtspUrl = "rtsp://192.168.43.1:8554/live/" + channel;
+ }
+ // 使用公开测试视频流
+ Media media = new Media(libVLC, Uri.parse(rtspUrl));
+
+ // 或者使用本地资源
+ // Media media = new Media(libVLC, Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sample));
+ Log.d(TAG, "创建 Media 对象后的地址: " + media.getUri());
+ mediaPlayer.setMedia(media);
+
+ // 设置视频输出
+ IVLCVout vout = mediaPlayer.getVLCVout();
+ vout.setVideoSurface(surfaceTexture);
+ vout.addCallback(this);
+ vout.attachViews();
+ mediaPlayer.setAspectRatio(null); // 设置为 null 以自适应屏幕
+ mediaPlayer.setScale(0); // 设置缩放比例为 0 以自适应屏幕
+ // 设置视频缩放模式为居中
+ mediaPlayer.setVideoScale(MediaPlayer.ScaleType.SURFACE_BEST_FIT);
+ // 开始播放
+ mediaPlayer.play();
+ } catch (Exception e) {
+ Log.e(TAG, "加载媒体失败: " + e.getMessage());
+ Toast.makeText(this, "加载媒体失败", Toast.LENGTH_SHORT).show();
+ attemptReconnect();
}
}
+ private String getErrorString(int state) {
+ switch (state) {
+ case Media.State.Error:
+ return "未知错误";
+// case Media.State.Invalid:
+// return "无效媒体";
+ case Media.State.NothingSpecial:
+ return "未初始化";
+ case Media.State.Opening:
+ return "正在打开";
+// case Media.State.Buffering:
+// return "缓冲中";
+ case Media.State.Playing:
+ return "播放中";
+ case Media.State.Paused:
+ return "已暂停";
+ case Media.State.Stopped:
+ return "已停止";
+ case Media.State.Ended:
+ return "播放结束";
+ default:
+ return "状态: " + state;
+ }
+ }
@Override
- protected void onDestroy() {
- super.onDestroy();
- AlertDialogUtils.dismiss(alertDialog);
- releasePlayerInternal();
- stopStreaming();
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ Log.d(TAG, "SurfaceTexture 可用");
+ surfaceTexture = surface;
+ isSurfaceReady = true;
+ if (isPlayerReady) {
+ loadMedia();
+ }
}
- @OptIn(markerClass = UnstableApi.class)
@Override
- protected void onPause() {
- super.onPause();
- if (Util.SDK_INT < 24) {
- releasePlayerInternal();
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ Log.d(TAG, "SurfaceTexture 尺寸改变: " + width + "x" + height);
+ if (mediaPlayer != null) {
+ mediaPlayer.getVLCVout().setWindowSize(width, height);
+ // 设置视频缩放模式
+ mediaPlayer.setAspectRatio(null); // 自适应屏幕
+ mediaPlayer.setScale(0); // 自动缩放
}
}
- @OptIn(markerClass = UnstableApi.class)
@Override
- protected void onStop() {
- super.onStop();
- if (Util.SDK_INT >= 24) {
- releasePlayerInternal();
- }
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ Log.d(TAG, "SurfaceTexture 销毁");
+ releasePlayer();
+ isSurfaceReady = false;
+ return true;
}
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // 每次 SurfaceTexture 更新时调用
+ }
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.stream_activity_actions, menu);
- return true;
+ public void onSurfacesCreated(IVLCVout vout) {
+ Log.d(TAG, "VLC 表面创建完成");
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- switch (id) {
- case android.R.id.home: //返回键的id
- this.finish();
- return false;
- case R.id.action_play:
- // scanWifi();
- initializePlayer();
- break;
- default:
- break;
+ public void onSurfacesDestroyed(IVLCVout vout) {
+ Log.d(TAG, "VLC 表面销毁");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (isPlayerReady && !mediaPlayer.isPlaying() && isSurfaceReady) {
+ loadMedia();
}
+ }
- return super.onOptionsItemSelected(item);
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mediaPlayer != null) {
+ mediaPlayer.pause();
+ }
}
@Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.back:
- finish();
- break;
- case R.id.refresh:
- initEvent();
- break;
+ protected void onStop() {
+ super.onStop();
+ if (mediaPlayer != null) {
+ mediaPlayer.stop();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ AlertDialogUtils.dismiss(alertDialog);
+ releasePlayer();
+ stopStreaming();
+ }
+
+ private void releasePlayer() {
+ if (mediaPlayer != null) {
+ IVLCVout vout = mediaPlayer.getVLCVout();
+ vout.detachViews();
+ vout.removeCallback(this);
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+ if (libVLC != null) {
+ libVLC.release();
+ libVLC = null;
}
+ isPlayerReady = false;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/mpremote/VideoActivity.java b/app/src/main/java/com/xypower/mpremote/VideoActivity.java
index d6a43c9..0b80e61 100644
--- a/app/src/main/java/com/xypower/mpremote/VideoActivity.java
+++ b/app/src/main/java/com/xypower/mpremote/VideoActivity.java
@@ -2,10 +2,6 @@ package com.xypower.mpremote;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.media3.common.MediaItem;
-import androidx.media3.common.Player;
-import androidx.media3.exoplayer.ExoPlayer;
-import androidx.media3.ui.PlayerView;
import android.content.Intent;
import android.content.res.Configuration;
@@ -19,7 +15,6 @@ import java.io.File;
public class VideoActivity extends AppCompatActivity {
- private ExoPlayer exoPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -34,14 +29,6 @@ public class VideoActivity extends AppCompatActivity {
Intent intent = getIntent();
String path = intent.getStringExtra("path");
if (path != null) {
- PlayerView playerView = (PlayerView)findViewById(R.id.playerView);
-
- exoPlayer = new ExoPlayer.Builder(this).build();
- playerView.setPlayer(exoPlayer);
- MediaItem mediaItem = MediaItem.fromUri(Uri.fromFile(new File(path)));
- exoPlayer.addMediaItem(mediaItem);
- exoPlayer.prepare();
- exoPlayer.play();
}
@@ -50,8 +37,6 @@ public class VideoActivity extends AppCompatActivity {
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
- PlayerView playerView = (PlayerView)findViewById(R.id.playerView);
- playerView.requestLayout();
}
@Override
diff --git a/app/src/main/res/layout/activity_device.xml b/app/src/main/res/layout/activity_device.xml
index b29023f..bfd2006 100644
--- a/app/src/main/res/layout/activity_device.xml
+++ b/app/src/main/res/layout/activity_device.xml
@@ -62,7 +62,7 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
@@ -79,4 +95,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_video.xml b/app/src/main/res/layout/activity_video.xml
index f15e27c..394e27c 100644
--- a/app/src/main/res/layout/activity_video.xml
+++ b/app/src/main/res/layout/activity_video.xml
@@ -6,13 +6,13 @@
android:layout_height="match_parent"
tools:context=".VideoActivity">
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index dab7c28..f9e65bb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,4 +18,5 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.enableJetifier=true
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 2399754..728fb3b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,22 +1,24 @@
pluginManagement {
repositories {
+ google()
+ maven { url 'https://jitpack.io' }
maven{ url 'https://maven.aliyun.com/repository/google'}
maven{ url 'https://maven.aliyun.com/repository/gradle-plugin'}
maven{ url 'https://maven.aliyun.com/repository/public'}
maven{ url 'https://maven.aliyun.com/repository/jcenter'}
gradlePluginPortal()
- google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
+ google()
+ maven { url 'https://jitpack.io' }
maven{ url 'https://maven.aliyun.com/repository/google'}
maven{ url 'https://maven.aliyun.com/repository/gradle-plugin'}
maven{ url 'https://maven.aliyun.com/repository/public'}
maven{ url 'https://maven.aliyun.com/repository/jcenter'}
- google()
mavenCentral()
}
}