鸿蒙应用开发:实现简单的媒体播放器
- 2021 年 12 月 10 日
本文字数:6861 字
阅读完需:约 23 分钟

距离 Harmony OS 发布已过去了一段时间,为了了解鸿蒙系统的功能与特性,今天我们将准备使用系统 API 实现一个简单的媒体播放器 demo。
大家在阅读本文后会对媒体播放器相关的 API 有一定的了解,并且可以根据文中的步骤一起动手实操,实现在鸿蒙系统上的简单媒体播放器!
VideoPlayerDemo 仓库地址:VideoPlayerDemo: 鸿蒙系统 API 实现简单媒体播放器
话不多说,下面我将带领大家一起看一下媒体播放器的实现流程。
一、媒体资源文件的权限申请
Harmony OS 中所有的应用均在应用沙盒内运行。默认情况下,应用只能访问有限的系统资源,系统负责管理应用对资源的访问权限。
所以要实现媒体播放器功能当然需要访问媒体资源,这时就需要向操作系统申请权限:
1、需要在 config.json 中的 ""reqPermissions" 添加配置:
"reqPermissions": [ { "name": "ohos.permission.READ_USER_STORAGE" }]2、需要在程序启动时请求读取本地媒体资源的权限:
if (verifyCallingOrSelfPermission(SystemPermission.READ_USER_STORAGE) != IBundleManager.PERMISSION_GRANTED) { requestPermissionsFromUser(new String[]{SystemPermission.READ_USER_STORAGE}, REQUEST_CODE);}二、获取本地的媒体资源文件
在申请到媒体资源的权限后,我们就可以去获取本地的媒体资源信息,包括音频及视频。
我们可以通过以下操作,将所有的音视频文件标识为 AVElement 并存入 List<AVElement> 中方便我们做 UI 展示,稍后可通过 AVElement 获取媒体资源文件路径,传递给系统播放器。
private final List<AVElement> avElements = new ArrayList<>();
public VideoElementManager(Context context) { loadAudioFromMediaLibrary(context); loadVideoFromMediaLibrary(context);}
// 查找音频private void loadAudioFromMediaLibrary(Context context) { Uri audioUri = AVStorage.Audio.Media.EXTERNAL_DATA_ABILITY_URI; DataAbilityHelper helper = DataAbilityHelper.creator(context, audioUri, false); try { ResultSet resultSet = helper.query(audioUri, null, null); LogUtil.info(TAG, "The audio result size: " + resultSet.getRowCount()); processResult(resultSet); resultSet.close(); } catch (DataAbilityRemoteException e) { LogUtil.error(TAG, "Query system media failed."); } finally { helper.release(); }}
// 查找视频private void loadVideoFromMediaLibrary(Context context) { Uri videoUri = AVStorage.Video.Media.EXTERNAL_DATA_ABILITY_URI; DataAbilityHelper helper = DataAbilityHelper.creator(context, videoUri, false); try { ResultSet resultSet = helper.query(videoUri, null, null); LogUtil.info(TAG, "The video result size: " + resultSet.getRowCount()); processResult(resultSet); resultSet.close(); } catch (DataAbilityRemoteException e) { LogUtil.error(TAG, "Query system media failed."); } finally { helper.release(); }}
// 处理数据到 List<AVElement>private void processResult(ResultSet resultSet) { while (resultSet.goToNextRow()) { String path = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.DATA)); String title = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.TITLE)); AVDescription bean = new AVDescription.Builder().setTitle(title).setIMediaUri(Uri.parse(path)).setMediaId(path).build(); avElements.add(new AVElement(bean, AVElement.AVELEMENT_FLAG_PLAYABLE)); }}三、简单的播放功能实现
通过以上的步骤,我们已经获取了媒体资源权限,并且获取到本地的所有音视频,下面我们将通过调用系统 API 实现简单的播放功能。
下图为媒体播放器界面:
通过上面的 UI 界面,我们可以清晰的看出要实现哪些简单功能,例如播放、暂停、恢复、跳转、音量调节、倍速播放和播放进度更新。
下面将给大家详细介绍以上这些功能的具体实现方法。
1、渲染 View 准备
需要播放媒体资源,特别是视频资源,我们当然需要有 View 去渲染,鸿蒙系统 API 需要设置的 View 是 Surface,我们通过以下方法获取 Surface。
// 声明 SurfaceProvider, Surfaceprivate SurfaceProvider surfaceProvider;private DirectionalLayout playViewLayout;private Surface surface;
private void addSurfaceProvider() { surfaceProvider = new SurfaceProvider(this);
if (surfaceProvider.getSurfaceOps().isPresent()) { surfaceProvider.getSurfaceOps().get().addCallback(new SurfaceCallBack()); surfaceProvider.pinToZTop(true); }}
// 生成 Surface 的回调class SurfaceCallBack implements SurfaceOps.Callback { @Override public void surfaceCreated(SurfaceOps callbackSurfaceOps) { if (surfaceProvider.getSurfaceOps().isPresent()) { surface = surfaceProvider.getSurfaceOps().get().getSurface(); LogUtil.info(TAG, "surface set"); } }
@Override public void surfaceChanged(SurfaceOps callbackSurfaceOps, int format, int width, int height) { LogUtil.info(TAG, "surface changed"); }
@Override public void surfaceDestroyed(SurfaceOps callbackSurfaceOps) { LogUtil.info(TAG, "surface destroyed"); }}playViewLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_playViewLayout);playViewLayout.addComponent(surfaceProvider);2、播放
我们获取到 Surface 后就可以调用系统 API 进行播放了。以下是对系统 API 的简单封装,我们可以调用 play 接口播放媒体资源。
private Player videoPlayer;private Runnable videoRunnable;
public synchronized void play(AVElement avElement, Surface surface) { if (videoPlayer != null) { // 关闭当前播放资源 // release 会重置播放状态并关闭定时器 videoPlayer.stop(); videoPlayer.release(); videoPlayer = null; }
if (videoRunnable != null) { ThreadPoolManager.getInstance().cancel(videoRunnable); }
videoPlayer = new Player(context); setPlayerCallback();
videoRunnable = () -> playInner(avElement, surface); ThreadPoolManager.getInstance().execute(videoRunnable); LogUtil.info(TAG, "play");}
private void playInner(AVElement avElement, Surface surface) { Source source = new Source(avElement.getAVDescription().getMediaUri().toString()); videoPlayer.setSource(source); videoPlayer.setVideoSurface(surface); LogUtil.info(TAG, source.getUri());
videoPlayer.prepare(); videoPlayer.play(); playbackState = PlaybackState.VIDEO_PLAYER_PLAYING; startTimer();}
// 设置 Callbackprivate void setPlayerCallback() { videoPlayer.setPlayerCallback(new Player.IPlayerCallback() { @Override public void onPrepared() { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onPrepared(); } }); }
@Override public void onMessage(int type, int extra) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onMessage(type, extra); } }); }
@Override public void onError(int errorType, int errorCode) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onError(errorType, errorCode); } }); }
@Override public void onResolutionChanged(int width, int height) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onResolutionChanged(width, height); } }); }
@Override public void onPlayBackComplete() { playbackState = PlaybackState.VIDEO_PLAYER_PLAY_ENDED; endTimer(); if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onPlayBackComplete(); } }); }
@Override public void onRewindToComplete() { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onRewindToComplete(); } }); }
@Override public void onBufferingChange(int percent) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onBufferingChange(percent); } }); }
@Override public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onNewTimedMetaData(mediaTimedMetaData); } }); }
@Override public void onMediaTimeIncontinuity(Player.MediaTimeInfo mediaTimeInfo) { if (eventHandler == null) { return; } EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onMediaTimeIncontinuity(mediaTimeInfo); } }); } });}3、暂停、恢复和跳转
实现一个媒体播放器,我们当然是需要暂停、恢复和跳转功能,以下是简单的封装。
这里有一个问题大家需要注意,虽然系统 API 中 getCurrentTime 接口返回值单位是毫秒,但是跳转接口 rewindTo 的接口参数单位是微秒。
// 暂停public synchronized void pause() { if (videoPlayer == null) { return; } videoPlayer.pause(); playbackState = PlaybackState.VIDEO_PLAYER_PAUSING; endTimer(); LogUtil.info(TAG, "pause");}
// 恢复public synchronized void resume() { if (videoPlayer == null) { return; } videoPlayer.play(); playbackState = PlaybackState.VIDEO_PLAYER_PLAYING; startTimer(); LogUtil.info(TAG, "resume");}
// 跳转public synchronized void seekTo(long millisecond) { if (videoPlayer == null) { return; }
// 注意,rewindTo 接口参数单位是微秒 videoPlayer.rewindTo(millisecond * 1000); LogUtil.info(TAG, "seek" + videoPlayer.getCurrentTime());}4、时长获取和播放进度显示
我们对获取时长接口做了简单的封装,但是系统 API 中没有相关的播放进度回调,为了进行 UI 展示,我们需要自己维护一个播放状态和定时器去定时获取当前播放进度。
// 获取资源总时长,需要收到 onPrepared 回调后调用public synchronized int getDuration() { if (videoPlayer == null) { return 0; }
return videoPlayer.getDuration();}
private Timer timer;private PlaybackState playbackState;
private void startTimer() { if (timer == null) { timer = new Timer("PlaybackPorgressTimer"); timer.schedule(new TimerTask() { @Override public void run() { playbackProgressUpdate(); } }, 200L, 200L); }}
private void endTimer() { if (timer == null) { return; } timer.cancel(); timer = null;}
private void playbackProgressUpdate() { if (eventHandler == null || videoPlayer == null) { return; } int currentPlaybackProgress = videoPlayer.getCurrentTime(); EventHandler handler = new EventHandler(EventRunner.getMainEventRunner()); handler.postTask(new Runnable() { @Override public void run() { eventHandler.onPlaybackProgressUpdate(currentPlaybackProgress); } });}5、音量调节和倍速播放
鸿蒙播放器系统 API 同样暴露了音量调节和倍速播放的接口,不过需要注意的是音量调节的系统 API 参数范围是 0.0~1.0,改接口仅控制播放器的音量,与手机侧边音量控制无关,有兴趣的话可以自己测试一下。
public synchronized void setPlaybackVolume(int volume) { if (videoPlayer == null) { return; }
videoPlayer.setVolume((float) ((float) volume/100.0));}
public synchronized void setPlaybackSpeed(float speed) { if (videoPlayer == null) { return; }
videoPlayer.setPlaybackSpeed(speed);}
public synchronized float getPlaybackSpeed() { if (videoPlayer == null) { return 0.0f; }
return videoPlayer.getPlaybackSpeed();}四、总结
通过调用系统 API 实现一个简单的媒体播放器,相信大家一定对 Harmony OS 都有了初步的了解与认识,比如说真正意义上的跑起来一个鸿蒙 APP,中间经过了开发者账号的申请、APP 证书的申请等等。
在 11 月底,ZEGO 即构科技首发适配鸿蒙系统的 Express SDK 1.0 版本,并正式启动了公测,这其中包括摄像头、麦克风、扬声器等设备的深度兼容与适配,实现了更加稳定、快速的设备管理能力,感兴趣可点击链接了解:ZEGO 即构科技首发适配鸿蒙系统的 Express SDK 1.0 版本,并正式启动公测!
版权声明: 本文为 InfoQ 作者【ZEGO即构】的原创文章。
原文链接:【http://xie.infoq.cn/article/52da465b3ae7358612539c4d7】。
本文遵守【CC BY-NC-ND】协议,转载请保留原文出处及本版权声明。
ZEGO即构
专注音视频领域19年 2020.04.15 加入
全球领先的音视频云服务商,已为映客、酷狗、喜马拉雅、荔枝、好未来、作业帮、掌门一对一、Live.Me、UpLive、Mico、平安科技等众多行业头部企业提供音视频云服务。











评论