一、前言
最近在做工业平板监控项目,需要同时播放多路 RTSP 摄像头视频。调研了一圈方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MediaPlayer | 系统自带 | 不支持RTSP认证,兼容性极差 | 本地视频 |
| IjkPlayer | 功能全,文档多 | 已停更,体积大(20MB+) | 短视频APP |
| ExoPlayer | Google官方 | RTSP支持实验性,Bug多 | 网络流媒体 |
| libVLC | 协议全,稳定性高 | 包体大(40MB),API底层 | 监控/直播 |
最终选择 libVLC 3.5.1,毕竟是VLC官方出品,对各种国产摄像头的兼容性最好。本文给出可直接落地的完整代码。
二、Gradle配置
java
// build.gradle (app)
dependencies {
implementation 'org.videolan.android:libvlc-all:3.5.1'
}
注意: libvlc-all 包含完整解码器(约40MB)。如果只需要基础功能,可换 libvlc-core 减小体积。
三、权限声明
XML
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
四、核心封装类:VlcRtspPlayer.java
直接可用的生产级封装,处理了异步初始化、视图保护、自动重连、资源回收等关键问题:
java
package com.example.rtspplayerdemo.vlcvideo;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
import org.videolan.libvlc.MediaPlayer;
import org.videolan.libvlc.util.VLCVideoLayout;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* VLC RTSP 播放器封装类
* 特性:异步初始化、自动重连、视图就绪检测、完整生命周期管理
*/
public class VlcRtspPlayer {
private static final String TAG = "VlcRtspPlayer";
private static final int DEFAULT_NETWORK_CACHE = 500;
private static final int DEFAULT_RECONNECT_DELAY = 3000;
private static final int DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
private static final int CONNECTION_TIMEOUT_MS = 10000;
private Context context;
private volatile LibVLC libVLC;
private volatile MediaPlayer mediaPlayer;
private volatile VLCVideoLayout videoLayout;
private volatile String currentUrl;
private volatile PlayerState playerState = PlayerState.IDLE;
private volatile int reconnectAttempts = 0;
private volatile boolean isReleased = false;
private AtomicBoolean isViewAttached = new AtomicBoolean(false);
private boolean muteAudio = true;
private boolean useTCP = true;
private int networkCache = DEFAULT_NETWORK_CACHE;
private boolean autoReconnect = true;
private int reconnectDelay = DEFAULT_RECONNECT_DELAY;
private int maxReconnectAttempts = DEFAULT_MAX_RECONNECT_ATTEMPTS;
private final ExecutorService executorService = Executors.newCachedThreadPool();
private volatile Future<?> pendingConnectionTask;
private final Handler uiHandler = new Handler(Looper.getMainLooper());
private OnPlayStateListener stateListener;
public enum PlayerState {
IDLE, CONNECTING, PLAYING, PAUSED, ERROR, RECONNECTING, RELEASED
}
public interface OnPlayStateListener {
void onSuccess();
void onFails(String error);
void onRetrying(String message);
void onStateChanged(PlayerState newState);
}
public VlcRtspPlayer(Context context) {
this.context = context.getApplicationContext();
}
public void setVideoLayout(VLCVideoLayout videoLayout) {
uiHandler.post(() -> this.videoLayout = videoLayout);
}
public void play(String rtspUrl) {
if (isReleased) {
Log.w(TAG, "播放器已释放,无法播放");
return;
}
if (rtspUrl == null || rtspUrl.trim().isEmpty()) {
notifyError("RTSP地址为空");
return;
}
if (!rtspUrl.startsWith("rtsp://")) {
notifyError("RTSP地址格式错误");
return;
}
if (rtspUrl.equals(currentUrl) && isPlaying()) {
Log.d(TAG, "已经在播放相同的URL,跳过");
return;
}
stopInternal();
this.currentUrl = rtspUrl;
this.reconnectAttempts = 0;
updateState(PlayerState.CONNECTING);
pendingConnectionTask = executorService.submit(() -> {
try {
initAndPlaySync();
} catch (Exception e) {
if (!isReleased) {
Log.e(TAG, "异步播放任务异常", e);
notifyError("播放初始化失败");
}
}
});
}
public void playWithContinuousRetry(String rtspUrl) {
this.maxReconnectAttempts = Integer.MAX_VALUE;
this.autoReconnect = true;
play(rtspUrl);
}
public void stopContinuousRetry() {
this.maxReconnectAttempts = DEFAULT_MAX_RECONNECT_ATTEMPTS;
}
private void stopInternal() {
cancelPendingConnection();
if (isViewAttached.compareAndSet(true, false)) {
uiHandler.post(() -> {
if (mediaPlayer != null) {
try {
mediaPlayer.detachViews();
} catch (Exception e) {
Log.w(TAG, "detachViews 异常: " + e.getMessage());
}
}
});
}
if (mediaPlayer != null) {
try {
mediaPlayer.stop();
} catch (Exception e) {
// 忽略
}
}
}
private void cancelPendingConnection() {
if (pendingConnectionTask != null) {
pendingConnectionTask.cancel(true);
pendingConnectionTask = null;
}
}
public void pause() {
if (isReleased) return;
uiHandler.post(() -> {
if (mediaPlayer != null && playerState == PlayerState.PLAYING) {
mediaPlayer.pause();
updateState(PlayerState.PAUSED);
}
});
}
public void resume() {
if (isReleased) {
if (currentUrl != null) {
play(currentUrl);
}
return;
}
uiHandler.post(() -> {
if (mediaPlayer != null && playerState == PlayerState.PAUSED) {
mediaPlayer.play();
} else if (playerState == PlayerState.IDLE || playerState == PlayerState.ERROR) {
if (currentUrl != null) {
play(currentUrl);
}
}
});
}
public void stop() {
if (isReleased) return;
stopInternal();
updateState(PlayerState.IDLE);
}
public void forceReplay() {
reconnectAttempts = 0;
if (currentUrl != null) {
play(currentUrl);
}
}
public void release() {
if (isReleased) return;
isReleased = true;
updateState(PlayerState.RELEASED);
cancelPendingConnection();
uiHandler.removeCallbacksAndMessages(null);
if (isViewAttached.compareAndSet(true, false)) {
if (mediaPlayer != null) {
try {
mediaPlayer.detachViews();
} catch (Exception e) {
Log.w(TAG, "release detachViews 异常: " + e.getMessage());
}
}
}
final MediaPlayer player = mediaPlayer;
final LibVLC vlc = libVLC;
mediaPlayer = null;
libVLC = null;
videoLayout = null;
executorService.execute(() -> {
if (player != null) {
try {
player.stop();
player.release();
} catch (Exception e) {
Log.w(TAG, "release player 异常: " + e.getMessage());
}
}
if (vlc != null) {
try {
vlc.release();
} catch (Exception e) {
Log.w(TAG, "release libVLC 异常: " + e.getMessage());
}
}
});
executorService.shutdownNow();
currentUrl = null;
Log.d(TAG, "播放器已释放");
}
public boolean isPlaying() {
return !isReleased && mediaPlayer != null && mediaPlayer.isPlaying();
}
public boolean isActive() {
return !isReleased && (playerState == PlayerState.CONNECTING ||
playerState == PlayerState.PLAYING ||
playerState == PlayerState.RECONNECTING);
}
public String getCurrentUrl() {
return currentUrl;
}
public PlayerState getPlayerState() {
return playerState;
}
public void setOnPlayStateListener(OnPlayStateListener listener) {
this.stateListener = listener;
}
public void setMuteAudio(boolean mute) { this.muteAudio = mute; }
public void setUseTCP(boolean useTCP) { this.useTCP = useTCP; }
public void setNetworkCache(int cacheMs) { this.networkCache = cacheMs; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
public void setReconnectDelay(int delayMs) { this.reconnectDelay = delayMs; }
public void setMaxReconnectAttempts(int maxAttempts) { this.maxReconnectAttempts = maxAttempts; }
// ========== 内部实现 ==========
private void initAndPlaySync() {
if (isReleased) return;
if (mediaPlayer != null) {
try {
mediaPlayer.stop();
} catch (Exception e) {
// 忽略
}
}
if (!initVLCSafely()) {
if (!isReleased) {
notifyError("VLC初始化失败");
}
return;
}
try {
MediaPlayer player = new MediaPlayer(libVLC);
if (isReleased) {
player.release();
return;
}
this.mediaPlayer = player;
player.setEventListener(event -> {
if (!isReleased) {
uiHandler.post(() -> handlePlayerEvent(event));
}
});
startPlaybackInternal();
} catch (Exception e) {
if (!isReleased) {
Log.e(TAG, "MediaPlayer创建失败", e);
notifyError("播放器创建失败");
}
}
}
private boolean initVLCSafely() {
try {
ArrayList<String> options = getLibVLCOptions();
libVLC = new LibVLC(context, options);
return true;
} catch (Exception e1) {
Log.w(TAG, "带选项初始化失败,尝试简化选项", e1);
try {
ArrayList<String> simpleOptions = new ArrayList<>();
simpleOptions.add("--no-audio");
simpleOptions.add("--network-caching=" + networkCache);
libVLC = new LibVLC(context, simpleOptions);
return true;
} catch (Exception e2) {
Log.e(TAG, "所有初始化尝试都失败", e2);
return false;
}
}
}
private ArrayList<String> getLibVLCOptions() {
ArrayList<String> options = new ArrayList<>();
if (muteAudio) options.add("--no-audio");
options.add("--network-caching=" + networkCache);
options.add("--rtsp-tcp");
options.add("--live-caching=" + networkCache);
options.add("--ipv4-timeout=" + CONNECTION_TIMEOUT_MS);
options.add("--no-stats");
options.add("--no-osd");
return options;
}
private void startPlaybackInternal() {
if (isReleased || mediaPlayer == null || libVLC == null || currentUrl == null) {
return;
}
try {
Media media = new Media(libVLC, android.net.Uri.parse(currentUrl));
media.addOption(":network-caching=" + networkCache);
media.addOption(":ipv4-timeout=" + CONNECTION_TIMEOUT_MS);
if (useTCP) media.addOption(":rtsp-tcp");
mediaPlayer.setMedia(media);
uiHandler.post(() -> {
if (isReleased || mediaPlayer == null) {
media.release();
return;
}
if (videoLayout == null) {
Log.e(TAG, "videoLayout 为空,无法附加视图");
media.release();
notifyError("视频布局未初始化");
return;
}
if (isViewAttached.compareAndSet(true, false)) {
try {
mediaPlayer.detachViews();
} catch (Exception e) {
Log.w(TAG, "预分离视图异常: " + e.getMessage());
}
}
if (!isVideoLayoutReady(videoLayout)) {
Log.w(TAG, "videoLayout 未准备好,延迟100ms重试");
uiHandler.postDelayed(() -> {
if (!isReleased && mediaPlayer != null) {
attachViewsWithRetry(media, 0);
} else {
media.release();
}
}, 100);
return;
}
attachViewsWithRetry(media, 0);
});
} catch (Exception e) {
if (!isReleased) {
notifyError("播放失败");
}
}
}
private boolean isVideoLayoutReady(VLCVideoLayout layout) {
if (layout == null) return false;
return layout.getChildCount() > 0 || (layout.getWidth() > 0 && layout.getHeight() > 0);
}
private void attachViewsWithRetry(Media media, int retryCount) {
if (isReleased || mediaPlayer == null || videoLayout == null) {
if (media != null) media.release();
return;
}
if (retryCount > 3) {
Log.e(TAG, "附加视图重试次数超限");
media.release();
notifyError("视频视图初始化失败");
return;
}
try {
if (!isVideoLayoutReady(videoLayout)) {
Log.w(TAG, "videoLayout 仍未准备好,第" + (retryCount + 1) + "次重试");
uiHandler.postDelayed(() -> attachViewsWithRetry(media, retryCount + 1), 100);
return;
}
mediaPlayer.attachViews(videoLayout, null, false, false);
isViewAttached.set(true);
mediaPlayer.play();
media.release();
Log.d(TAG, "视图附加成功");
} catch (IllegalStateException e) {
Log.e(TAG, "attachViews 失败 (IllegalState): " + e.getMessage());
retryAttachViews(media, retryCount);
} catch (NullPointerException e) {
Log.e(TAG, "attachViews 失败 (NullPointer): " + e.getMessage());
uiHandler.postDelayed(() -> attachViewsWithRetry(media, retryCount + 1), 100);
}
}
private void retryAttachViews(Media media, int retryCount) {
if (isReleased || mediaPlayer == null || videoLayout == null) {
if (media != null) media.release();
return;
}
uiHandler.postDelayed(() -> {
if (isReleased || mediaPlayer == null || videoLayout == null) {
if (media != null) media.release();
return;
}
try {
mediaPlayer.release();
mediaPlayer = new MediaPlayer(libVLC);
mediaPlayer.setEventListener(event -> {
if (!isReleased) {
uiHandler.post(() -> handlePlayerEvent(event));
}
});
if (currentUrl != null && media != null) {
Media newMedia = new Media(libVLC, android.net.Uri.parse(currentUrl));
newMedia.addOption(":network-caching=" + networkCache);
mediaPlayer.setMedia(newMedia);
newMedia.release();
}
if (media != null) media.release();
mediaPlayer.attachViews(videoLayout, null, false, false);
isViewAttached.set(true);
mediaPlayer.play();
} catch (Exception e) {
Log.e(TAG, "重试 attachViews 失败: " + e.getMessage());
if (media != null) media.release();
notifyError("视频视图初始化失败");
}
}, 100);
}
private void handlePlayerEvent(MediaPlayer.Event event) {
if (isReleased) return;
switch (event.type) {
case MediaPlayer.Event.Opening:
updateState(PlayerState.CONNECTING);
break;
case MediaPlayer.Event.Playing:
updateState(PlayerState.PLAYING);
reconnectAttempts = 0;
if (stateListener != null) {
uiHandler.post(() -> stateListener.onSuccess());
}
break;
case MediaPlayer.Event.Paused:
updateState(PlayerState.PAUSED);
break;
case MediaPlayer.Event.Stopped:
if (autoReconnect && !isReleased) {
updateState(PlayerState.RECONNECTING);
scheduleReconnect();
}
break;
case MediaPlayer.Event.EndReached:
if (autoReconnect && !isReleased) {
updateState(PlayerState.RECONNECTING);
scheduleReconnect();
}
break;
case MediaPlayer.Event.EncounteredError:
handleError("播放错误");
if (autoReconnect && reconnectAttempts < maxReconnectAttempts && !isReleased) {
updateState(PlayerState.RECONNECTING);
scheduleReconnect();
} else {
updateState(PlayerState.ERROR);
}
break;
}
}
private void scheduleReconnect() {
if (playerState == PlayerState.RECONNECTING && reconnectAttempts > 0) {
Log.w(TAG, "已经在重连中,跳过");
return;
}
if (isReleased || reconnectAttempts >= maxReconnectAttempts) {
if (!isReleased) {
notifyError("重连次数已达上限");
}
return;
}
reconnectAttempts++;
if (stateListener != null) {
stateListener.onRetrying("连接中断,正在恢复 " + reconnectAttempts + "...");
}
uiHandler.postDelayed(() -> {
if (!isReleased && (playerState == PlayerState.RECONNECTING || playerState == PlayerState.ERROR)) {
if (isViewAttached.compareAndSet(true, false)) {
if (mediaPlayer != null) {
try {
mediaPlayer.detachViews();
} catch (Exception e) {
Log.w(TAG, "重连前 detachViews 异常: " + e.getMessage());
}
}
}
pendingConnectionTask = executorService.submit(this::restartPlaybackSync);
}
}, reconnectDelay);
}
private void restartPlaybackSync() {
if (isReleased) return;
if (mediaPlayer != null) {
try {
mediaPlayer.stop();
mediaPlayer.release();
} catch (Exception e) {
// 忽略
}
mediaPlayer = null;
}
if (libVLC != null && currentUrl != null && !isReleased) {
try {
mediaPlayer = new MediaPlayer(libVLC);
mediaPlayer.setEventListener(event -> {
if (!isReleased) {
uiHandler.post(() -> handlePlayerEvent(event));
}
});
startPlaybackInternal();
} catch (Exception e) {
if (!isReleased) {
notifyError("重连失败");
}
}
}
}
private void handleError(String errorInfo) {
if (stateListener != null) stateListener.onFails(errorInfo);
}
private void notifyError(String message) {
updateState(PlayerState.ERROR);
if (stateListener != null) stateListener.onFails(message);
}
public VLCVideoLayout getVideoLayout() {
return videoLayout;
}
private void updateState(PlayerState newState) {
if (this.playerState == newState) return;
this.playerState = newState;
if (stateListener != null) {
stateListener.onStateChanged(newState);
}
}
}
五、Activity使用示例
java
package com.example.rtspplayerdemo.vlcvideo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.example.rtspplayerdemo.R;
import org.videolan.libvlc.util.VLCVideoLayout;
/**
* RTSP视频播放页面 - 单路示例
*/
public class VlcRtspActivity extends AppCompatActivity {
private static final String TAG = "VlcRtspActivity";
private VLCVideoLayout videoLayout;
private ProgressBar loadingIndicator;
private TextView tvError;
private TextView tvStatus;
private VlcRtspPlayer vlcPlayer;
private String rtspUrl = "rtsp://admin:password@192.168.1.100:554/stream";
private static final int MSG_PLAY_SUCCESS = 1001;
private static final int MSG_PLAY_FAIL = 1002;
private static final int MSG_PLAY_SUCCESS_BACKUP = 1003;
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MSG_PLAY_SUCCESS:
loadingIndicator.setVisibility(View.GONE);
tvError.setVisibility(View.GONE);
tvStatus.setVisibility(View.GONE);
break;
case MSG_PLAY_FAIL:
loadingIndicator.setVisibility(View.GONE);
tvStatus.setVisibility(View.GONE);
tvError.setVisibility(View.VISIBLE);
tvError.setText("连接失败,点击重试");
break;
case MSG_PLAY_SUCCESS_BACKUP:
if (vlcPlayer != null && vlcPlayer.isPlaying()
&& loadingIndicator.getVisibility() == View.VISIBLE) {
Log.d(TAG, "备用机制:视频已在播放,隐藏加载提示");
loadingIndicator.setVisibility(View.GONE);
tvStatus.setVisibility(View.GONE);
}
break;
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 全屏+常亮
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_vlc_rtsp);
initViews();
initPlayer();
}
private void initViews() {
videoLayout = findViewById(R.id.video_layout);
loadingIndicator = findViewById(R.id.loading_indicator);
tvError = findViewById(R.id.tv_error);
tvStatus = findViewById(R.id.tv_status);
tvError.setOnClickListener(v -> {
if (vlcPlayer != null) {
tvError.setVisibility(View.GONE);
vlcPlayer.forceReplay();
}
});
}
private void initPlayer() {
if (!validateUrl(rtspUrl)) return;
vlcPlayer = new VlcRtspPlayer(this);
vlcPlayer.setMuteAudio(true);
vlcPlayer.setUseTCP(true);
vlcPlayer.setNetworkCache(500);
vlcPlayer.setAutoReconnect(true);
vlcPlayer.setReconnectDelay(3000);
vlcPlayer.setMaxReconnectAttempts(5);
vlcPlayer.setOnPlayStateListener(new VlcRtspPlayer.OnPlayStateListener() {
@Override
public void onSuccess() {
uiHandler.sendEmptyMessage(MSG_PLAY_SUCCESS);
}
@Override
public void onFails(String error) {
uiHandler.removeMessages(MSG_PLAY_FAIL);
uiHandler.sendEmptyMessageDelayed(MSG_PLAY_FAIL, 5000);
}
@Override
public void onRetrying(String message) {
runOnUiThread(() -> {
tvStatus.setText(message);
tvStatus.setVisibility(View.VISIBLE);
});
}
@Override
public void onStateChanged(VlcRtspPlayer.PlayerState newState) {
Log.d(TAG, "状态: " + newState);
}
});
// 关键:等待布局就绪
videoLayout.getViewTreeObserver().addOnGlobalLayoutListener(
new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (videoLayout.getWidth() > 0 && videoLayout.getHeight() > 0) {
videoLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
vlcPlayer.setVideoLayout(videoLayout);
playVideo();
}
}
});
}
private boolean validateUrl(String url) {
if (url == null || url.trim().isEmpty()) {
showError("RTSP地址为空");
return false;
}
if (!url.startsWith("rtsp://")) {
showError("RTSP地址格式错误");
return false;
}
return true;
}
private void playVideo() {
loadingIndicator.setVisibility(View.VISIBLE);
tvStatus.setText("正在连接...");
tvStatus.setVisibility(View.VISIBLE);
tvError.setVisibility(View.GONE);
vlcPlayer.playWithContinuousRetry(rtspUrl);
// 备用机制:3秒后检查状态
uiHandler.removeMessages(MSG_PLAY_SUCCESS_BACKUP);
uiHandler.sendEmptyMessageDelayed(MSG_PLAY_SUCCESS_BACKUP, 3000);
}
private void showError(String message) {
tvError.setText(message);
tvError.setVisibility(View.VISIBLE);
loadingIndicator.setVisibility(View.GONE);
tvStatus.setVisibility(View.GONE);
}
@Override
protected void onResume() {
super.onResume();
if (vlcPlayer != null) {
if (!vlcPlayer.isActive()) {
if (videoLayout.getWidth() > 0 && videoLayout.getHeight() > 0) {
vlcPlayer.setVideoLayout(videoLayout);
playVideo();
}
} else {
vlcPlayer.resume();
}
}
}
@Override
protected void onPause() {
super.onPause();
if (vlcPlayer != null) {
vlcPlayer.stopContinuousRetry();
vlcPlayer.pause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
uiHandler.removeCallbacksAndMessages(null);
if (vlcPlayer != null) {
vlcPlayer.release();
vlcPlayer = null;
}
}
}
六、布局文件
java
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<org.videolan.libvlc.util.VLCVideoLayout
android:id="@+id/video_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/loading_indicator"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:indeterminateTint="@android:color/white" />
<TextView
android:id="@+id/tv_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="80dp"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@android:color/white"
android:textSize="16sp"
android:visibility="gone" />
</FrameLayout>
七、关键踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
attachViews崩溃 |
布局未测量完成 | 用ViewTreeObserver等待宽高>0 |
| 有声音没画面 | 硬解码失败或丢包 | 强制TCP传输,或切换软解 |
| 重连后黑屏 | 未重新attachViews |
重连前先detachViews() |
| Activity退出后闪退 | 播放器未释放 | onDestroy中调用release() |
| 多路播放卡顿 | 缓存太大或解码器冲突 | 减小缓存,或降低分辨率 |
八、多路播放扩展
如需同时播放多路(如4分屏),创建多个VlcRtspPlayer实例即可:
java
// 左右双路示例
VlcRtspPlayer leftPlayer = new VlcRtspPlayer(this);
VlcRtspPlayer rightPlayer = new VlcRtspPlayer(this);
leftPlayer.setVideoLayout(leftVideoLayout);
rightPlayer.setVideoLayout(rightVideoLayout);
leftPlayer.play(url1);
rightPlayer.play(url2);
注意: 多路播放对CPU和内存要求较高,建议根据设备性能限制路数(平板一般4路,手机2路)。
九、结语
libVLC 虽然包体大,但在监控场景下的稳定性确实值得信赖。本文的封装类已在多个工业项目上线运行,处理了各种国产摄像头的兼容性问题。如有疑问欢迎评论区交流。