Android는 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)는 개발자가 Android 애플리케이션에서 비디오 및 오디오를 재생하는 데 사용할 수 있는 완전한 API 세트를 제공합니다. 기본 재생 제어 외에도 libVLC는 비디오 필터, 자막 지원, 미디어 메타데이터 획득 등과 같은 일부 고급 기능도 제공합니다.

아래에서는 VLC 라이브러리를 사용하여 Android에서 RTSP 비디오 스트림을 재생하는 방법을 소개합니다. vlc-안드로이드 github

1. 먼저 VLC 라이브러리 종속성을 추가합니다.

Android 프로젝트 build.gradle파일에서 다음 종속 항목을 추가합니다.

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

프로젝트를 동기화하여 VLC 라이브러리를 가져옵니다.

2. 레이아웃 파일을 만듭니다.

res/layout예를 들어 디렉토리에 레이아웃 파일을 만들고 비디오 재생을 위한 TextureView를 추가합니다 . activity_main.xml(TextureView를 사용하는 이유는 아래에서 설명하겠습니다.)

<?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. 플레이어 도구 클래스 캡슐화


/**
 * 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. 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;

        }
    }

알았어, 그만둬.

후속 조치

vlc 플레이어를 사용할 때 MediaPlayer.stop() 메서드가 정지되거나 응용 프로그램이 충돌하는 경우가 종종 있습니다. 가능한 이유는 stop()메서드가 호출될 때 비디오가 버퍼링 중이기 때문입니다. 이때 호출하는 stop()메서드는 버퍼링이 완료될 때까지 기다린 후 플레이어를 중지하며, 버퍼링 시간이 너무 길면 정지 또는 ANR이 발생할 수 있습니다. 비동기 스레드를 사용하여 기본 스레드에서 버퍼링을 기다리지 않고 플레이어를 중지할 수 있습니다.

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

추천

출처blog.csdn.net/gs12software/article/details/130618598