简易视频播放器(案例)

介绍

img

本篇Codelab使用ArkTS语言实现视频播放器,主要包括主界面和视频播放界面,我们将一起完成以下功能:

  1. 主界面顶部使用Swiper组件实现视频海报轮播。
  2. 主界面下方使用List组件实现视频列表。
  3. 播放界面使用Video组件实现视频播放。
  4. 在不使用视频组件默认控制器的前提下,实现自定义控制器。
  5. 播放界面底部使用图标控制视频播放/暂停。
  6. 播放界面底部使用Slider组件控制和实现视频播放进度。
  7. 播放界面使用Stack容器组件的Z序控制在视频播放画面上展示开始/暂停/加载图标。

主界面中最近播放和为你推荐列表播放网络视频,需将CommonConstants.ets中的NET属性修改为网络视频地址。

相关概念

  • Swiper组件:滑块视图容器,提供子组件滑动轮播显示的能力。
  • List组件:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
  • Video组件:用于播放视频文件并控制其播放状态的组件。
  • Navigator组件:路由容器组件,提供路由跳转能力。
  • ForEach组件:ForEach基于数组类型数据执行循环渲染。

完整示例

gitee源码地址

源码下载

简易视频播放器(ArkTS).zip

代码结构解读

本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。

├──entry/src/main/ets             // 代码区
│  ├──common
│  │  └──constants
│  │     └──CommonConstants.ets   // 样式常量类
│  ├──entryability
│  │  └──EntryAbility.ts          // 程序入口类
│  ├──model
│  │  └──VideoControll.ets        // 视频播放控制相关方法类
│  ├──pages
│  │  ├──SimpleVideoIndex.ets     // 主界面
│  │  └──SimpleVideoPlay.ets      // 视频播放界面
│  ├──view
│  │  ├──IndexModule.ets          // 自定义首页List模块组件文件
│  │  ├──IndexSwiper.ets          // 自定义首页Swiper组件文件
│  │  ├──VideoPlayer.ets          // 自定义播放界面视频组件文件
│  │  └──VideoPlaySlider.ets      // 自定义播放界面视频进度条组件文件
│  └──viewmodel
│     ├──HorizontalVideoItem.ets  // 水平视频类
│     ├──ParamItem.ets            // 参数类
│     ├──SwiperVideoItem.ets      // banner视频类
│     └──VideoData.ets            // 首页相关数据
└──entry/src/main/resource        // 应用静态资源目录

构建主界面

主界面由视频轮播模块和多个视频列表模块组成,效果图如图:

img

VideoData.ets中定义的视频轮播图数组SWIPER_VIDEOS和视频列表图片数组HORIZONTAL_VIDEOS。

// VideoData.ets
import { HorizontalVideoItem } from './HorizontalVideoItem';
import { SwiperVideoItem } from './SwiperVideoItem';

export const SWIPER_VIDEOS: SwiperVideoItem[] = [
  new SwiperVideoItem($r('app.media.banner1')),
  new SwiperVideoItem($r('app.media.banner2')),
  new SwiperVideoItem($r('app.media.banner3'))
];

export const HORIZONTAL_VIDEOS: HorizontalVideoItem[] = [
  new HorizontalVideoItem(1, $r('app.media.video_list0'), '视频1'),
  new HorizontalVideoItem(2, $r('app.media.video_list1'), '视频2'),
  new HorizontalVideoItem(3, $r('app.media.video_list2'), '视频3')
];

IndexSwiper.ets文件中定义的轮播图子组件SwiperVideo,点击轮播图片,页面跳转到视频播放页面,并携带本地视频flag,效果图如图:

img

// IndexSwiper.ets
@Component
export struct SwiperVideo {
  build() {
    Column() {
      Swiper() {
        ForEach(SWIPER_VIDEOS, (item: SwiperVideoItem) => {
          SwiperItem({ imageSrc: item.image, source: $rawfile('videoTest.mp4') })
        }, (item: SwiperVideoItem) => JSON.stringify(item))
      }
      .autoPlay(true)
    }
    // 样式设置
    ...
  }
}

