VLC は、VideoLAN 組織によって開発および保守されている非常に強力なオープン ソース メディア プレーヤーです。元々は学校のプロジェクト用に開発されましたが、今では世界中で最も人気のあるメディア プレーヤーの 1 つになりました。
VLC には次の主な機能があります。
-
マルチプラットフォームのサポート: VLC は、Windows、macOS、Linux、iOS、Android など、ほとんどすべての主流オペレーティング システムをサポートします。これは、VLC を使用してほぼすべてのデバイスでメディアを再生できることを意味します。
-
マルチフォーマットのサポート: 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 などのさまざまなネットワーク ストリーミング形式もサポートしています。
-
高度な機能: 基本的な再生機能に加えて、VLC は、プレイリスト管理、オーディオおよびビデオ効果の調整、字幕サポート、ストリーミング メディア サーバーおよびクライアント、メディア トランスコーディングなどの一連の高度な機能も提供します。
-
オープンソースで無料: 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
たとえば のディレクトリにレイアウト ファイルを作成し、ビデオactivity_main.xml
を再生するための TextureView を追加します (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) {
}
}
OK、これでビデオ再生機能が完了しました
ただし、実際の開発では、スクリーンショットを撮ったり、ビデオを録画したりする機能も登場することがあります。以下に 1 つずつ紹介します。
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 ソース コードをダウンロードして自分でコンパイルし、基盤となる libVLC ライブラリにアクセスするための JNI コードを作成する必要があります。ただし、この方法には、基礎となる 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();