• 鸿蒙应用开发学习:使用视频播放(Video)组件播放视频和音频文件


    一、前言

    播放音视频是手机的重要功能之一,近期我学习了在鸿蒙系统应用开发中实现音视频的播放功能,应用中使用到了视频播放(Video)组件,@ohos.file.picker(选择器)。特撰此文分享一下我的学习经历。

    二、参考资料

    本次开发学习,主要参考了以下三个资料。

    1.鸿蒙arkts的video组件简单使用示例

    第一个资料是在CSDN社区里搜到了一篇简绍Video组件使用的博文,有现成的代码示例,一开始我就是参考了这篇博文的代码开始了视频和音频播放的学习。

    2.官方开发文档(3.1/4.0)-指南-UI开发-添加组件-视频播放(Video)

    第二个资料是官方网站内Video组件的指南,关于如何实现播放手机内的视频和音频相关知识,我参考了这个指南中的一些内容。

    3.官方开发文档(3.1/4.0)-API参考-ArkTS接口参考-文件管理-@ohos.file.picker(选择器)

    第三个资料是官方网站提供的文件管理器接口资料,获取手机内的视频的路径和音频文件路径的方法来自于这篇文档。

    三、实现过程

    1.先照着别人的教程试着做

    最开始,我参照第一个链接中的内容,做了一个播放视频和音频的简单应用,利用Tabs组件制作了几个页面。其中标题为视频1和视频2的页面是参考了示例代码制作的,这两个页面都是播放的网络视频,视频地址来自于第一个链接提供的代码,在真机上测试可以正常播放。

    第一个页面的初始状态,带自定义的控制器,播放的网络视频,后面又做了改动

    第二个页面是Video组件的基础使用方法,播放的网络视频。

    第二个页面的代码

    1. TabContent() {
    2. Column({ space: 20 }) {
    3. Text("播放网络视频")
    4. .fontSize(14)
    5. .textAlign(TextAlign.Center)
    6. .width('100%')
    7. Video({
    8. src: "https://vd3.bdstatic.com/mda-pmj5ajqd7p4b6pgb/576p/h264/1703044058699262355/mda-pmj5ajqd7p4b6pgb.mp4?auth_key=1703138418-0-0-618ea72b33be241c96c6cff86c06e080&bcevod_channel=searchbox_feed&cr=1&cd=0&pd=1&pt=4&logid=0018430194&vid=9762003448174112444&abtest=all"
    9. }).width("100%").aspectRatio(1.3)
    10. }
    11. }.tabBar("视频2")

    第三个页面计划实现播放本地视频功能,但由于我没有示例中对应的视频资源,就没有照着做,需要另外想办法。第四个页面要实现播放音频功能,示例中给的音频链接不能正常访问,也无法正常播放,也需要另外想办法。

    2.对播放本地视频的页面进行改进

    参考资料中第一个链接里的示例中播放本地视频部分,需要将视频文件保存到项目的resources/rawfile文件夹下,等于是将视频写死了,而我希望通过浏览手机文件进行选择,实现播放手机内的任一本地视频的目的,因此需要对软件进行改进。

    首先,我对参考资料中第二个链接的内容进行了学习,该文档中关于加载本地视频的知识除了使用资源访问符$rawfile()引用视频资源方法外,还有通Data Ability提供的视频路径频路径访问本地视频的方法。但这个Data Ability组件,我看了相关资料也没弄明白怎么用。让此事进入了死胡同。

    但我之前学习了显示手机上的图片功能时,参考过一篇博文“【鸿蒙应用ArkTS开发系列】- 选择图片、文件和拍照功能实现”。这篇博文介绍了从手机相册和手机文件夹中选择图片,并在应用中显示的方法。我照着实现了该功能。该应用在显示图片信息的同时还会显示图片文件的Uri信息。我利用该功能从相册里选中视频文件后,虽然不能在应用中显示视频,但是视频的Uri信息确显示了出来。

    手机上的视频文件

    通过上述的显示手机图片的应用,从相册内选择视频文件

    虽然视频文件没有显示出来,但文件Uri还是提供出来了(这是另外一个显示图片的应用)。

    我尝试将Uri信息写入我的本地视频播放代码中,在真机上测试,可以播放(见下图)。

    关键代码

    1. Video({
    2. src: "datashare:///media/video/914" // 手机视频的路径
    3. }).width("100%").aspectRatio(1.3)

    通过上面的测试,说明只要获取到视频文件的Uri信息,并将其赋值给Video组件的src参数后,就可以播放了。接下来的开发步骤就是实现获取视频文件的Uri并赋值给Video组件的src参数。

    我通过对官方文档的研究发现,使用@ohos.file.picker (选择器)可以实现对视频和音频文件的选择(相关内容见第三个链接)。其中选择视频文件要用到PhotoViewPicker。并且文档中有相应的示例代码。我对自己的应用进行了如下修改:

    2.1 修改显示页面,添加了“选择文件”按钮组件和显示视频文件Uri的文本组件

    1. TabContent() {
    2. Column({ space: 20 }) {
    3. Text("播放手机视频")
    4. .fontSize(14)
    5. .textAlign(TextAlign.Center)
    6. .width('100%')
    7. Video({
    8. src: this.videoUri // 手机上的视频文件路径
    9. }).width("100%").aspectRatio(1.3)
    10. Button("选择文件")
    11. .onClick(() => {
    12. this.VideoPicker() // 自定义函数获取本地视频路径
    13. })
    14. Text("文件路径" + this.videoUri) // 显示视频文件路径
    15. .fontSize(14)
    16. .textAlign(TextAlign.Center)
    17. .width('100%')
    18. }
    19. }.tabBar("视频3")

    2.2 添加了使用PhotoViewPicker选择器获取视频文件路径的函数

    1. async VideoPicker() {
    2. try {
    3. let PhotoSelectOptions = new picker.PhotoSelectOptions();
    4. PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE; // 文件类型 .IMAGE_TYPE
    5. PhotoSelectOptions.maxSelectNumber = 1; // 可选择的文件数量
    6. let photoPicker = new picker.PhotoViewPicker();
    7. photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult) => {
    8. console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
    9. this.videoUri = PhotoSelectResult.photoUris[0] // 获取视频文件路径
    10. }).catch((err) => {
    11. console.error('PhotoViewPicker.select failed with err: ' + err);
    12. });
    13. } catch (err) {
    14. console.error('PhotoViewPicker failed with err: ' + err);
    15. }
    16. }

    修改代码后,就可以选择手机上的视频文件进行播放了。

    3.对播放音频的页面进行改进

    由于参考资料中第一个链接提供的网络音频文件不能正常访问,因此,我决定将播放音频的页面修改为播放本地音频。从手机获取上获取音频文件路径的方法是通过@ohos.file.picker (选择器)的AudioViewPicker来实现的,链接三的文档中同样有示例代码,复制过来稍加修改就可以了。

    3.1页面代码修改如下

    1. TabContent() {
    2. Column({ space: 20 }) {
    3. Text("播放手机音频")
    4. .fontSize(14)
    5. .textAlign(TextAlign.Center)
    6. .width('100%')
    7. Video({
    8. src: this.audioUri // 手机上的音频文件路径
    9. }).width('100%').aspectRatio(1.3)
    10. Button("选择文件")
    11. .onClick(() => {
    12. this.AudioPicker() // 自定义函数获取本地音频路径
    13. })
    14. Text("文件路径" + this.audioUri) // 显示音频文件路径
    15. .fontSize(14)
    16. .textAlign(TextAlign.Center)
    17. .width('100%')
    18. }
    19. }.tabBar("音频")

    3.2 添加了使用PhotoViewPicker选择器选择视频文件的函数

    1. async AudioPicker() {
    2. try {
    3. let AudioSelectOptions = new picker.AudioSelectOptions();
    4. let audioPicker = new picker.AudioViewPicker();
    5. audioPicker.select(AudioSelectOptions).then((AudioSelectResult) => {
    6. console.info('AudioViewPicker.select successfully, AudioSelectResult uri: ' + JSON.stringify(AudioSelectResult));
    7. this.audioUri = AudioSelectResult[0] // 获取音频文件路径
    8. }).catch((err) => {
    9. console.error('AudioViewPicker.select failed with err: ' + err);
    10. });
    11. } catch (err) {
    12. console.error('AudioViewPicker failed with err: ' + err);
    13. }
    14. }

    修改代码后,就可以对手机上的音频文件进行选择和播放了(见下图)。

    4.对第一个页面的进行调整

    在观看了官网上关于Video组件的文档资料后(链接二),我又尝试着对第一个页面内进行了一些修改,主要是取消了默认控制器的显示;增加了显示播放进度的组件;调整了页面布局;将“移动进度到”按钮对应的进度参数从固定值“30”,改为了变量,增加了文本输入框,修改这个变量,使得该功能更灵活。

    代码修改后:

    1. TabContent() {
    2. Column({ space: 20 }) {
    3. Text("播放网络视频-带控制器")
    4. .fontSize(14)
    5. .textAlign(TextAlign.Center)
    6. .width('100%')
    7. Video({
    8. controller: this.controller,
    9. currentProgressRate: this.speed,
    10. src: "https://vd4.bdstatic.com/mda-phegibuu9ba9nrpe/hd/cae_h264/1692171953345322752/mda-phegibuu9ba9nrpe.mp4?auth_key=1703502162-0-0-6fd1550e28060e61121ed6d2a773b68c&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=0162714477&vid=1316841417577475878&abtest=all"
    11. })
    12. .controls(false)
    13. .width("100%")
    14. .aspectRatio(1.4)
    15. .onPrepared((event) => {
    16. this.durationTime = event.duration // 获取视频总时长
    17. })
    18. .onUpdate((event) => {
    19. this.currentTime = event.time // 视频播放时获取当前时间
    20. })
    21. // 播放进度显示
    22. Row() {
    23. Text(JSON.stringify(this.currentTime) + 's') // 文本形式显示播放进度
    24. // 滑动条形式显示播放进度
    25. Slider({
    26. value: this.currentTime,
    27. min: 0,
    28. max: this.durationTime
    29. })
    30. .onChange((value: number, mode: SliderChangeMode) => {
    31. this.controller.setCurrentTime(value);
    32. }).layoutWeight(1)
    33. Text(JSON.stringify(this.durationTime) + 's')
    34. }.width("100%")
    35. // 播放倍速滑动选择
    36. Row() {
    37. Slider({
    38. value: this.speed,
    39. min: 0.75,
    40. step: 0.25,
    41. max: 2,
    42. style: SliderStyle.InSet
    43. }).layoutWeight(1)
    44. .showSteps(true)
    45. .onChange(value => {
    46. this.speed = value
    47. })
    48. Text(this.speed + "倍速") // 文本形式显示播放速率信息
    49. .fontSize(14)
    50. .textAlign(TextAlign.Center)
    51. }.width("100%")
    52. // 自定义播放控制器
    53. Row({ space: 10 }) {
    54. Button("播放")
    55. .onClick(() => {
    56. this.controller.start()
    57. })
    58. Button("暂停")
    59. .onClick(() => {
    60. this.controller.pause()
    61. })
    62. Button("结束")
    63. .onClick(() => {
    64. this.controller.stop()
    65. })
    66. }
    67. // 移动进度控制
    68. Row({ space: 10 }) {
    69. Button("移动进度到")
    70. .onClick(() => {
    71. this.controller.setCurrentTime(parseInt(this.jumpToTime)) // 设置视频控制器当前时间,单位为秒
    72. })
    73. TextInput({ text: this.jumpToTime }).layoutWeight(1) // 文本输入框,输入要移动到的进度位置
    74. .onChange((value: string) => {
    75. this.jumpToTime = value
    76. })
    77. Text("s")
    78. }.width('60%')
    79. }.width('100%')
    80. }.tabBar("视频1")
    5.遗留的三个问题

    开发学习到这里,让我对Video组件和@ohos.file.picker (选择器)有了一些基础的了解,但现在还存在三个问题需要在之后的学习中去解决。

    第一个问题是播放网络视频都是在代码中写死了的。原因是,我暂时还没有办法从视频网站上获取到视频文件的网络地址,只能先按着别人的教程做,目的是验证功能,没有什么实际意义。

    第二个问题是点击Video组件默认控制器右侧的全屏按钮后视频放大,如果视频是横屏,手机不会自动以横屏显示视频,且视频显示显示不全(如下图),对此,暂时没有找到相关的解决办法。


    全屏显示后异常

    第三个问题是经过测试发现,当前的代码不适合播放竖屏形式的视频,原因是Video组件内设置的长宽比为1.3或1.4(即横屏)。如果视频是竖屏形式的,在Video组件中播放时,组件会将视频上部和下部裁切掉,只保留中间的部分。

    这三个问题,希望能在不久的将来找到解决方法,使得视频播放功能更实用。先记录在这里以备忘。

    四、代码展示

    最后展示本次学习的成果

    1. import picker from '@ohos.file.picker';
    2. @Entry
    3. @Component
    4. struct EasyVideoPlayPage {
    5. @State videoUri: string = "" // 视频文件地址
    6. @State audioUri: string = "" // 音频文件地址
    7. @State speed: number = 1 // 视频播放速率,默认为1倍速
    8. @State currentTime: number = 0; // 当前时间(秒)
    9. @State durationTime: number = 0; // 视频总时长(秒)
    10. @State jumpToTime: string = "0"; // 移动进度到(秒)
    11. controller: VideoController = new VideoController() // 视频控制器
    12. build() {
    13. Column() {
    14. Tabs() {
    15. TabContent() {
    16. Column({ space: 20 }) {
    17. Text("播放网络视频-带控制器")
    18. .fontSize(14)
    19. .textAlign(TextAlign.Center)
    20. .width('100%')
    21. Video({
    22. controller: this.controller,
    23. currentProgressRate: this.speed,
    24. src: "https://vd4.bdstatic.com/mda-phegibuu9ba9nrpe/hd/cae_h264/1692171953345322752/mda-phegibuu9ba9nrpe.mp4?auth_key=1703502162-0-0-6fd1550e28060e61121ed6d2a773b68c&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=0162714477&vid=1316841417577475878&abtest=all"
    25. })
    26. .controls(false)
    27. .width("100%")
    28. .aspectRatio(1.4)
    29. .onPrepared((event) => {
    30. this.durationTime = event.duration // 获取视频总时长
    31. })
    32. .onUpdate((event) => {
    33. this.currentTime = event.time // 视频播放时获取当前时间
    34. })
    35. // 播放进度显示
    36. Row() {
    37. Text(JSON.stringify(this.currentTime) + 's') // 文本形式显示播放进度
    38. // 滑动条形式显示播放进度
    39. Slider({
    40. value: this.currentTime,
    41. min: 0,
    42. max: this.durationTime
    43. })
    44. .onChange((value: number, mode: SliderChangeMode) => {
    45. this.controller.setCurrentTime(value);
    46. }).layoutWeight(1)
    47. Text(JSON.stringify(this.durationTime) + 's')
    48. }.width("100%")
    49. // 播放倍速滑动选择
    50. Row() {
    51. Slider({
    52. value: this.speed,
    53. min: 0.75,
    54. step: 0.25,
    55. max: 2,
    56. style: SliderStyle.InSet
    57. }).layoutWeight(1)
    58. .showSteps(true)
    59. .onChange(value => {
    60. this.speed = value
    61. })
    62. Text(this.speed + "倍速") // 文本形式显示播放速率信息
    63. .fontSize(14)
    64. .textAlign(TextAlign.Center)
    65. }.width("100%")
    66. // 自定义播放控制器
    67. Row({ space: 10 }) {
    68. Button("播放")
    69. .onClick(() => {
    70. this.controller.start()
    71. })
    72. Button("暂停")
    73. .onClick(() => {
    74. this.controller.pause()
    75. })
    76. Button("结束")
    77. .onClick(() => {
    78. this.controller.stop()
    79. })
    80. }
    81. // 移动进度控制
    82. Row({ space: 10 }) {
    83. Button("移动进度到")
    84. .onClick(() => {
    85. this.controller.setCurrentTime(parseInt(this.jumpToTime)) // 设置视频控制器当前时间,单位为秒
    86. })
    87. TextInput({ text: this.jumpToTime }).layoutWeight(1) // 文本输入框,输入要移动到的进度位置
    88. .onChange((value: string) => {
    89. this.jumpToTime = value
    90. })
    91. Text("s")
    92. }.width('60%')
    93. }.width('100%')
    94. }.tabBar("视频1")
    95. TabContent() {
    96. Column({ space: 20 }) {
    97. Text("播放网络视频")
    98. .fontSize(14)
    99. .textAlign(TextAlign.Center)
    100. .width('100%')
    101. Video({
    102. src: "https://vd3.bdstatic.com/mda-pmj5ajqd7p4b6pgb/576p/h264/1703044058699262355/mda-pmj5ajqd7p4b6pgb.mp4?auth_key=1703138418-0-0-618ea72b33be241c96c6cff86c06e080&bcevod_channel=searchbox_feed&cr=1&cd=0&pd=1&pt=4&logid=0018430194&vid=9762003448174112444&abtest=all"
    103. }).width("100%").aspectRatio(1.3)
    104. }
    105. }.tabBar("视频2")
    106. TabContent() {
    107. Column({ space: 20 }) {
    108. Text("播放手机视频")
    109. .fontSize(14)
    110. .textAlign(TextAlign.Center)
    111. .width('100%')
    112. Video({
    113. src: this.videoUri // 手机上的视频文件路径
    114. }).width("100%").aspectRatio(1.3)
    115. Button("选择文件")
    116. .onClick(() => {
    117. this.VideoPicker() // 自定义函数获取本地视频路径
    118. })
    119. Text("文件路径" + this.videoUri) // 显示视频文件路径
    120. .fontSize(14)
    121. .textAlign(TextAlign.Center)
    122. .width('100%')
    123. }
    124. }.tabBar("视频3")
    125. TabContent() {
    126. Column({ space: 20 }) {
    127. Text("播放手机音频")
    128. .fontSize(14)
    129. .textAlign(TextAlign.Center)
    130. .width('100%')
    131. Video({
    132. src: this.audioUri // 手机上的音频文件路径
    133. }).width('100%').aspectRatio(1.3)
    134. Button("选择文件")
    135. .onClick(() => {
    136. this.AudioPicker() // 自定义函数获取本地音频路径
    137. })
    138. Text("文件路径" + this.audioUri) // 显示音频文件路径
    139. .fontSize(14)
    140. .textAlign(TextAlign.Center)
    141. .width('100%')
    142. }
    143. }.tabBar("音频")
    144. }.animationDuration(500)
    145. }
    146. .justifyContent(FlexAlign.Start)
    147. .width('100%')
    148. .backgroundColor('#EAEAEA')
    149. .padding(10) // 根 Column -end
    150. }
    151. // 视频文件选择器:代码来源于官方开发文档-API参考-ArkTS接口参考-文件管理-@ohos.file.picker-PhotoViewPicker,对源码进行了修改
    152. async VideoPicker() {
    153. try {
    154. let PhotoSelectOptions = new picker.PhotoSelectOptions();
    155. PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE; // 文件类型 .IMAGE_TYPE
    156. PhotoSelectOptions.maxSelectNumber = 1; // 可选择的文件数量
    157. let photoPicker = new picker.PhotoViewPicker();
    158. photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult) => {
    159. console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
    160. console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult.photoUris[0]));
    161. this.videoUri = PhotoSelectResult.photoUris[0] // 获取视频文件路径
    162. }).catch((err) => {
    163. console.error('PhotoViewPicker.select failed with err: ' + err);
    164. });
    165. } catch (err) {
    166. console.error('PhotoViewPicker failed with err: ' + err);
    167. }
    168. }
    169. // 音频文件选择器:代码来源于官方开发文档-API参考-ArkTS接口参考-文件管理-@ohos.file.picker-AudioViewPicker,对源码进行了修改
    170. async AudioPicker() {
    171. try {
    172. let AudioSelectOptions = new picker.AudioSelectOptions();
    173. let audioPicker = new picker.AudioViewPicker();
    174. audioPicker.select(AudioSelectOptions).then((AudioSelectResult) => {
    175. console.info('AudioViewPicker.select successfully, AudioSelectResult uri: ' + JSON.stringify(AudioSelectResult));
    176. this.audioUri = AudioSelectResult[0] // 获取音频文件路径
    177. }).catch((err) => {
    178. console.error('AudioViewPicker.select failed with err: ' + err);
    179. });
    180. } catch (err) {
    181. console.error('AudioViewPicker failed with err: ' + err);
    182. }
    183. }
    184. }

  • 相关阅读:
    电线延长寿命小妙招
    Python:函数篇(每周练习)
    【全开源】进销存订货通管理系统(FastAdmin+ThinkPHP+Layui)
    《A Biography of the Pixel》摘阅
    【Node.js】深度解析搭建后台服务器-http模块
    Linux 本地zabbix结合内网穿透工具实现安全远程访问浏览器
    GoLong的学习之路(六)语法之指针
    gem5 编译与环境搭建
    Java 解决long类型数据在前后端传递失真问题
    如何使用Python实现发送邮件功能
  • 原文地址:https://blog.csdn.net/bahamutj/article/details/136590575