@Component
struct SwiperItem {
  private imageSrc: Resource = $r('app.string.empty');
  private source: Resource = $r('app.string.empty');
  private paramItem: ParamItem = new ParamItem();
  ...

  build() {
    // 跳转一:使用Navigator组件跳转到视频播放界面
    Navigator({ target: SECOND_PAGE, type: NavigationType.Push }) {
      Image(this.imageSrc)
        .borderRadius(MARGIN_FONT_SIZE.FIRST_MARGIN)
    }
    .params(this.paramItem)
  }
}

IndexModule.ets文件中定义的视频列表图片子组件VideoModule,点击子组件中的图片,页面跳转到视频播放页面,并携带网络视频flag,效果图如图:

img

// IndexModule.ets
@Component
export struct VideoModule {
  private moduleName: string = '';

  build() {
    Column() {
      // 视频列表上方的文本信息
      ...
      // 视频列表组件
      List({ space: MARGIN_FONT_SIZE.FIRST_MARGIN }) {
        ForEach(HORIZONTAL_VIDEOS, (item: HorizontalVideoItem) => {
          ListItem() {
            HorizontalItem({
              imageSrc: item.image,
              source: NET,
              videoName: item.name
            })
          }
        }, (item: HorizontalVideoItem) => JSON.stringify(item))
      }
      // 设置列表横向排列
      .listDirection(Axis.Horizontal)
    }
    // 样式设置
    ...
  }
}

@Component
struct HorizontalItem {
  private imageSrc: Resource = $r('app.string.empty');
  private source: string = '';
  private videoName: string = '';

  build() {
    // 跳转二:使用route跳转到视频播放界面
    Column() {
      Image(this.imageSrc)
        .width(MARGIN_FONT_SIZE.SEVENTH_MARGIN)
        .height(MARGIN_FONT_SIZE.SIXTH_MARGIN)
        .onClick(() => {
          router.pushUrl({
            url: SECOND_PAGE,
            params: { source: this.source }
          });
        })
     ...
    }
    .justifyContent(FlexAlign.Center)
  }
}

在SimpleVideoIndex.ets主界面中引用SwiperVideo和VideoModule子组件。

// SimpleVideoIndex.ets
@Entry
@Component
struct SimpleVideoIndex {
  build() {
    Column({ space: MARGIN_FONT_SIZE.FOURTH_MARGIN }) {
      // 视频轮播组件
      SwiperVideo()
      List() {
        ForEach(LIST, (item: string) => {
          ListItem() {
            VideoModule({ moduleName: item })
              .margin({ top: MARGIN_FONT_SIZE.FIRST_MARGIN })
          }
        }, (item: string) => JSON.stringify(item))
      }
      .listDirection(Axis.Vertical)
      .margin({ top: MARGIN_FONT_SIZE.THIRD_MARGIN })
    }
    ...
  }
}

构建视频播放界面

VideoPlayer.ets其中定义了视频播放子组件VideoPlayer ,onPrepared回调方法中可以获取视频总时长,onUpdate回调方法中可实时获取到视频播放的当前时间戳,onFinish是视频播放结束后的回调方法,onError是视频播放出错的回调方法。

// VideoPlayer.ets
@Component
export struct VideoPlayer {
  private source: string | Resource = '';
  private controller: VideoController = new VideoController();
  private previewUris: Resource = $r('app.media.preview');
  @Provide currentTime: number = 0;
  @Provide durationTime: number = 0;
  @Provide durationStringTime: string = START_TIME;
  @Provide currentStringTime: string = START_TIME;
  @Consume isPlay: boolean;
  @Consume isOpacity: boolean;
  @Consume flag: boolean;
  @Consume isLoading: boolean;
  @Consume progressVal: number;

  build() {
    Column() {
      Video({
        src: this.source,
        previewUri: this.previewUris,
        controller: this.controller
      })
        .width(ALL_PERCENT)
        .height(STRING_PERCENT.NINETY_PERCENT)
        .controls(false)
        .autoPlay(false)
        .objectFit(ImageFit.Contain)
        .loop(false)
        .onUpdate((event) => {
          if (event) {
            this.currentTime = event.time;
            this.currentStringTime = changeSliderTime(this.currentTime);
          }
        })
        .onPrepared((event) => {
          this.prepared(event?.duration);
        })
        .onFinish(() => {
          this.finish();
        })
        .onError(() => {
          prompt.showToast({
            duration: COMMON_NUM_DURATION,
            message: MESSAGE
          });
        })
      VideoSlider({ controller: this.controller })
    }
  }
  ...
}

