安卓使用VLC播放视频,实现截图和录制功能

VLC是一款非常强大的开源媒体播放器,由VideoLAN组织开发和维护。它最初是为学校项目开发的,但现在已经成为全球最流行的媒体播放器之一。

VLC具有以下几个主要特点:

  1. 多平台支持:VLC支持几乎所有主流的操作系统,包括Windows、macOS、Linux、iOS和Android。这意味着你可以在几乎任何设备上使用VLC播放媒体。

  2. 多格式支持:VLC支持大量的视频和音频格式,包括MP4, MKV, AVI, MOV, OGG, FLAC, TS, M2TS, WV, AAC等视频格式,MPEG-1/2, MPEG-4, H.263, H.264, H.265/HEVC, VP8, VP9, AV1等视频编码格式,以及MP3, AAC, Vorbis, FLAC, ALAC, WMA, MIDI等音频编码格式。此外,VLC还支持各种网络流格式,如HTTP, RTSP, HLS, Dash, Smooth Streaming等。

  3. 高级功能:除了基本的播放功能,VLC还提供了一系列高级功能,如播放列表管理、音频和视频效果调整、字幕支持、流媒体服务器和客户端、媒体转码等。

  4. 开源和免费:VLC是完全开源的,这意味着任何人都可以查看和修改它的源代码。同时,VLC是完全免费的,没有任何广告或者内购。

VLC的Android库(libVLC)提供了一套完整的API,开发者可以使用这套API在Android应用中播放视频和音频。除了基本的播放控制,libVLC还提供了一些高级功能,如视频滤镜、字幕支持、媒体元数据获取等。

下面,我将介绍在安卓中如何使用VLC库进行播放RTSP视频流。vlc-android github

1.首先,添加VLC库依赖:

在你的Android项目的build.gradle文件中,添加以下依赖项:

dependencies {
    implementation 'org.videolan.android:libvlc-all:4.0.0-eap9'
}

同步项目以导入VLC库。

2.创建一个布局文件:

res/layout目录下创建一个布局文件,例如activity_main.xml,并添加一个TextureView用于播放视频:(为什么使用TextureView?下面会讲到)




       

 3.封装一个播放器工具类


/**
 * VLC播放视频工具类
 */
public class VLCPlayer implements MediaPlayer.EventListener{

    private LibVLC libVLC;
    private MediaPlayer mediaPlayer;

    private int videoWidth = 0;  //视频宽度
    private int videoHeight = 0; //视频高度

    public VLCPlayer(Context context) {
        ArrayList options = new ArrayList<>();
        options.add("--no-drop-late-frames"); //防止掉帧
        options.add("--no-skip-frames"); //防止掉帧
        options.add("--rtsp-tcp");//强制使用TCP方式
        options.add("--avcodec-hw=any"); //尝试使用硬件加速
        options.add("--live-caching=0");//缓冲时长

        libVLC = new LibVLC(context, options);
        mediaPlayer = new MediaPlayer(libVLC);
        mediaPlayer.setEventListener(this);
    }

