Android uses VLC to play video, realize screenshot and record function

VLC is a very powerful open source media player developed and maintained by the VideoLAN organization. It was originally developed for school projects, but has now become one of the most popular media players worldwide.

VLC has the following main features:

  1. Multi-platform support: VLC supports almost all mainstream operating systems, including Windows, macOS, Linux, iOS, and Android. This means you can use VLC to play media on almost any device.

  2. Multi-format support: VLC supports a large number of video and audio formats, including MP4, MKV, AVI, MOV, OGG, FLAC, TS, M2TS, WV, AAC and other video formats, MPEG-1/2, MPEG-4, H.263 , H.264, H.265/HEVC, VP8, VP9, ​​AV1 and other video encoding formats, and MP3, AAC, Vorbis, FLAC, ALAC, WMA, MIDI and other audio encoding formats. In addition, VLC also supports various network streaming formats, such as HTTP, RTSP, HLS, Dash, Smooth Streaming, etc.

  3. Advanced functions: In addition to basic playback functions, VLC also provides a series of advanced functions, such as playlist management, audio and video effect adjustment, subtitle support, streaming media server and client, media transcoding, etc.

  4. Open source and free: VLC is completely open source, which means that anyone can view and modify its source code. At the same time, VLC is completely free, without any advertisements or in-app purchases.

VLC's Android library (libVLC) provides a complete set of APIs that developers can use to play video and audio in Android applications. In addition to basic playback control, libVLC also provides some advanced features, such as video filters, subtitle support, media metadata acquisition, etc.

Below, I will introduce how to use the VLC library to play RTSP video streams in Android. vlc-android github

1. First, add the VLC library dependency:

In your Android project's build.gradlefiles, add the following dependencies:

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

Synchronize the project to import the VLC library.

2. Create a layout file:

res/layoutCreate a layout file in the directory, for example , activity_main.xmland add a TextureView for playing video: (Why use TextureView? I will talk about it below)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

       <TextureView
          android:id="@+id/video_view"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />

</RelativeLayout>

 3. Encapsulate a player tool class


/**
 * 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<String> 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. Called in 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) {

    }

}

Ok, now the video playback functionality is complete

However, in actual development, you may also encounter the function of taking screenshots and recording videos, which are introduced one by one below:

1. Video recording

For video recording and saving to local mp4 files, the vlc library has provided a MediaPlayer.record() method.

We add the following code in 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);
    }
}

Apply for permissions in 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. Video screenshot

In some older versions there are MediaPlayer.takeSnapshot()methods that can be used directly.

In recent versions of the VLC library, MediaPlayer.takeSnapshot()the method was removed. The reason could be that the feature's implementation in a cross-platform scenario has issues or poor performance, or that the feature needs to be refactored to accommodate other changes to the library.

However, in newer versions of VLC (like VLC 4.x), a new screenshot API has been introduced. In this version, you can use libvlc_video_take_snapshot()the function to capture the current frame and save it as a file. The signature of this function is as follows:

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 );

The function accepts the following parameters:

  • p_mi: A libvlc_media_player_tpointer to the media player instance to capture the current frame.
  • num: The serial number of the video track to be captured.
  • psz_filepath: A C string specifying the file path to save the screenshot.
  • i_width: The width of the screenshot to save. Can be set to 0 to use the width of the source video.
  • i_height: The height of the screenshot to be saved. Can be set to 0 to use the height of the source video.

If the library does not provide a direct API, you need to download the vlc source code, compile it yourself, and write JNI code to access the underlying libVLC library. But please note that this method requires a certain understanding of the underlying libVLC library and JNI programming.

For this method, I uploaded a source code, which contains a self-compiled library to support the screenshot function: source code

Now that the new version has removed this method, another solution is to use TextureView for screenshots. (SurfaceView's double buffering mechanism, can't cut the picture)

Add the following code in 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;
    }

The videoWidth and videoHeight inside are obtained after the MediaPlayer.Event.Vout event occurs.

In MainActivity call:

     @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, call it a day.

Follow up

When using the vlc player, it is often encountered that the MediaPlayer.stop() method freezes or causes the application to crash. The likely reason is that stop()the video is buffering when the method is called. At this time, the calling stop()method will wait for the buffering to be completed before stopping the player. If the buffering time is too long, it may cause freeze or ANR. You can use an asynchronous thread to stop the player, avoiding waiting for buffering in the main thread.

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

Guess you like

Origin blog.csdn.net/gs12software/article/details/130618598