在自定义组件VideoPlayer底部使用了自定义子组件VideoSlider,VideoSlider自定义组件中显示和控制视频播放进度,效果图如图:

img

// VideoPlaySlider.ets
@Component
export struct VideoSlider {
  @Consume isOpacity: boolean;
  private controller: VideoController = new VideoController();
  @Consume currentStringTime: string;
  @Consume currentTime: number;
  @Consume durationTime: number;
  @Consume durationStringTime: string;
  @Consume isPlay: boolean;
  @Consume flag: boolean;
  @Consume isLoading: boolean;
  @Consume progressVal: number;

  build() {
    Row({ space: MARGIN_FONT_SIZE.FIRST_MARGIN }) {
      ...
      Slider({
        value: this.currentTime,
        min: 0,
        max: this.durationTime,
        step: 1,
        style: SliderStyle.OutSet
      })
        .blockColor($r('app.color.white'))
        .width(STRING_PERCENT.SLIDER_WITH)
        .trackColor(Color.Gray)
        .selectedColor($r("app.color.white"))
        .showSteps(true)
        .showTips(true)
        .trackThickness(this.isOpacity ? SMALL_TRACK_THICK_NESS : BIG_TRACK_THICK_NESS)
        .onChange((value: number, mode: SliderChangeMode) => {
          this.sliderOnchange(value, mode);
        })
      ...
    }
    .opacity(this.isOpacity ? DEFAULT_OPACITY : 1)
    ...
  }
  ...
}

在VideoController.ets中的视频控制和回调的相关方法。

// VideoControll.ets
export function changeSliderTime(value: number): string {
  let second: number = value % COMMON_NUM_MINUTE;
  let min: number = Number.parseInt((value / COMMON_NUM_MINUTE).toString());
  let head = min < COMMON_NUM_DOUBLE ? `${ZERO_STR}${min}` : min;
  let end = second < COMMON_NUM_DOUBLE ? `${ZERO_STR}${second}` : second;
  let nowTime = `${head}${SPLIT}${end}`;
  return nowTime;
}

在SimpleVideoPlay.ets播放界面,引用VideoPlayer子组件,并在视频播放页面使用堆叠容器,在视频播放画面中心堆叠控制、视频加载图标,效果图如图:

img

// SimpleVideoPlay.ets
@Entry
@Component
struct Play {
  // 取到Index页面跳转来时携带的source对应的数据。
  private source: string = (router.getParams() as Record<string, Object>).source as string;
  private startIconResource: Resource = $r('app.media.ic_public_play');
  private backIconResource: Resource = $r('app.media.ic_back');
  @Provide isPlay: boolean = false;
  @Provide isOpacity: boolean = false;
  controller: VideoController = new VideoController();
  @Provide isLoading: boolean = false;
  @Provide progressVal: number = 0;
  @Provide flag: boolean = false;

  ...
  onPageHide() {
    this.controller.pause();
  }

  build() {
    Column() {
      // 顶部返回以及标题
    ...
      Stack() {
        // 不同的播放状态渲染不同得控制图片
        if (!this.isPlay && !this.isLoading) {
          Image(this.startIconResource)
            .width(MARGIN_FONT_SIZE.FIFTH_MARGIN)
            .height(MARGIN_FONT_SIZE.FIFTH_MARGIN)
            // 同一容器中兄弟组件显示层级关系,z值越大,显示层级越高 用于控制图片在视频上。
            .zIndex(STACK_STYLE.IMAGE_Z_INDEX)
        }
        if (this.isLoading) {
          Progress({
            value: STACK_STYLE.PROGRESS_VALUE,
            total: STACK_STYLE.PROGRESS_TOTAL,
            type: ProgressType.ScaleRing
          })
            .color(Color.Grey)
            .value(this.progressVal)
            .width(STACK_STYLE.PROGRESS_WIDTH)
            .style({
              strokeWidth: STACK_STYLE.PROGRESS_STROKE_WIDTH,
              scaleCount: STACK_STYLE.PROGRESS_SCALE_COUNT,
              scaleWidth: STACK_STYLE.PROGRESS_SCALE_WIDTH
            })
            .zIndex(STACK_STYLE.PROGRESS_Z_INDEX)
        }
        VideoPlayer({
          source: this.source,
          controller: this.controller
        })
          .zIndex(0)
      }
    }
    .height(ALL_PERCENT)
    .backgroundColor(Color.Black)
  }
}