    /**
     * 设置播放视图
     * @param textureView
     */
    public void setVideoSurface(TextureView textureView) {
        mediaPlayer.getVLCVout().setVideoSurface(textureView.getSurfaceTexture());
        mediaPlayer.getVLCVout().setWindowSize(textureView.getWidth(), textureView.getHeight());
        textureView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                                       int oldLeft, int oldTop, int oldRight, int oldBottom) {
                // 获取新的宽度和高度
                int newWidth = right - left;
                int newHeight = bottom - top;
                // 设置VLC播放器的宽高参数
                mediaPlayer.getVLCVout().setWindowSize(newWidth, newHeight);
            }
        });

        mediaPlayer.getVLCVout().attachViews();

    }

    /**
     * 设置播放地址
     * @param url
     */
    public void setDataSource(String url) {
        try {
            Media media = new Media(libVLC, Uri.parse(url));
            mediaPlayer.setMedia(media);
        }catch (Exception e){
            Log.e("VLCPlayer",e.getMessage(),e);
        }
    }

    /**
     * 播放
     */
    public void play() {
        if (mediaPlayer == null) {
            return;
        }
        mediaPlayer.play();
    }

    /**
     * 暂停
     */
    public void pause() {
        if (mediaPlayer == null) {
            return;
        }
        mediaPlayer.pause();
    }

    /**
     * 停止播放
     */
    public void stop() {
        if (mediaPlayer == null) {
            return;
        }
        mediaPlayer.stop();
    }
    

    /**
     * 释放资源
     */
    public void release() {
        if(mediaPlayer!=null) {
            mediaPlayer.release();
        }
        if(libVLC!=null) {
            libVLC.release();
        }
    }


    @Override
    public void onEvent(MediaPlayer.Event event) {
        switch (event.type) {
            case MediaPlayer.Event.Buffering:
                // 处理缓冲事件
                if (callback != null) {
                    callback.onBuffering(event.getBuffering());
                }
                break;
            case MediaPlayer.Event.EndReached:
                // 处理播放结束事件
                if (callback != null) {
                    callback.onEndReached();
                }
                break;
            case MediaPlayer.Event.EncounteredError:
                // 处理播放错误事件
                if (callback != null) {
                    callback.onError();
                }
                break;
            case MediaPlayer.Event.TimeChanged:
                // 处理播放进度变化事件
                if (callback != null) {
                    callback.onTimeChanged(event.getTimeChanged());
                }
                break;
            case MediaPlayer.Event.PositionChanged:
                // 处理播放位置变化事件
                if (callback != null) {
                    callback.onPositionChanged(event.getPositionChanged());
                }
                break;
            case MediaPlayer.Event.Vout:
                //在视频开始播放之前,视频的宽度和高度可能还没有被确定,因此我们需要在MediaPlayer.Event.Vout事件发生后才能获取到正确的宽度和高度
                IMedia.VideoTrack vtrack = (IMedia.VideoTrack) mediaPlayer.getSelectedTrack(Media.Track.Type.Video);
                videoWidth = vtrack.width;
                videoHeight = vtrack.height;
                break;
        }
    }
    

    private VLCPlayerCallback callback;

    public void setCallback(VLCPlayerCallback callback) {
        this.callback = callback;
    }

    public interface VLCPlayerCallback {
        void onBuffering(float bufferPercent);
        void onEndReached();
        void onError();
        void onTimeChanged(long currentTime);
        void onPositionChanged(float position);
    }

}

4.在MainActivity调用

public class MainActivity extends AppCompatActivity implements VLCPlayer.VLCPlayerCallback {
    private static final int REQUEST_STORAGE_PERMISSION = 1;

    private static final String rtspUrl = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4";

    private ProgressBar progressBar;

    private TextureView textureView;

    private VLCPlayer vlcPlayer;
    private boolean isRecording;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        textureView = findViewById(R.id.video_view);

        progressBar = findViewById(R.id.progressBar);

        textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
                initVLCPlayer();
            }

            @Override
            public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {

            }
        });

    }

    private void initVLCPlayer() {
        vlcPlayer = new VLCPlayer(this);
        vlcPlayer.setVideoSurface(textureView);
        vlcPlayer.setDataSource(rtspUrl);
        vlcPlayer.setCallback(this);
        vlcPlayer.play();
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        vlcPlayer.release();
    }


    @Override
    public void onBuffering(float bufferPercent) {
        if (bufferPercent >= 100) {
            progressBar.setVisibility(View.GONE);
        } else{
            progressBar.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public void onEndReached() {
        progressBar.setVisibility(View.GONE);
        Toast.makeText(this, "播放结束",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onError() {
        progressBar.setVisibility(View.GONE);
        Toast.makeText(this, "播放出错",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onTimeChanged(long currentTime) {

    }

    @Override
    public void onPositionChanged(float position) {

    }

}

好了,现在视频播放功能已经完成了

然而,在实际开发中,可能还会遇到要对视频进行截图和录制的功能,下面一一介绍:

1.视频录制

针对视频录制并保存到本地mp4文件,这个vlc库已经提供了一个MediaPlayer.record()方法。

我们在VLCPlayer添加如下代码

public class VLCPlayer{

    //其他省略
    
    /**
     * 录制视频
     * @param filePath 保存文件的路径
     */
    public boolean startRecording(String filePath) {
        if (mediaPlayer == null) {
            return false;
        }
        return mediaPlayer.record(filePath);
    }

    /**
     * 停止录制
     */
    public void stopRecording() {
        if (mediaPlayer == null) {
            return;
        }
        mediaPlayer.record(null);
    }
}

在MainActivity中申请权限

private static final int REQUEST_STORAGE_PERMISSION = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    requestStoragePermission();
}

private void requestStoragePermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_STORAGE_PERMISSION);
    } else {
        initVLCPlayer();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == REQUEST_STORAGE_PERMISSION) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            initVLCPlayer();
        } else {
            Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
            finish();
        }
    }
}

2.视频截图

在一些老版本中有MediaPlayer.takeSnapshot()方法可直接使用。

在VLC库的最近的版本中,MediaPlayer.takeSnapshot()方法被移除。原因可能是该功能在跨平台场景下的实现存在问题或性能低下,或者这个功能需要重构以适应库的其他更改。

