尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Android极简播放器:基于VideoView与MediaPlayer的轻量级实现

发布时间:2026/9/25 4:49:20

资讯中心
01
ARTICLE

Android极简播放器:基于VideoView与MediaPlayer的轻量级实现

Android极简播放器:基于VideoView与MediaPlayer的轻量级实现
1. 为什么我要在Android上折腾一个最简单的播放器做Android开发这些年播放器这个需求几乎每隔一段时间就会冒出来一次。有时候是客户要一个能播本地视频的小工具有时候是产品想在App里嵌一段宣传片还有时候只是自己写个Demo验证某个功能。每次遇到这种需求我第一反应都是去GitHub上找现成的库结果往往是这样ExoPlayer功能强大但接入成本不低IJKPlayer编译一次要等半天各种万能播放器要么依赖一堆so库要么文档写得云里雾里。后来我干脆自己动手基于Android原生的MediaPlayer和VideoView封装了一个极简的AVPlayer。核心目标就三个字简单、能跑、够用。它不追求支持所有冷门格式也不搞硬解码软解码自动切换那一套就是把打开一个视频文件并流畅播放这件事做到最干净。这篇文章我会把这个播放器的设计思路、关键代码、踩过的坑以及为什么在某些场景下简单反而是最优解全部摊开讲清楚。如果你正在找一个能直接抄进项目里用的轻量播放方案或者你被那些重型播放器库折磨过那这篇内容应该能帮你省下不少时间。我会从最基础的环境准备讲起一直讲到实际项目里怎么处理横竖屏切换、怎么控制进度条、怎么避免内存泄漏这些真实问题。全程都是我自己跑通过的代码和配置不是从文档里复制粘贴的。2. 环境准备与项目骨架搭建2.1 Android Studio版本选择与SDK配置先说环境。我用的Android Studio是2021.3.1 Patch 1这个版本也就是大家常说的Dolphin。为什么特意提版本因为播放器涉及MediaPlayer和SurfaceView这些底层API不同API级别行为差异挺明显的。我建议minSdkVersion至少设到21也就是Android 5.0这样能覆盖绝大多数设备同时MediaPlayer的稳定性也足够好。targetSdkVersion我设的是33编译时用的是34实测下来没什么兼容问题。在build.gradle里你不需要引入任何第三方播放器依赖。这一点很重要——很多教程上来就让你加ExoPlayer或者各种implementation但我们的目标是最简单所以依赖列表保持干净android { compileSdk 34 defaultConfig { minSdk 21 targetSdk 33 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.constraintlayout:constraintlayout:2.1.4 }就这些。appcompat和constraintlayout是几乎所有项目都会用的基础库不算额外负担。如果你连这两个都不想要用原生FrameLayout也能写但布局会麻烦一些。2.2 权限声明与文件访问策略播放本地视频绕不开文件读取权限。这里有个坑我踩过Android 6.0之后READ_EXTERNAL_STORAGE需要动态申请而Android 10之后又引入了分区存储WRITE_EXTERNAL_STORAGE基本被废弃了。我的做法是在AndroidManifest.xml里声明READ_EXTERNAL_STORAGE并且加上android:requestLegacyExternalStoragetrue来兼容Android 10的设备uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / application android:requestLegacyExternalStoragetrue ... /application然后在Activity里动态申请。这里我写了一个简单的权限检查逻辑如果没授权就请求授权失败就弹个Toast提示用户去设置里开。代码不复杂但少了这一步在Android 6以上的设备上直接读文件会崩。注意如果你只播放App内部assets目录或者res/raw目录里的视频那连权限都不用申请直接通过AssetFileDescriptor或者资源ID就能播。这是最省事的方案适合视频文件不大的场景。2.3 布局文件的最小化设计布局我用了ConstraintLayout因为控制播放器比例和按钮位置比较方便。核心就三个元素一个VideoView占满上半部分一个SeekBar做进度条下面一排按钮控制播放暂停和全屏切换。androidx.constraintlayout.widget.ConstraintLayout ... VideoView android:idid/videoView android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toTopOfid/seekBar app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / SeekBar android:idid/seekBar android:layout_width0dp android:layout_heightwrap_content app:layout_constraintBottom_toTopOfid/btnPlay ... / Button android:idid/btnPlay android:text播放/暂停 ... / /androidx.constraintlayout.widget.ConstraintLayoutVideoView本身继承自SurfaceView内部封装了MediaPlayer所以用起来比直接操作MediaPlayer简单得多。但它的缺点是不够灵活比如你想自定义渲染或者做滤镜那就得换成SurfaceView加MediaPlayer的组合。对于最简单这个目标VideoView是首选。3. 核心播放逻辑VideoView与MediaPlayer的取舍3.1 为什么我最终选了VideoView而不是裸MediaPlayer很多人一提到Android播放器就觉得应该直接用MediaPlayer觉得VideoView太重了。但实际用下来VideoView帮你处理了Surface的生命周期、视频尺寸自适应、以及和MediaPlayer的绑定关系。如果你自己写MediaPlayer加SurfaceView光是处理surfaceCreated、surfaceDestroyed和MediaPlayer状态同步就能写上百行代码而且很容易出bug。VideoView的核心方法就几个setVideoPath()、start()、pause()、seekTo()、getDuration()、getCurrentPosition()。这些方法覆盖了90%的播放需求。我实测过用VideoView播放一个1080p的MP4文件从点击到画面出现大概300毫秒左右和裸MediaPlayer差别不大。当然VideoView也有局限。它不支持播放列表自动切换不支持倍速播放需要API 23以上用MediaPlayer.setPlaybackParams()也不支持字幕外挂。但我们的定位就是最简单这些高级功能不在考虑范围内。3.2 播放器初始化的完整代码链路初始化逻辑我写在onCreate()里核心是设置视频路径和准备监听VideoView videoView findViewById(R.id.videoView); SeekBar seekBar findViewById(R.id.seekBar); Button btnPlay findViewById(R.id.btnPlay); String videoPath Environment.getExternalStorageDirectory() /test.mp4; videoView.setVideoPath(videoPath); videoView.setOnPreparedListener(mp - { // 视频准备完成后设置进度条最大值 seekBar.setMax(videoView.getDuration()); // 开始播放 videoView.start(); // 启动进度更新 updateProgress(); }); videoView.setOnCompletionListener(mp - { // 播放结束重置按钮状态 btnPlay.setText(重新播放); }); videoView.setOnErrorListener((mp, what, extra) - { Toast.makeText(this, 播放出错: what, Toast.LENGTH_SHORT).show(); return true; });这里有个细节setOnPreparedListener是在视频解码准备完成后回调的这时候getDuration()才能拿到正确的时长。如果你在setVideoPath()之后立刻调getDuration()返回的是-1。这个坑我见过不少人踩。3.3 进度条同步的两种实现方式进度条同步我试过两种方案。第一种是用Handler每隔500毫秒发一次消息更新SeekBarprivate Handler handler new Handler(Looper.getMainLooper()); private Runnable progressRunnable new Runnable() { Override public void run() { if (videoView.isPlaying()) { seekBar.setProgress(videoView.getCurrentPosition()); handler.postDelayed(this, 500); } } };第二种是用VideoView自带的setOnInfoListener但那个回调频率不稳定有时候快有时候慢。所以我最终选了Handler方案500毫秒的间隔在视觉上足够流畅又不会太耗电。用户拖动进度条的时候需要暂停更新等拖动结束再seekTo()seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { Override public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) { if (fromUser) { videoView.seekTo(progress); } } Override public void onStartTrackingTouch(SeekBar sb) { handler.removeCallbacks(progressRunnable); } Override public void onStopTrackingTouch(SeekBar sb) { handler.post(progressRunnable); } });这个逻辑看起来简单但如果不处理fromUser标志会出现用户拖一下进度条自己跳回去的鬼畜现象。4. 实际项目里绕不开的五个坑4.1 横竖屏切换导致播放中断这是最经典的问题。默认情况下屏幕旋转会触发Activity重建VideoView被销毁播放自然就断了。解决方案有两种一是在AndroidManifest.xml里给Activity加上android:configChangesorientation|screenSize|keyboardHidden让系统不重建Activity二是在onSaveInstanceState里保存播放位置重建后恢复。我推荐第一种因为简单直接activity android:name.PlayerActivity android:configChangesorientation|screenSize|keyboardHidden android:screenOrientationsensor /加上之后旋转屏幕时VideoView会自己调整尺寸播放不中断。但要注意如果你的布局里有其他依赖屏幕尺寸的元素可能需要手动处理一下。4.2 播放网络视频时的缓冲处理VideoView播放网络视频时如果网速慢会黑屏一段时间。用户体验很差。我的做法是加一个ProgressBar在onPrepared之前显示onPrepared之后隐藏。同时设置MediaPlayer的缓冲大小videoView.setOnPreparedListener(mp - { mp.setBufferSize(1024 * 1024); // 1MB缓冲 progressBar.setVisibility(View.GONE); videoView.start(); });但说实话VideoView对网络视频的支持比较弱它不支持HLS自适应码率也不支持断点续传。如果你的项目主要是播网络视频我还是建议上ExoPlayer。VideoView更适合本地文件播放。4.3 内存泄漏Handler和内部类的隐患我前面写的Handler和Runnable如果直接用匿名内部类会持有Activity的引用导致Activity无法回收。解决办法是把Handler改成静态内部类或者用WeakReference包裹private static class SafeHandler extends Handler { private final WeakReferencePlayerActivity activityRef; SafeHandler(PlayerActivity activity) { super(Looper.getMainLooper()); activityRef new WeakReference(activity); } Override public void handleMessage(Message msg) { PlayerActivity activity activityRef.get(); if (activity ! null !activity.isFinishing()) { // 更新UI } } }另外在onDestroy()里一定要调用handler.removeCallbacksAndMessages(null)把消息队列清空。4.4 视频比例与屏幕适配VideoView默认会按照视频原始比例显示但如果视频比例和屏幕比例差太多会出现黑边或者拉伸。我一般用setOnPreparedListener里获取视频宽高然后动态调整VideoView的布局参数mp.setOnVideoSizeChangedListener((mp1, width, height) - { // 根据视频宽高比调整VideoView尺寸 int videoRatio width / height; int screenRatio screenWidth / screenHeight; if (videoRatio screenRatio) { // 视频更宽按宽度适配 videoView.getLayoutParams().width screenWidth; videoView.getLayoutParams().height screenWidth / videoRatio; } else { // 视频更高按高度适配 videoView.getLayoutParams().height screenHeight; videoView.getLayoutParams().width screenHeight * videoRatio; } videoView.requestLayout(); });这段代码能让视频在保持比例的前提下尽量填满屏幕实测效果比默认的好很多。4.5 播放结束后的状态重置VideoView播放结束后isPlaying()返回false但getCurrentPosition()会停在最后一帧。如果用户再点播放需要先seekTo(0)再start()。我在onCompletionListener里做了处理videoView.setOnCompletionListener(mp - { btnPlay.setText(重新播放); seekBar.setProgress(0); videoView.seekTo(0); });这样用户再点按钮时从头开始播。如果不重置有些设备上会直接黑屏或者卡住。5. 进阶从VideoView迁移到MediaPlayer的时机5.1 什么时候该放弃VideoViewVideoView虽然简单但遇到下面这些需求时就得换方案需要倍速播放、需要多音轨切换、需要自定义渲染比如加滤镜、需要播放DRM加密内容。这些场景下VideoView的封装反而成了阻碍。我一般会在项目初期用VideoView快速验证等需求明确了再决定要不要迁移。迁移的成本主要是把VideoView替换成SurfaceView加MediaPlayer然后手动处理Surface生命周期。代码量大概会增加两三百行但换来的是完全的控制权。5.2 MediaPlayer的核心状态机MediaPlayer有一套严格的状态机搞错了就会抛IllegalStateException。核心状态包括Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted、Error。调用方法时必须按顺序来比如prepare()之前必须setDataSource()start()之前必须prepare()。我见过最常见的错误是在setDataSource()之后直接调start()结果崩了。正确的顺序是MediaPlayer mp new MediaPlayer(); mp.setDataSource(path); mp.setDisplay(surfaceHolder); mp.prepare(); // 或者 prepareAsync() mp.start();如果用prepareAsync()要等onPrepared回调之后再start()。5.3 用SurfaceView替代VideoView的代码骨架如果你决定迁移代码大概长这样SurfaceView surfaceView findViewById(R.id.surfaceView); surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { Override public void surfaceCreated(SurfaceHolder holder) { mediaPlayer new MediaPlayer(); mediaPlayer.setDisplay(holder); mediaPlayer.setDataSource(videoPath); mediaPlayer.prepareAsync(); } Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} Override public void surfaceDestroyed(SurfaceHolder holder) { if (mediaPlayer ! null) { mediaPlayer.release(); mediaPlayer null; } } });注意surfaceDestroyed里必须释放MediaPlayer否则会泄漏。而且释放之后不能再调用它的方法否则崩。6. 我在真实项目里总结的几条经验6.1 视频文件格式的选择比播放器更重要很多人花大量时间优化播放器代码结果视频文件本身编码就有问题。我实测下来H.264编码加AAC音频的MP4文件兼容性最好从Android 4.4到Android 14都能播。如果你用H.265或者VP9低端设备上可能直接黑屏。所以在上传视频之前先用工具转成标准H.264格式能省掉后面一堆麻烦。6.2 不要在主线程做文件IO读取视频文件路径、检查文件是否存在这些操作如果放在主线程视频大了会卡顿甚至ANR。我一般用AsyncTask或者ExecutorService在后台线程做准备好之后再切回主线程更新UI。虽然VideoView.setVideoPath()本身是异步的但前面的文件检查逻辑还是要注意。6.3 测试时一定要用真机模拟器上播放视频经常出问题要么没声音要么画面卡住。这是因为模拟器的GPU渲染和真机不一样。我踩过好几次坑在模拟器上跑得好好的一到真机就黑屏。所以播放器相关的功能必须用真机测试而且最好多找几台不同品牌的设备。6.4 关于免费和开源的取舍标题里写了亲测免费这里说的免费是指不依赖任何收费SDK。Android原生的MediaPlayer和VideoView本身就是系统的一部分不需要额外授权。但如果你用了某些第三方播放器库要注意它们的License。有些库说是开源但商用需要授权。我这个方案完全基于系统API没有任何法律风险。6.5 后续可以扩展的方向如果你把这个基础版本跑通了后面可以按需加功能。比如加一个视频列表用RecyclerView展示点击切换播放或者加一个手势控制左滑快退右滑快进再或者加一个悬浮窗播放用WindowManager把SurfaceView浮在其他应用上面。这些扩展都不难核心的播放逻辑不用动。最后分享一个我常用的调试技巧在onErrorListener里把what和extra打印出来然后去查Android源码里MediaPlayer的错误码定义。比如what1, extra-1004通常表示文件格式不支持what1, extra-110表示超时。知道具体错误码排查起来快很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。