使用 LibVLC 构建自定义 Android 视频播放器:分步指南,降低延迟/图像失帧(附源码)
前言
在这篇博文中,我们将深入探讨使用 LibVLC 库的自定义 Android 视频播放器的实现细节。本分步指南将涵盖设置播放器、处理各种事件以及在您的 Android 应用程序中提供无缝视频播放体验的基本方面。
一、LibVLC 概述:
视频播放是许多 Android 应用程序中的常见功能,从流媒体服务到多媒体播放器。LibVLC 是一种流行的多媒体框架,为开发人员构建功能丰富的视频播放器提供了坚实的基础。我们将利用 LibVLC 的功能创建自定义视频播放器。
二、引入依赖
在深入研究实现之前,请确保在 Android 项目中设置了必要的依赖项。包括 LibVLC 库及其关联的依赖项。您可以在 LibVLC AAR下载地址上找到相应的AAR包并导入。
三、LibVLC 使用
1.初始化 LibVLC 实例
public class PlayerView extends FrameLayout { private LibVLC mLibVLC; private MediaPlayer mediaPlayer; // Constructor and other methods... private void init() { // Initialize LibVLC and MediaPlayer mLibVLC = LibVLCUtil.getLibVLC(CloudVlcOptions.getInstance().getOptions()); mediaPlayer = new MediaPlayer(mLibVLC); // Additional setup... } }
2.设置 SurfaceView
public class PlayerView extends FrameLayout { private SurfaceView mSurface; private SurfaceHolder mSurfaceHolder; // Constructor and other methods... private void init() { // Additional setup... // Set up SurfaceView mSurface = findViewById(R.id.player_surface); mSurfaceHolder = mSurface.getHolder(); mSurfaceHolder.setFormat(PixelFormat.RGBX_8888); mSurfaceHolder.addCallback(surfaceViewCallback); // Additional surface-related configuration... } private SurfaceHolder.Callback surfaceViewCallback = new SurfaceHolder.Callback() { // Implement surface-related callbacks... }; }
3.实现事件处理
public class PlayerView extends FrameLayout { private MediaPlayer.EventListener eventListener; // Constructor and other methods... private void init() { // Additional setup... // Event listener eventListener = new MediaPlayer.EventListener() { @Override public void onEvent(MediaPlayer.Event event) { // Handle MediaPlayer events } }; } }
4.媒体播放控制
public class PlayerView extends FrameLayout { // Constructor and other methods... public void start() { // Start playback mediaPlayer.play(); } public void pausePlay(boolean hasToDetach) { // Pause playback if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); } // Additional handling... } public void resumePlay() { try { //设置视频界面 vlcVout.detachViews(); vlcVout.setVideoSurface(mSurface.getHolder().getSurface(), mSurface.getHolder()); //设置播放窗口的尺寸 vlcVout.setWindowSize(mSurface.getWidth(), mSurface.getHeight()); vlcVout.addCallback(callback); vlcVout.setVideoView(mSurface); vlcVout.attachViews(this.layoutListener); isAttachViewsReady = true; mediaPlayer.setEventListener(eventListener); } catch (Exception e) { e.printStackTrace(); } } public void releasePlayer() { // Release player resources if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; } // Additional cleanup... } // Additional playback control methods... }
5.布局和显示处理
public class PlayerView extends FrameLayout { // Constructor and other methods... private void updateVideoSurfaces() { // Update video surfaces based on layout and display size } private void changeMediaPlayerLayout(int displayW, int displayH) { // Change video placement using MediaPlayer API } // Additional layout and display handling methods... }
结论
通过执行这些步骤,您可以使用 LibVLC 将自定义 Android 视频播放器集成到您的应用程序中。以下为LibVLC使用源码↓
1.创建LibVLC自定义View继承FrameLayout
public class PlayerView extends FrameLayout { private static final String TAG = "PlayerView"; public interface OnChangeListener { void onBuffer(float buffer); void onEnd(); void onTimeChange(MediaPlayer.Event event); void onNewLayout(); void onPlaying(); void onPause(); void onError(); } private Context mContext; private LibVLC mLibVLC; private MediaPlayer mediaPlayer; private IVLCVout vlcVout; private int videoWidth; private int videoHeight; private boolean isLocal; private long totalTime = 0; private SurfaceView mSurface; //private SurfaceView mSubtitlesSurface; private SurfaceHolder mSurfaceHolder; //private SurfaceHolder mSubtitlesSurfaceHolder; private FrameLayout mSurfaceFrame; private boolean isAttachViewsReady = false; private OnChangeListener mOnChangeListener; private boolean mCanSeek = false; private String url; //------------------------------ private final Handler mHandler = new Handler(); private View.OnLayoutChangeListener mOnLayoutChangeListener = null; private int mVideoHeight = 0; private int mVideoWidth = 0; private int mVideoVisibleHeight = 0; private int mVideoVisibleWidth = 0; private int mVideoSarNum = 0; private int mVideoSarDen = 0; private static final int SURFACE_BEST_FIT = 0; private static final int SURFACE_FIT_SCREEN = 1; private static final int SURFACE_FILL = 2; private static final int SURFACE_16_9 = 3; private static final int SURFACE_4_3 = 4; private static final int SURFACE_ORIGINAL = 5; private static int CURRENT_SIZE = SURFACE_BEST_FIT; private int screenWid, screenHei; public PlayerView(Context context) { super(context); mContext = context; init(); } public PlayerView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; init(); } public void initPlayer(String url) { this.url = url; isLocal = false; initializeMedia(); updateVideoSurfaces(); } public void initPlayer(File file) { this.url = file.getAbsolutePath(); isLocal = true; initializeMedia(); updateVideoSurfaces(); } private void init() { LayoutInflater.from(getContext()).inflate(R.layout.hz_view_player, this); mSurface = (SurfaceView) findViewById(R.id.player_surface); mSurfaceFrame = (FrameLayout) findViewById(R.id.player_surface_frame); mLibVLC = LibVLCUtil.getLibVLC(CloudVlcOptions.getInstance().getOptions()); mediaPlayer = new MediaPlayer(mLibVLC); mSurfaceHolder = mSurface.getHolder(); mSurfaceHolder.setFormat(PixelFormat.RGBX_8888); mSurfaceHolder.setKeepScreenOn(true); mSurfaceHolder.addCallback(surfaceViewCallback); //设置视频界面 vlcVout = mediaPlayer.getVLCVout(); vlcVout.setVideoSurface(mSurface.getHolder().getSurface(), mSurface.getHolder()); vlcVout.setWindowSize(mSurface.getWidth(), mSurface.getHeight()); vlcVout.addCallback(callback); vlcVout.setVideoView(mSurface); vlcVout.attachViews(this.layoutListener); //设置播放窗口的尺寸 PixelFormat info = new PixelFormat(); PixelFormat.getPixelFormatInfo(PixelFormat.RGBX_8888, info); isAttachViewsReady = true; mediaPlayer.setEventListener(eventListener); if (mOnLayoutChangeListener == null) { mOnLayoutChangeListener = new View.OnLayoutChangeListener() { private final Runnable mRunnable = new Runnable() { @Override public void run() { updateVideoSurfaces(); } }; @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { mHandler.removeCallbacks(mRunnable); mHandler.post(mRunnable); } } }; } mSurfaceFrame.addOnLayoutChangeListener(mOnLayoutChangeListener); } SurfaceHolder.Callback surfaceViewCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { } @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) { } @Override public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { releasePlayer(); // 在这里调用销毁播放器的方法 } }; private IVLCVout.Callback callback = new IVLCVout.Callback() { @Override public void onSurfacesCreated(IVLCVout ivlcVout) { } @Override public void onSurfacesDestroyed(IVLCVout ivlcVout) { } }; private IVLCVout.OnNewVideoLayoutListener layoutListener = new IVLCVout.OnNewVideoLayoutListener() { @Override public void onNewVideoLayout(IVLCVout ivlcVout, int i, int i1, int i2, int i3, int i4, int i5) { try { screenWid = i; screenHei = i1; totalTime = mediaPlayer.getLength(); mCanSeek = mediaPlayer.isSeekable(); videoWidth = i; videoHeight = i1; LogUtil.e("--------onNewLayout------------"); if (mOnChangeListener != null) { mOnChangeListener.onNewLayout(); } //------------------------- mVideoWidth = i; mVideoHeight = i1; mVideoVisibleWidth = i2; mVideoVisibleHeight = i3; mVideoSarNum = i4; mVideoSarDen = i5; updateVideoSurfaces(); Log.d(TAG, "Video dimensions: " + videoWidth + "x" + videoHeight); } catch (Exception e) { Log.d("vlc-newlayout", e.toString()); } } }; private MediaPlayer.EventListener eventListener = new MediaPlayer.EventListener() { @Override public void onEvent(MediaPlayer.Event event) { try { if (event.type == MediaPlayer.Event.Buffering) { if (mOnChangeListener != null) { mOnChangeListener.onBuffer(event.getBuffering()); } } else if (event.type == MediaPlayer.Event.TimeChanged) { if (mOnChangeListener != null) { mOnChangeListener.onTimeChange(event); } } else if (event.type == MediaPlayer.Event.Playing) { if (mOnChangeListener != null) { mOnChangeListener.onPlaying(); } } else if (event.type == MediaPlayer.Event.Paused) { if (mOnChangeListener != null) { mOnChangeListener.onPause(); } } else if (event.type == MediaPlayer.Event.EncounteredError) { if (mOnChangeListener != null) { mOnChangeListener.onError(); } } //播放结束 if (mediaPlayer.getPlayerState() == Media.State.Ended && event.type == MediaPlayer.Event.EndReached) { if (mOnChangeListener != null) { mOnChangeListener.onEnd(); } } } catch (Exception e) { Log.d("vlc-event", e.toString()); } } }; public void setOnChangeListener(OnChangeListener listener) { mOnChangeListener = listener; } public void changeSurfaceSize(int width) { this.screenWid = width; this.screenHei = videoHeight; ViewGroup.LayoutParams layoutParams = mSurface.getLayoutParams(); layoutParams.width = width; layoutParams.height = (int) Math.ceil((float) videoHeight * (float) width / (float) videoWidth); mSurface.setLayoutParams(layoutParams); } public void changeSurfaceSize(int width, int height) { this.screenWid = width; this.screenHei = height; Log.d(TAG, "Video dimensions: " + videoWidth + "x" + videoHeight); ViewGroup.LayoutParams layoutParams = mSurface.getLayoutParams(); float max = Math.max((float) videoWidth / (float) width, (float) videoHeight / (float) height); layoutParams.width = (int) Math.ceil((float) videoWidth / max); layoutParams.height = (int) Math.ceil((float) videoHeight / max); mSurfaceHolder.setFixedSize(videoWidth, videoHeight); mSurface.setLayoutParams(layoutParams); } public void setZOrderMediaOverlay(boolean isOverlay) { mSurface.setZOrderMediaOverlay(isOverlay); } public void start() { try { if (mediaPlayer.getPlayerState() == Media.State.Ended) { restart(); } else if (!mediaPlayer.isPlaying()) { mediaPlayer.play(); } } catch (Exception e) { e.printStackTrace(); } } public void restart() { if (mediaPlayer.getPlayerState() == Media.State.Ended) { Media media = null; if (isLocal) { media = new Media(mLibVLC, url); } else { media = new Media(mLibVLC, Uri.parse(url)); } setupMediaOptions(media); mediaPlayer.setMedia(media); } mediaPlayer.play(); } private void initializeMedia() { Media media = null; if (isLocal) { media = new Media(mLibVLC, url); } else { media = new Media(mLibVLC, Uri.parse(url)); } if (mediaPlayer.getMedia() == null) { setupMediaOptions(media); mediaPlayer.setMedia(media); } } private Media createMedia(String mediaUrl) { return new Media(mLibVLC, Uri.parse(mediaUrl)); } private void setupMediaOptions(Media media) { int cache = 300; media.addOption(":network-caching=" + cache); media.addOption(":file-caching=" + cache); media.addOption(":live-caching=" + cache); media.addOption(":sout-mux-caching=" + cache); media.addOption(":codec=mediacodec,iomx,all"); media.setHWDecoderEnabled(true, false); } public void pausePlay(boolean hasToDetach) { try { if(mediaPlayer!=null){ if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); //imgPlay.setBackgroundResource(R.drawable.videoviewx_play); } if (hasToDetach) { vlcVout.detachViews(); vlcVout.removeCallback(callback); mediaPlayer.setEventListener(null); isAttachViewsReady = false; } } } catch (Exception e) { e.printStackTrace(); } } public void resumePlay() { try { //设置视频界面 vlcVout.detachViews(); vlcVout.setVideoSurface(mSurface.getHolder().getSurface(), mSurface.getHolder()); //设置播放窗口的尺寸 vlcVout.setWindowSize(mSurface.getWidth(), mSurface.getHeight()); vlcVout.addCallback(callback); vlcVout.setVideoView(mSurface); vlcVout.attachViews(this.layoutListener); isAttachViewsReady = true; mediaPlayer.setEventListener(eventListener); } catch (Exception e) { e.printStackTrace(); } } public void releasePlayer() { try { if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; } if (mLibVLC != null) { mLibVLC.release(); mLibVLC = null; } } catch (Exception e) { e.printStackTrace(); } } public boolean isAttachViewsReady() { return isAttachViewsReady; } public long getTime() { return mediaPlayer.getTime(); } public long getLength() { return mediaPlayer.getLength(); } public void setTime(long time) { mediaPlayer.setTime(time); } public void setNetWorkCache(int time) { //mediaPlayer.setNetworkCaching(time); } public boolean isPlaying() { if(mediaPlayer!=null){ return mediaPlayer.isPlaying(); } return false; } public boolean isSeekable() { return mediaPlayer.isSeekable(); } public int getVolume() { return mediaPlayer.getVolume(); } public void setVolume(int volume) { mediaPlayer.setVolume(volume); } public void seek(int delta) { if (mediaPlayer.getLength() restart(); } long position = delta; if (position = ar) dh = dw / ar; /* horizontal */ else dw = dh * ar; /* vertical */ break; case SURFACE_FILL: break; case SURFACE_16_9: ar = 16.0 / 9.0; if (dar = ar) scale = displayW / (float) videoW; /* horizontal */ else scale = displayH / (float) videoH; /* vertical */ mediaPlayer.setScale(scale); mediaPlayer.setAspectRatio(null); } else { mediaPlayer.setScale(0); mediaPlayer.setAspectRatio(!videoSwapped ? "" + displayW + ":" + displayH : "" + displayH + ":" + displayW); } break; } case SURFACE_16_9: mediaPlayer.setAspectRatio("16:9"); mediaPlayer.setScale(0); break; case SURFACE_4_3: mediaPlayer.setAspectRatio("4:3"); mediaPlayer.setScale(0); break; case SURFACE_ORIGINAL: mediaPlayer.setAspectRatio(null); mediaPlayer.setScale(1); break; } } }
2.自定义View的XML
3.PlayerView中引用的‘LibVLCUtil’ VLC设置项已单例模式创建
public class LibVLCUtil { private static LibVLC libVLC = null; public synchronized static LibVLC getLibVLC(ArrayList options) throws IllegalStateException { if (libVLC == null) { if (options == null) { libVLC = new LibVLC(MyApplication.getInstance(),new ArrayList()); } else { libVLC = new LibVLC(MyApplication.getInstance(),options); } } return libVLC; } }
4.PlayerView中引用的‘CloudVlcOptions’ VLC设置项已单例模式创建
public class CloudVlcOptions { private static CloudVlcOptions instance=null; private ArrayList list=null; private CloudVlcOptions(){ this.list=new ArrayList(); list.add(false ? "--audio-time-stretch" : "--no-audio-time-stretch"); list.add("--audio-resampler"); list.add("--aout=opensles"); list.add("--audio-time-stretch"); list.add("--network-caching="+300); list.add("--clock-jitter="+300); list.add("--file-caching=" + 300); list.add("--demux=avformat"); list.add("-vvv"); } public static CloudVlcOptions getInstance(){ if(instance==null){ instance=new CloudVlcOptions(); } return instance; } public ArrayList getOptions(){ return list; } }
如有出现图像失帧或出现libvlc input source: attachment of directory-extractor failed...类似的错误,可以尝试添加"--demux=avformat",或查看VLC官网议题:图像失帧,尝试是否对你有利。
5.Activity中使用
public class VideoPlayerTestActivity extends BaseActivity implements View.OnClickListener { private String mUrl; private File file; private boolean isChangeDisplay = false; private long timeCount = 0; private PlayerView mPlayerView; private RelativeLayout durationBarRl; private ImageView playIconIv; private TextView curDurationTv; private TextView totalDurationTv; private SeekBar progressSeekBar; private ImageView shareIv; private SimpleDateFormat sp = new SimpleDateFormat("mm:ss"); private boolean hasSetVideoPosition = false; //发送心跳 private Timer mHeartBeatTimer = null; private boolean isVisFront = false; @Override protected int getContentViewId() { return R.layout.video_player; } @Override protected void initView() { super.initView(); DeviceTools.hideNavigationBar(this); mUrl = getIntent().getStringExtra("url"); file = (File) getIntent().getSerializableExtra("file"); if (TextUtils.isEmpty(mUrl)) { shareIv.setVisibility(View.VISIBLE); } else { shareIv.setVisibility(View.GONE); } initPlayer(); if(Config.CURRENT_DEVICE_TYPE != null){ if(Config.CURRENT_DEVICE_TYPE.equals(Type.DvrType.SG20_B.getVal()) || Config.CURRENT_DEVICE_TYPE.equals(Type.DvrType.SG18A.getVal())){ progressSeekBar.setEnabled(false); }else { progressSeekBar.setEnabled(true); initSeekBar(); } }else { progressSeekBar.setEnabled(true); initSeekBar(); } } @Override protected void findViews() { super.findViews(); mPlayerView = findViewById(R.id.pv_video); durationBarRl = findViewById(R.id.rl_duration_bar); playIconIv = findViewById(R.id.iv_play_icon); curDurationTv = findViewById(R.id.tv_cur_duration); totalDurationTv = findViewById(R.id.tv_total_duration); progressSeekBar = findViewById(R.id.seek_bar_progress); shareIv = findViewById(R.id.iv_share); findViewById(R.id.iv_close).setOnClickListener(this); mPlayerView.setOnClickListener(this); playIconIv.setOnClickListener(this); shareIv.setOnClickListener(this); } private void initSeekBar() { progressSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { boolean isUser = false; @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { isUser = b; } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { if (isUser) { mPlayerView.seek(seekBar.getProgress()); } } }); } private void share() { if (file == null) { return; } Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); intent.setType("*/*"); startActivity(intent); } private void initPlayer() { //初始化播放器 if (!TextUtils.isEmpty(mUrl)) { mPlayerView.initPlayer(mUrl); } if (file != null) { mPlayerView.initPlayer(file); } //设置事件监听,监听缓冲进度等 mPlayerView.setOnChangeListener(new PlayerView.OnChangeListener() { @Override public void onBuffer(float buffer) { if (buffer != 100) { LoadingUtil.showLoadingDialog(); } else { LoadingUtil.hideLoadingDialog(); if (hasSetVideoPosition) { hasSetVideoPosition = false; } } } @Override public void onEnd() { playIconIv.setImageResource(R.mipmap.hz_dvr_icon_play); ToastUtils.showToast(VideoPlayerTestActivity.this, MyApplication.getAppResources().getString(R.string.play_end)); LoadingUtil.hideLoadingDialog(); progressSeekBar.setProgress(progressSeekBar.getMax()); if (mPlayerView.getLength() > 0) { Date curDate = new Date(mPlayerView.getLength()); curDurationTv.setText(sp.format(curDate)); } } @Override public void onTimeChange(MediaPlayer.Event event) { progressSeekBar.setProgress((int) mPlayerView.getTime()); Date curDate = new Date(mPlayerView.getTime()); curDurationTv.setText(sp.format(curDate)); } @Override public void onNewLayout() { onPlayerLoadComplete(); mPlayerView.changeSurfaceSize(DeviceTools.getWindowHeight(),DeviceTools.getWindowWidth()); LogUtil.e("----------onNewLayout--------------"); if (isChangeDisplay) { isChangeDisplay = false; mPlayerView.start(); } } @Override public void onPlaying() { playIconIv.setImageResource(R.mipmap.hz_dvr_icon_pause); } @Override public void onPause() { playIconIv.setImageResource(R.mipmap.hz_dvr_icon_play); } @Override public void onError() { } }); // 开始播放 mPlayerView.start(); hasSetVideoPosition = true; } private void onPlayerLoadComplete() { Date curDate = new Date(mPlayerView.getTime()); Date totalDate = new Date(mPlayerView.getLength()); curDurationTv.setText(sp.format(curDate)); if (mPlayerView.getLength() > 0) { totalDurationTv.setText(sp.format(totalDate)); progressSeekBar.setMax((int) mPlayerView.getLength()); Drawable drawable = new BitmapDrawable(getResources(), BitmapFactory.decodeResource(getResources(), R.mipmap.hz_box_dvr_video_slider)); progressSeekBar.setThumb(drawable); } else { progressSeekBar.setMax((int) timeCount); totalDurationTv.setText(sp.format(timeCount)); } progressSeekBar.setProgress((int) mPlayerView.getTime()); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override protected void onDestroy() { super.onDestroy(); if(mPlayerView!=null){ mPlayerView.releasePlayer(); } } @Override protected void onResume() { super.onResume(); mPlayerView.resumePlay(); mPlayerView.start(); } @Override protected void onPause() { super.onPause(); mPlayerView.pausePlay(true); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.pv_video: isVisFront = !isVisFront; this.durationBarRl.setVisibility(isVisFront?View.GONE:View.VISIBLE); break; case R.id.iv_close: finish(); break; case R.id.iv_play_icon: if (mPlayerView.isPlaying()) { mPlayerView.pausePlay(false); playIconIv.setImageResource(R.mipmap.hz_dvr_icon_play); } else { mPlayerView.start(); playIconIv.setImageResource(R.mipmap.hz_dvr_icon_pause); } break; case R.id.iv_share: share(); break; } } }
6.Activity的XML
7.资源文件
hz_dvr_icon_play
hz_dvr_icon_pause
hz_box_dvr_video_slider
hz_dvr_close_setting_window
hz_dvr_file_icon_share
四、总结
使用 LibVLC 构建功能强大的 Android 视频播放器为开发人员提供了处理多媒体内容的多功能解决方案。按照这篇博文中概述的步骤,您可以创建具有高级播放控件、事件处理和布局调整功能丰富的视频播放器。LibVLC 的强大功能使其成为在 Android 应用程序中为用户提供无缝多媒体体验的绝佳选择。