然而,在VLC的新版本(如VLC 4.x)中,已经引入了新的截图API。在这个版本中,你可以使用libvlc_video_take_snapshot()函数截取当前帧并保存为文件。这个函数的签名如下:

int libvlc_video_take_snapshot( libvlc_media_player_t *p_mi, unsigned num, const char *psz_filepath, unsigned int i_width, unsigned int i_height );

该函数接受以下参数:

  • p_mi:一个libvlc_media_player_t指针,指向要截取当前帧的媒体播放器实例。
  • num:要截取的视频轨道的序号。
  • psz_filepath:一个C字符串,指定要保存截图的文件路径。
  • i_width:要保存的截图的宽度。可以设置为0以使用源视频的宽度。
  • i_height:要保存的截图的高度。可以设置为0以使用源视频的高度。

如果库没有提供直接的API,你需要下载vlc源码,自行编译,编写JNI代码来访问底层libVLC库。但请注意,这种方法需要对底层libVLC库以及JNI编程有一定了解。

针对这个方式,我上传了一份源码,里面包含了一个自编译的库支持截图功能:源码

既然新版本它删除了这个方法,另一个解决方案是使用TextureView进行截图。(SurfaceView的双缓冲机制,截不了图)

在VLCPlayer中添加如下代码:

/**
     * 截图保存
     * @param textureView
     */
    public boolean takeSnapShot(TextureView textureView,String path) {
        if(videoHeight == 0 || videoWidth == 0){
            return false;
        }
        Bitmap snapshot = textureView.getBitmap();
        if (snapshot != null) {
            // 获取TextureView的尺寸和视频的尺寸
            int viewWidth = textureView.getWidth();
            int viewHeight = textureView.getHeight();

            // 计算视频在TextureView中的实际显示区域
            float viewAspectRatio = (float) viewWidth / viewHeight;
            float videoAspectRatio = (float) videoWidth / videoHeight;

            int left, top, width, height;
            if (viewAspectRatio > videoAspectRatio) {
                // 视频在TextureView中是上下居中显示的
                width = viewWidth; // 宽度为屏幕宽度
                height = viewWidth * videoHeight / videoWidth; // 计算对应的高度
                left = 0; // 起始位置为左边
                top = (viewHeight - height) / 2; // 计算上边距,保证视频在TextureView中居中
            } else {
                // 视频在TextureView中是左右居中显示的
                width = viewWidth;
                height = viewWidth * videoHeight / videoWidth;
                left = 0;
                top = (viewHeight - height) / 2;
            }

            // 截取视频的实际显示区域
            Bitmap croppedSnapshot = Bitmap.createBitmap(snapshot, left, top, width, height);

            try {
                File snapshotFile = new File(path, "snapshot.jpg");
                FileOutputStream outputStream = new FileOutputStream(snapshotFile);
                croppedSnapshot.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                outputStream.close();

            } catch (IOException e) {
                Log.e("VlcPlayer",e.getMessage(), e);
                return false;
            }
        }
        return true;
    }

里面的videoWidth和videoHeight是我们从MediaPlayer.Event.Vout事件发生后获取到的。

在MainActivity调用:

     @Override
     public void onClick(View view) {
        String sdcardPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();

        switch (view.getId()) {
            case R.id.tv_takesnap:
                new Thread(() -> {
                    vlcPlayer.takeSnapShot(textureView, sdcardPath);
                }).start();
                Toast.makeText(MainActivity.this, "截图完成", Toast.LENGTH_SHORT).show();
                break;

            case R.id.tv_record:
                if (!isRecording) {
                    if (vlcPlayer.startRecording(sdcardPath)) {
                        Toast.makeText(MainActivity.this, "录制开始", Toast.LENGTH_SHORT).show();
                        tvRecord.setText("停止");
                        isRecording = true;
                    }
                } else {
                    vlcPlayer.stopRecording();
                    Toast.makeText(MainActivity.this, "录制结束", Toast.LENGTH_SHORT).show();
                    isRecording = false;
                    tvRecord.setText("录制");
                }
                break;

        }
    }

OK,打完收工。

后续

在使用vlc播放器时,经常会遇到MediaPlayer.stop()方法卡顿或者引起应用崩溃的问题。可能的原因是在调用stop()方法时,视频正在缓冲中。此时调用stop()方法会等待缓冲完成后才停止播放器,如果缓冲时间过长,容易引起卡顿或者ANR。可以使用异步线程来停止播放器,避免在主线程中等待缓冲。

new Thread(new Runnable() {
    @Override
    public void run() {
        mMediaPlayer.stop();
    }
}).start();

你可能感兴趣的:(音视频开发,音视频,vlc,RTSP)