Android libVLC 3.5.1 实现 RTSP 视频播放完整方案

一、前言

最近在做工业平板监控项目,需要同时播放多路 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 虽然包体大,但在监控场景下的稳定性确实值得信赖。本文的封装类已在多个工业项目上线运行,处理了各种国产摄像头的兼容性问题。如有疑问欢迎评论区交流。

相关推荐
sjmaysee2 小时前
PostgreSQL异常:An IO error occurred while sending to the backend
java
liuyao_xianhui2 小时前
优选算法_岛屿数量_floodfill算法)_bfs_C++
java·开发语言·数据结构·c++·算法·链表·宽度优先
321茄子2 小时前
idea 撤销吴提交代码
java·ide·intellij-idea
蜜獾云2 小时前
windows java jar 包后台运行
java
EasyDSS2 小时前
私有化部署视频高清直播点播系统/私有化视频会议解决方案EasyDSS在政务融媒体平台视频直播/视频会议场景中的应用解析
音视频·媒体·政务
€8112 小时前
Java入门级教程29——Spring Cloud:Eureka 注册发现 + MySQL 数据交互 + 负载均衡
java·开发语言·mysql·spring cloud·eureka·负载均衡
澄风2 小时前
深入理解Java SPI:机制、原理、实战与开源框架应用全解析
java·开发语言·开源
希望永不加班2 小时前
SpringBoot 接口测试:Postman 与 JUnit 5 实战
java·spring boot·后端·junit·postman
AirDroid_cn2 小时前
手机上看的网页,怎样自动在荣耀 MagicOS 10 平板上接着打开?
android·智能手机·电脑·荣耀手机