# SwipePlayer **Repository Path**: allenymt/swipe-player ## Basic Information - **Project Name**: SwipePlayer - **Description**: HarmonyOS短视频流畅滑动解决方案 - **Primary Language**: TypeScript - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 12 - **Created**: 2024-12-11 - **Last Updated**: 2024-12-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 基于SwipePlayer三方库的短视频流畅点播切换场景解决方案 ## SwipePlayer简介 SwipePlayer三方库主要聚焦短视频流畅点播切换场景,提供短视频上下滑动切换和快速起播能力,同时通过自定义能力的开放满足应用不同短视频滑动场景的业务诉求。 SwipePlayer底层封装了系统AVPlayer及XComponent、Swiper、LazyForEach组件,基于AVPlayer的预创建、预加载以及提前起播实现了快速起播的能力。 SwipePlayer三方库通过SwipePlayer组件以及SwipePlayerController控制器对外提供能力,包括上下滑动、快速起播、动态添加数据源、 自定义跟随视频滑动的组件、自定义滑动组件切换、全屏和弹框等,目的是让开发者在开发过程中无需关注繁琐的AVPlayer创建、管理和播放逻辑,可以基于SwipePlayer 库能力快速实现短视频流畅滑动的场景开发体验,使开发者可以更加聚焦实际场景业务的开发。 ## 特性 + 支持网络视频(http/https)上下滑动及快速起播 + 支持动态添加/刷新播放源数据 + 支持自定义跟随视频滑动的组件 + 支持自定义滑动组件切换 + 支持自定义弹框(可自定义动画和弹框方向,提供默认动画实现) + 支持全屏/退出全屏播放,支持定义全屏播放界面组件 + 支持进度条(通过组件形式提供) ## 依赖版本 HarmonyOS 5.0.0 Release及以上 + 手机版本:5.0.0.102 ## 下载安装 使用ohpm安装依赖 ```shell ohpm install @hadss/swipeplayer ``` 或者按需在模块中配置运行时依赖,修改oh-package.json5 ```json5 { "dependencies": { "@hadss/swipeplayer": "^1.0.0-rc.2" } } ``` ## 快速开始 ### 支持网络视频(http/https)上下滑动及快速起播 定义控制器SwipePlayerController,初始化数据源,初始化SwipePlayer入口组件即可实现短视频滑动和快速起播能力。 ```extendtypescript import { util } from '@kit.ArkTS'; @Entry @Component struct Index { sourceUrls: Array = [ { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/headphones/freebuds-lipstick-2/videos/huawei-freebuds-lipstick-2-creative-design-id.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/wearables/watch-ultimate-new/video/huawei-watch-ultimate-pv.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/visions/v5-max-110/video/huawei-vision-v-5-max-pv.mp4' } ]; playerData:VideoData[] = []; // 1.定义控制器 swipePlayerController: SwipePlayerController = new SwipePlayerController(); dataSources: SwipePlayerIDataSource = new SwipePlayerIDataSource(); // 2.创建播放源 aboutToAppear(): void { this.sourceUrls.forEach((item, i) => { let tempData = new VideoData(); tempData.url = item.url; this.playerArr.push(tempData); }); this.dataSources.pushData(this.playerData); } build() { Column() { // 3.创建入口组件 SwipePlayer({ datasource: this.dataSources, swipePlayerController: this.swipePlayerController, }) } .width('100%') .height('100%') } } export interface MediaData { url: string; } export class VideoData implements PlayerData { url: string = ''; id: string = util.generateRandomUUID(true); getId(): string{ return this.id } getSource(): string { return this.url; } getCover(): string | undefined { return undefined; } getType(): string { if(this.url) { return 'video'; } else { return 'ad'; } } } ``` ### 支持动态添加/刷新播放源数据 ```extendtypescript @Entry @Component struct Index { sourceUrls: Array = [ { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/headphones/freebuds-lipstick-2/videos/huawei-freebuds-lipstick-2-creative-design-id.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/wearables/watch-ultimate-new/video/huawei-watch-ultimate-pv.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/visions/v5-max-110/video/huawei-vision-v-5-max-pv.mp4' } ]; playerData:VideoData[] = []; // 1.定义控制器 swipePlayerController: SwipePlayerController = new SwipePlayerController(); dataSources: SwipePlayerIDataSource = new SwipePlayerIDataSource(); // 2.创建播放源 aboutToAppear(): void { ... // 同上在此插入播放源 } refreshText(): string { if (this.offsetY > 200) { return '释放刷新'; } else { return '下拉刷新'; } } build() { Column() { // 3.创建入口组件 SwipePlayer({ datasource: this.dataSources, swipePlayerController: this.swipePlayerController, options: { swiperCallback: { onAnimationStart: (targetIndex: number) => { let totalNum = this.dataSources.totalCount(); if (totalNum - targetIndex <= 5) { this.dataSources.pushData(this.playerData); } }, onChange: (index: number) => { if (index === 0 || !index) { this.panDirection = PanDirection.Down; } else { this.panDirection = PanDirection.None; } this.isSelected = 1; } } } }) } .width('100%') .height('100%') // 下拉刷新视频 .parallelGesture( PanGesture({ fingers: 1, direction: this.panDirection }) .onActionUpdate((event: GestureEvent) => { if (event.offsetY < 0) { this.offsetY = 0; } else { this.offsetY = event.offsetY; } }) .onActionEnd(() => { if (this.offsetY > 200) { this.dataSources.reloadData(this.playerData.reverse()); } this.offsetY = 0; }) ); } } ``` ### 在滑动视频组件上添加自定义组件 短视频滑动场景往往包含很多随视频一起滑动的组件,如点赞评论转发,视频内容描述等,由于不同的应用这些自定义组件的内容各不相同,SwipePlayer提供自定义能力, 可以通过传递customBuilder组件的形式定义这些随视频滑动的自定义组件。 ```extendtypescript @Entry @Component struct Index { sourceUrls: Array = [ { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/headphones/freebuds-lipstick-2/videos/huawei-freebuds-lipstick-2-creative-design-id.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/wearables/watch-ultimate-new/video/huawei-watch-ultimate-pv.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/visions/v5-max-110/video/huawei-vision-v-5-max-pv.mp4' } ]; playerData:VideoData[] = []; // 1.定义控制器 swipePlayerController: SwipePlayerController = new SwipePlayerController(); dataSources: SwipePlayerIDataSource = new SwipePlayerIDataSource(); // 2.创建播放源 aboutToAppear(): void { // 同上在此插入播放源 ... } build() { Column() { // 3.创建入口组件 SwipePlayer({ datasource: this.dataSources, swipePlayerController: this.swipePlayerController, options: { viewBuilder:(type: string, isFullScreen?: boolean) => { if (type ==== 'video' && !isFullScreen) { return ViewScreen; } } } }) } .width('100%') .height('100%'); } } @Builder export function viewScreen(dataObj: ESObject, playerSession: PlayerSession | undefined, swipePlayerController: SwipePlayerController): void { VerticalScreenComponent({ verticalScreenData: dataObj as VideoData, playerSession: playerSession, swipePlayerController: swipePlayerController }); } // 竖屏页面样式 @Component export struct VerticalScreenComponent { build() { RelativeContainer() { ... } .width('100%') .height('100%') } } ``` ### 自定义滑动切换组件(广告,图片轮播等) 短视频切换场景下除了视频播放场景外还可能存在广告页,图片轮播等场景和其他一些不可预期的页面,SwipePlayer提供自定义滑动切换组件的能力, 通过传递customBuilder组件的形式实现。 ```extendtypescript @Entry @Component struct Index { sourceUrls: Array = [ { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/headphones/freebuds-lipstick-2/videos/huawei-freebuds-lipstick-2-creative-design-id.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/wearables/watch-ultimate-new/video/huawei-watch-ultimate-pv.mp4' }, { url: 'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/visions/v5-max-110/video/huawei-vision-v-5-max-pv.mp4' } ]; playerData:VideoData[] = []; // 1.定义控制器 swipePlayerController: SwipePlayerController = new SwipePlayerController(); dataSources: SwipePlayerIDataSource = new SwipePlayerIDataSource(); // 2.创建播放源 aboutToAppear(): void { ... // 同上在此插入播放源 } build() { Column() { // 3.创建入口组件 SwipePlayer({ datasource: this.dataSources, swipePlayerController: this.swipePlayerController, options: { viewBuilder: (type: string, isFullScreen?: boolean) => { if (type === 'video' && isFullScreen) { return fullScreen; } else if (type === 'video' && !isFullScreen) { return viewScreen; } else { return AdvertBuilder; } } } }) } .width('100%') .height('100%') } } // 竖屏页面 @Builder export function ViewScreen(dataObj: ESObject, playerSession: PlayerSession | undefined, swipePlayerController: SwipePlayerController): void { // 同上 ... } // 横屏页面 @Builder export function FullScreen(dataObj: ESObject, playerSession: PlayerSession | undefined, swipePlayerController: SwipePlayerController): void { // 同上 ... } // 广告页面 @Builder export function Advert(dataObj: ESObject, playerSession: PlayerSession | undefined, swipePlayerController: SwipePlayerController): void { Advert() } ``` ### 长按视频3s弹出倍速播放配置,选择倍速后弹出toast弹框提示并倍速播放视频 短视频滑动还涉及一些手势操作的业务场景,比如长按3s弹出倍速页面,可以通过组件提供的Gesture手势事件注册能力实现, 使用SwipePlayerController提供的弹框能力可以弹出自定义配置页面,应用还可以通过AVPlayer创建回调注册AVPlayer回调监听speedDone事件并在回调中弹出toast提示。 ```extendtypescript // 竖屏页面样式 @Component export struct VerticalScreenComponent { build() { RelativeContainer() { ... } .width('100%') .height('100%') // 页面长按弹出倍速弹窗 .gesture( LongPressGesture() .onAction(() => { this.swipePlayerController.openDialog({ // FastSpeedBuilder倍速弹窗的样式 dialogBuilder: () => wrapBuilder(FastSpeedBuilder), dataObj: this.swipeData }, PushDirection.DOWN_TO_UP, 350, false); }) ); } } // 倍速弹窗 @Component export struct FastSpeed { private swipeData: SwipeData = null!; build() { RelativeContainer() { ... Flex() { ForEach(CommonConstant.VIDEO_FAST_SPEED, (item: PlayerSpeed, commonIndex: number) => { // 倍速的文字,例如1x,2x Text(item + 'x') .onClick(() => { // 设置视频倍速 this.swipeData.swipePlayerController.setSpeed(item.playbackSpeed); promptAction.openCustomDialog({ // toast提示窗样式 builder: () => this.clickSpeedBuilder(), ... }); }); }); } .width('100%'); ... } } } } @Builder export function FastSpeedBuilder(swipeData: SwipeData, playerLayoutSize: PlayerLayoutSize, removeDialog: () => void, closeDialog: () => void) { FastSpeed({ swipeData: swipeData, closeDialog: closeDialog }); } ``` ### 点击评论按钮弹出评论页面,包含视频(缩放)上移动画,跟手动画 SwipePlayerController会封装点击页面弹框及视频动画并对外提供调用方法供应用使用,使用该方法时会弹出空白弹框,用户可以自定义弹框内容。 该弹框弹出式时会有默认视频动画(视频上移或缩放上移),收起弹框时默认有视频还原动画。 ```extendtypescript // 竖屏页面样式 @Component export struct VerticalScreenComponent { @State swipePlayerController: SwipePlayerController = null!; build() { RelativeContainer() { ... // 评论图标、评论数,点击拉起评论弹窗 Column() { Image(CommonConstant.CUSTOM_IMAGE[1]) .width('100%') .height('100%'); Text(this.verticalScreenData.commentsNum.toString()) } .onClick(() => { let withAnimation = false; // 获取屏幕宽度,判断直板机和折叠屏的状态 if (this.displayWidth < CommonConstant.PHONE_WIDTH) { withAnimation = true; } // 直板机和折叠屏折叠态,弹窗会压缩视频,否则不会压缩视频 this.swipePlayerController.openDialog({ // CommentsBuilder为评论区样式 dialogBuilder: () => wrapBuilder(CommentsBuilder), dataObj: this.dialogComponentData }, PushDirection.DOWN_TO_UP, 350, withAnimation) }); ... } } .width('100%') .height('100%') } } @Builder export function CommentsBuilder(dialogComponentData: CommentsData, playerLayoutSize: PlayerLayoutSize, removeDialog: () => void, closeDialog: () => void) { Comments({ playerLayoutSize: playerLayoutSize, closeDialog: closeDialog, removeDialog: removeDialog, dialogComponentData: dialogComponentData }); } @Component export struct Comments { @State playerLayoutSize: PlayerLayoutSize = null!; @Prop dialogComponentData: CommentsData = null!; private closeDialog: () => void = null!; private removeDialog: () => void = null!; build() { Column(){ ... } .width('100%') .height('70%'); } } ``` ### 横向视频全屏播放 播放横向适配时,会额外显示全屏播放按钮,点击后全屏播放,并显示返回键,标题,进度条等内容,使用SwipePlayerController提供的全屏和退出全屏能力结合自定义组件videoBuilder实现。 ```extendtypescript // 页面样式 @Component export struct VerticalScreenComponent { swipePlayerController: SwipePlayerController = null!; build() { RelativeContainer() { ... // 全屏按钮 Text() { Span(CommonConstant.REQUEST_FULL_SCREEN) .fontWeight(CommonConstant.CUSTOM_FONT_WEIGHT) .fontColor(Color.White); Span(' ') .letterSpacing(5); ImageSpan(CommonConstant.CUSTOM_IMAGE[5]) .width(CommonConstant.FULL_SCREEN_TEXT_WIDTH); } .onClick(() => { // 切换为横屏 this.swipePlayerController.requestFullScreen(false); }); ... } } .width('100%') .height('100%') } } ``` ### 实现自定义进度条 某些情况下应用不想使用默认进度条组件,需要实现自定义的进度条,可以通过options配置禁用显示默认进度条,然后通过videoBuilder结合AVPlayer监听事件注册实现自定义进度条。 ```extendtypescript @Entry @Component struct Index { ... build{ SwipePlayer({ datasource: this.dataSources, swipePlayerController: this.swipePlayerController, options: { viewBuilder:(type: string, isFullScreen?: boolean) => { if (type ==== 'video' && !isFullScreen) { return ViewScreen; } } } }) } } // 页面样式 @Component export struct VerticalScreenComponent { @State isVerticalScreen: boolean = true; @State isSliderMoving: boolean = false; @State swipePlayerController: SwipePlayerController = null!; playerSession: PlayerSession = null!; build() { RelativeContainer() { ... // 自定义进度条 ProgressBar({ isShowTime: !this.isVerticalScreen, isSliderMoving: this.isSliderMoving, playerSession: this.playerSession, trackColor: Color.Gray, selectedColor: CommonConstant.SPEED_BUTTON_FONT_COLOR }) ... } .width('100%') .height('100%') } } @Builder export function viewScreen(dataObj: ESObject, playerSession: PlayerSession | undefined, swipePlayerController: SwipePlayerController): void { VerticalScreenComponent({ verticalScreenData: dataObj as VideoData, playerSession: playerSession, swipePlayerController: swipePlayerController }); } ``` ## SwipePlayer接口和属性列表 ### SwipePlayer: 短视频流畅点播切换入口组件 | 参数 | 说明 | |------------------------------------|--------------------------------------------------| | dataSource: SwipePlayerIDataSource | 用于LazyForEach遍历的SwipePlayer的播放源数据对象 | | controller: SwipePlayerController | SwipePlayer的控制器,提供全屏/退出全屏,弹框/关闭弹框及AVPlayer相关控制能力 | | options: SwipePlayerOption | SwipePlayer的参数配置,提供缓存数 | ### SwipePlayerIDataSource: LazyForEach遍历的数据源对象 | 参数 | 说明 | |----------------------------------|---------------------| | dataSources: Array\ | SwipePlayer的播放源数据列表 | | 接口原型 | 参数 | 返回值 | 接口描述 | |------------|---------------------------------------|------------|------------------| | totalCount | | number | SwipePlayer播放源总数 | | getData | index: number | PlayerData | 获取数据源对象 | | pushData | data: PlayerData; Array\ | void | 动态添加数据源 | | reloadData | data: Array\ | void | 重新刷新数据源 | ### PlayerData:视频播放类型或自定义类型数据源接口(需开发者实现接口) | 参数 | 说明 | |----------------------------------------|----------------------------------------------------------------------| | getId:()=>string | 获取数据源Id | | getType:()=>string | 获取短视频播放源类型 | | getSource:()=>string;undefined | 获取短视频播放源路径,支持http/https | | getCover:()=>string;Resource;undefined | 获取视频封面,支持gif(在AVPlayer的initialized,prepared,playing期间显示,不设置默认显示黑屏画面) | ### 配置参数: SwipePlayerOptions | 参数 | 说明 | |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------| | cachedCount | LazyForEach缓存数量,范围0-3,默认1 | | videoPlayer: VideoPlayer | 视频配置项 | | viewBuilder: (type: string, isFullScreen?: boolean) => (dataObj: ESObject, playerSession: PlayerSession \| undefined, swipePlayerController: SwipePlayerController) => void | 自定义组件Builder函数 | | swiperCallBack?: SwiperCallback | SwipePlayer包装的Swiper组件的一些滑动回调注册 | ### VideoPlayer: 视频配置项 | 参数 | 说明 | |-------------------------------|----------------| | playIcon?: string \| Resource | 视频暂停状态下显示的播放按钮 | ### SwiperCallback: Swiper回调注册 | 参数 | 说明 | |--------------------------------------------------------------------------------------------------|-------------------------------| | onAnimationStart?: (index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => void | 对应Swiper组件的onAnimationStart回调 | | onAnimationEnd?: (index: number, extraInfo: SwiperAnimationEvent) => void | 对应Swiper组件的onAnimationEnd回调 | | onChange?: (index: number) => void | 对应Swiper组件的onChange回调 | ### SwipePlayerController: SwipePlayer组件的控制器 | 接口原型 | 参数 | 返回值 | 接口描述 | |-------------------|---------------------------------------------------------------------------------------------------------------------------|-------------------|---------------------------------------------------------| | getAVPlayer | | media.AVPlayer | 获取当前播放的AVPlayer对象,存在获取不到的情况 | | async play | | void | 播放当前视频,有状态校验 | | async pause | | void | 暂停播放当前视频,有状态校验 | | seek | timeMs: number | void | 跳转到指定位置播放,有状态校验 | | release | | void | 释放当前的AVPlayer对象,有状态校验 | | setSpeed | speed: media.PlaybackSpeed | void | 设置视频的播放速度,有状态校验 | | requestFullScreen | | void | 请求全屏播放,会隐藏videoBuilder组件 | | exitFullScreen | | void | 退出全屏播放,会显示videoBuilder组件 | | openDialog | builder: DialogBuilder, animationCallback: OpenAnimationCallback | void | 打开自定义弹框页面,支持传入自定义动画 | | openDialog | builder: DialogBuilder, direction:PushDirection, duration:number, withVideoAnimator:boolean, closeMaskCallback?: ()=>void | void | 打开自定义弹窗,支持传入动画方向,动画时长,是否压缩视频,以及点击遮罩层关闭探查时回调 | | closeDialog | closeAnimationCallback: CloseAnimationCallback | void | 关闭自定义页面弹框(可支持自定义动画) | | backPressListener | | BackPressListener | 提供在onBackPress注册函数,侧滑返回时能关闭弹窗 | ### DialogBuilder: 弹窗wrappedBuilder | 参数 | 返回值 | 接口描述 | |--------------------------------------------------------------------------------------------|------|-------------| | dialogBuilder: () => WrappedBuilder<\[ESObject, PlayerLayoutSize, () => void, () => void]> | void | 弹窗Builder | | dataObj: ESObject | | 弹窗Builder参数 | ### PlayerLayoutSize: 视频布局属性 | 接口原型 | 参数 | 返回值 | 接口描述 | |------------------------|-------------------------------------|------------------|----------------| | updatePlayerWidth | playerWidth: number \| string | void | 修改视频宽度 | | getPlayerWidth | | number \| string | 获取视频宽度 | | updatePlayerHeight | playerHeight: number \| string | void | 修改视频高度 | | getPlayerHeight | | number \| string | 获取视频高度 | | updatePlayerPositionX | playerPositionX: number \| string | void | 修改视频绝对定位X轴偏移量 | | getPlayerPositionX | | number \| string | 获取视频绝对定位X轴偏移量 | | updatePlayerPositionY | playerPositionY: number \| string | void | 修改视频绝对定位Y轴偏移量 | | getPlayerPositionX | | number \| string | 获取视频绝对定位Y轴偏移量 | | updateXComponentWidth | xComponentWidth: number \| string | void | 修改XComponent宽度 | | getXComponentWidth | | number \| string | 获取XComponent宽度 | | updateXComponentHeight | xComponentHeight: number \| string | void | 修改XComponent高度 | | getXComponentHeight | | number \| string | 获取XComponent高度 | ### AVPlayerListener: 储存AVPlayer对象及其对应的事件 | 接口原型 | 参数 | 返回值 | 接口描述 | |---------|----------------------------------------------------|------|------| | onEvent | key: string, callback: (...args: ESObject) => void | void | 注册回调 | ## 开源协议 本项目基于 Apache License 2.0,请自由地享受和参与开源。