VLC是一款非常强大的开源媒体播放器,由VideoLAN组织开发和维护。它最初是为学校项目开发的,但现在已经成为全球最流行的媒体播放器之一。
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)提供了一套完整的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();