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() } }