总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

  1. Swiper组件的使用。
  2. List组件的使用。
  3. Video组件的使用。
  4. Slider组件的使用。
  5. 如何实现自定义视频控制器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/298198.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

程序员必知!命令模式的实战应用与案例分析

命令模式是一种行为设计模式&#xff0c;它将请求封装为对象以实现客户端参数化、请求排队、日志记录及撤销操作&#xff0c;旨在解耦调用者与操作实现者&#xff0c;以智能家居为例&#xff0c;用户通过界面发送命令对象&#xff0c;设备作为接收者执行相应操作&#xff0c;无…

物理与网络安全

物流环境安全 场地选择考虑抗震、承重、防火、防水、供电、空气调节、电磁防护、雷击及静电 场地因素&#xff1a; 自然灾害&#xff0c;社会因素&#xff08;加油站、化工厂&#xff09;&#xff0c;配套条件&#xff08;消防&#xff0c;交通&#xff0c;电力&#xff0c;…

QT项目中添加资源文件和使用qss样式

时间记录&#xff1a;2024/1/6 一、添加使用资源文件 1.1 创建项目并打开项目&#xff08;带ui界面的项目&#xff09; 1.2 使用快捷键Ctrln创建文件&#xff0c;选择"QT"下面的"QT Resource File" 1.3 输入文件名和文件保存路径 1.4 将需要添加的文件…

钡铼网关 只需一台,解锁智慧无人搅拌站系统,绿色又环保

行业需求 为了提高搅拌站的自动化&#xff0c;减少人工繁琐的操作&#xff0c;同时记录物料的增减记录&#xff0c;实现对于物料从进场到出场的全周期管理。 系统介绍 针对搅拌站各个环节的需求大致相同&#xff0c;市场中逐渐流行一整套基本的智慧搅拌站解决方案。各种搅拌站…

水母目标检测数据集VOC格式500张

水母&#xff0c;一种美丽而神秘的海洋生物&#xff0c;以其独特的形态和生态习性而备受瞩目。 水母的体型呈伞状&#xff0c;身体透明&#xff0c;有各种颜色和花纹。它们没有骨骼&#xff0c;身体由胶状物质组成&#xff0c;非常柔软和脆弱。水母通过触手上的刺细胞释放毒素…

哪个洗地机质量好?2024年洗地机推荐

如今&#xff0c;家用洗地机已经成为越来越多家庭的不可或缺之物。然而&#xff0c;在市场上琳琅满目的洗地机品牌和型号中&#xff0c;究竟哪一个性价比高、质量好&#xff0c;很多消费者常常陷入选择困难。为了帮助大家能够更加轻松地购买到适合自己的洗地机&#xff0c;我们…

dotdotdot插件快速实现多行文本的省略

jQuery.dotdotdot 前言 在“css新增文本样式&#xff08;完整&#xff09;”这篇&#xff0c;我们介绍了text-overflow属性省略多余的文本。用text-overflow属性可以直接省略单行文本&#xff0c;但省略多行文本&#xff0c;单独使用CSS是无法实现&#xff0c;今天我们介绍一…

【Linux 内核源码分析】关于Linux内核源码目录结构

Linux内核源码采用树形结构。功能相关的文件放到不同的子目录下面&#xff0c;使程序更具有可读行。 使用Source Insight打开源码&#xff0c;如下图所示&#xff0c;可以看到源码是树形结构。 目录含义描述arch存放与体系结构相关的代码&#xff0c;包括不同硬件平台的特定代…

深度解析分布式算法:构建高效稳定的分布式系统

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

高校选课系统需求分析开发源码

高校学生选课报名系统包括学生、教师和管理员三方的功能需求&#xff0c;学生的需求是查询院系的课程、选课情况及个人信息修改&#xff1b;教师则需要查看和查询所有课程信息及自己的课程信息以及教师信息的修改&#xff1b;管理员则负责更为复杂的任务&#xff0c;包括对学生…

2023年阿里云云栖大会:前沿技术发布与未来展望

在2023年的阿里云云栖大会上&#xff0c;我见证了云计算和人工智能领域的又一历史性时刻。这次大会不仅是对未来科技趋势的一次深入探索&#xff0c;更是阿里云技术实力和创新能力的集中展示。 首先&#xff0c;千亿级参数规模的大模型通义千问2.0的发布&#xff0c;无疑将人工…

【大厂秘籍】系列 - Java多线程面试题

Java多线程面试题 友情提示&#xff0c;看完此文&#xff0c;在Java多线程这块&#xff0c;基本上可以吊打面试官了 线程和进程的区别 进程是资源分配的最小单位&#xff0c;线程是CPU调度的最小单位 线程是进程的子集&#xff0c;一个进程可以有很多线程&#xff0c;每条线…

【MongoDB】MongoDB查询语句find的使用,和提高查询速度的游标的使用,非常详细!!!

&#x1f601; 作者简介&#xff1a;一名大四的学生&#xff0c;致力学习前端开发技术 ⭐️个人主页&#xff1a;夜宵饽饽的主页 ❔ 系列专栏&#xff1a;MongoDB数据库 &#x1f450;学习格言&#xff1a;成功不是终点&#xff0c;失败也并非末日&#xff0c;最重要的是继续前…

MySQL基础篇(三)约束

一、概述 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 目的&#xff1a;保证数据库中数据的正确、有效性和完整性。 分类&#xff1a; 注意&#xff1a;约束是作用于表中字段上的&#xff0c;可以在创建表/修改表的时候添加约束。 二…

Java BIO、NIO、AIO、Netty知识详解(值得珍藏)

1. 什么是IO Java中I/O是以流为基础进行数据的输入输出的&#xff0c;所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。 在Java类库中&#xff0c;IO部分的内容是很庞大的&#xff0c;因为它涉及的领…

【每日一题】在链表中插入最大公约数

文章目录 Tag题目来源解题思路方法一&#xff1a;迭代 写在最后 Tag 【迭代】【辗转相除法】【链表】【2024-01-06】 题目来源 2807. 在链表中插入最大公约数 解题思路 方法一&#xff1a;迭代 思路 首先需要求两个数的最大公约数&#xff0c;使用辗转相除法。实现代码如下…

PyQT 多进程

在PyQt中&#xff0c;图形化界面&#xff08;GUI&#xff09;是运行在主线程中的&#xff0c;而多进程是在独立的进程中执行的。默认情况下&#xff0c;多进程之间是无法直接共享图形化界面的。 然而&#xff0c;有几种方法可以在多进程中与PyQt的图形化界面进行通信&#xff…

【大数据】Zookeeper 客户端的命令行操作

Zookeeper 客户端的命令行操作 1.显示某个路径下的所有节点&#xff1a;ls2.显示某个路径下的所有节点&#xff0c;以及当前节点的详细信息&#xff1a;ls23.创建节点&#xff1a;create4.创建临时节点&#xff1a;create -e5.创建顺序&#xff08;带编号&#xff09;节点&…

程序员副业之无人直播助眠

介绍和概览 大家好&#xff0c;我是小黑&#xff0c;本文给大家介绍一个比较轻松简单的副业&#xff0c;无人直播助眠副业。 这个项目的核心就是通过直播一些助眠素材来赚钱。比如你可以放一些舒缓的雨声之类的&#xff0c;吸引观众进来。然后&#xff0c;咱们可以挂个小程序…

DS|哈希查找

题目一&#xff1a;DS哈希查找 -- 线性探测再散列 题目描述&#xff1a; 定义哈希函数为H(key) key%11&#xff0c;输入表长&#xff08;大于、等于11&#xff09;。输入关键字集合&#xff0c;用线性探测再散列构建哈希表&#xff0c;并查找给定关键字。 输入要求&#xf…