HarmonyOS 鸿蒙Next 求一个类似抖音首页视频播放效果的demo

发布于 1周前 作者 zlyuanteng 来自 鸿蒙OS

HarmonyOS 鸿蒙Next 求一个类似抖音首页视频播放效果的demo

求一个类似抖音首页 视频播放 效果: 上下翻页切换视频,最好可预加载,并做好内存释放。
 

2 回复
demo如下(在module.json5文件配置ohos.permission.INTERNET权限):
//index.ets
import { media } from '@kit.MediaKit';
import { VideoPlayer } from '../component/VideoPlayer';
import { MyDataSource, MyDataSource1 } from '../model/MyDataSource';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = ‘[VideoPlayer]’;

@Entry @Component struct Index { @State videoFiles: media.AVFileDescriptor[] = []; @State sources: string[] = [ http://pic.jxxw.com.cn/v4/group6/M00/46/78/rBAVFGX7n8-AV-eqALs6PYM30QU262.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8OAuAeQ02Ar2vUF4paaM297.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8N6mAE4RTAfzVGt4a3q8950.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/E9/rBAVFGX8NyiAH-0GAzhCZIp9ov8315.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/DA/rBAVFGX8BOGAdpbeAZzE0OrluYE328.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/AF/rBAVFGX747yANuUQANxGPVX_AQs667.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/A8/rBAVFGX73eiANYqRAw2t0F21veQ341.mp4, http://pic.jxxw.com.cn/v4/group6/M00/46/78/rBAVFGX7n-2AaEoPAHxtdYWT1uE952.mp4, ]; @State curIndex: number = 0; @State firstFlag: boolean = true; private swiperController: SwiperController = new SwiperController();

aboutToAppear(): void { this.initFiles(); }

build() { Swiper(this.swiperController) { // LazyForEach(new MyDataSource(this.sources), (item: string, index: number) => { LazyForEach(new MyDataSource1(this.videoFiles), (item: string, index: number) => { VideoPlayer({ curSource: item, curIndex: this.curIndex, index: index, firstFlag: this.firstFlag }) }, (item: string, index: number) => JSON.stringify(item) + index) } .width(‘100%’) .height(‘100%’) .index($$this.curIndex) .vertical(true) .loop(false) .indicator(false) .backgroundColor(Color.Black) .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => { console.info(TAG, onGestureSwipe index: ${index},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.) }) .onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => { console.info(TAG, onAnimationStart index: ${index},targetIndex: ${targetIndex},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.) }) .onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => { console.info(TAG, onAnimationEnd index: ${index},extraInfo: ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(extraInfo)}.) }) }

initFiles() { let fileList: string[] = getContext(this).resourceManager.getRawFileListSync(‘video’); fileList.forEach((fileStr: string) => { // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址 // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度 let fileDescriptor = getContext().resourceManager.getRawFdSync(video/${fileStr}); let avFileDescriptor: media.AVFileDescriptor = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length }; this.videoFiles.push(avFileDescriptor) }) } } //VideoPlayer.ets import { media } from ‘@kit.MediaKit’; import { BusinessError } from ‘@kit.BasicServicesKit’;

const TAG = ‘[VideoPlayer]’;

enum StateEnum { idle = ‘idle’, initialized = ‘initialized’, prepared = ‘prepared’, playing = ‘playing’, paused = ‘paused’, completed = ‘completed’, stopped = ‘stopped’, released = ‘released’, error = ‘error’, }

@Preview @Component export struct VideoPlayer { @Prop @Watch(‘onIndexChange’) curIndex: number = -1; private index: number = 0; private curSource: media.AVFileDescriptor | string = ‘’; @State isPlaying: boolean = true; @State isOpacity: boolean = false; @State flag: boolean = false; @State currentTime: number = 0; @State durationTime: number = 0; @State durationStringTime: string = ‘00:00’; @State currentStringTime: string = ‘00:00’; private duration: number = 0; private surfaceID: string = ‘’; private xComponentController = new XComponentController(); private avPlayer: media.AVPlayer | undefined = undefined; @Link firstFlag: boolean;

onIndexChange() { console.info(TAG, enter onIndexChange. <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex} <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index}) if (this.curIndex !== this.index) { this.pause(); this.isOpacity = false; this.isPlaying = false; } else { if (this.flag === true) { this.play(); this.isPlaying = true; this.isOpacity = true; } else { // The scheduled task determines whether the video loading is complete. let intervalFlag = setInterval(() => { if (this.flag === true) { this.play(); this.isPlaying = true; this.isOpacity = true; clearInterval(intervalFlag); } }, 100); } } }

build() { Column() { Stack({ alignContent: Alignment.Center }) { Stack() { if (!this.isPlaying) { Image($r(‘app.media.ic_public_play’)) .width(50) .height(50) .zIndex(2) .onClick(() => { this.iconOnclick(); }); }

Column() { XComponent({ id: ‘’, type: XComponentType.SURFACE, libraryname: ‘’, controller: this.xComponentController }) .onLoad(() => { this.xComponentController.setXComponentSurfaceSize({ surfaceWidth: 1920, surfaceHeight: 1080 }); let surfaceID = this.xComponentController.getXComponentSurfaceId(); this.surfaceID = surfaceID; this.initAVPlayer(); }) .width(‘100%’) .height(‘100%’); } .zIndex(1) .onClick(() => { this.iconOnclick(); }) }

this.PlayControl() } .height(‘100%’) .backgroundColor(Color.Black) .width(‘100%’) } .width(‘100%’) .height(‘100%’) }

@Builder PlayControl() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { Image(this.isPlaying ? $r(‘app.media.ic_pause’) : $r(‘app.media.ic_play’)) .width(‘20vp’) .height(‘20vp’) .onClick(() => { this.iconOnclick(); }); Text(this.currentStringTime) .fontSize(‘14vp’) .fontColor(Color.White) .margin({ left: ‘2vp’ }) Slider({ value: this.currentTime, step: 1, min: 0, max: this.durationTime, style: SliderStyle.OutSet }) .blockColor(Color.White) .width(‘60%’) .trackColor(Color.Gray) .selectedColor(Color.White) .showSteps(false) .showTips(false) .trackThickness(this.isOpacity ? 2 : 4) .onChange((value: number, mode: SliderChangeMode) => { this.sliderOnchange(value, mode); }); Text(this.durationStringTime) .fontSize(‘14vp’) .fontColor(Color.White) .margin({ left: ‘2vp’}) } .zIndex(2) .padding({ right: ‘2vp’ }) .opacity(this.isOpacity ? 0.5 : 1) .offset({ x: 0, y: 350 }) .width(‘100%’) .backgroundBlurStyle(BlurStyle.Thin, { colorMode: ThemeColorMode.DARK }) }

iconOnclick() { if (this.isPlaying) { this.pause(); this.isOpacity = false; this.isPlaying = false; return; } if (this.flag === true) { this.play(); this.isPlaying = true; this.isOpacity = true; } else { // The scheduled task determines whether the video loading is complete. let intervalFlag = setInterval(() => { if (this.flag === true) { this.play(); this.isPlaying = true; this.isOpacity = true; clearInterval(intervalFlag); } }, 100); } }

sliderOnchange(value: number, mode: SliderChangeMode) { console.info(TAG, sliderOnchange. value is ${value}); this.currentTime = value if (mode === SliderChangeMode.Begin || mode === SliderChangeMode.Moving) { this.isOpacity = false; } if (mode === SliderChangeMode.End) { let seekTime: number = value * this.duration / this.durationTime; this.currentStringTime = this.secondToTime(Math.floor(seekTime / 1000)); console.info(TAG, sliderOnchange. time is ${seekTime}, currentTime is ${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.currentTime}); this.setSeek(seekTime); this.isOpacity = true; } }

secondToTime(seconds: number): string { let hourUnit = 60 * 60; let hour: number = Math.floor(seconds / hourUnit); let minute: number = Math.floor((seconds - hour * hourUnit) / 60); let second: number = seconds - hour * hourUnit - minute * 60; let hourStr: string = hour < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${hour.toString()} : ${hour.toString()} let minuteStr: string = minute < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${minute.toString()} : ${minute.toString()} let secondStr: string = second < 10 ? <span class="hljs-number"><span class="hljs-number">0</span></span>${second.toString()} : ${second.toString()} if (hour > 0) { return ${hourStr}:${minuteStr}:${secondStr}; } if (minute > 0) { return ${minuteStr}:${secondStr}; } else { return <span class="hljs-number"><span class="hljs-number">00</span></span>:${secondStr}; } }

initAVPlayer() { media.createAVPlayer().then((video: media.AVPlayer) => { if (video != null) { this.avPlayer = video; this.setAVPlayerCallback(video); // 设置播放源,使其进入initialized状态 if (typeof this.curSource === ‘string’) { video.url = this.curSource; } else { video.fdSrc = this.curSource; } console.info(TAG, ‘createAVPlayer success’); } else { console.error(TAG, ‘createAVPlayer fail’); } }).catch((error: BusinessError) => { console.error(TAG, AVPlayer catchCallback, error message:${error.message}); }); }

// 注册avplayer回调函数 setAVPlayerCallback(avPlayer: media.AVPlayer) { avPlayer.on(‘timeUpdate’, (time: number) => { // console.info(TAG, AVPlayer timeUpdate. time is ${time}); this.currentTime = Math.floor(time * this.durationTime / this.duration); this.currentStringTime = this.secondToTime(Math.floor(time / 1000)); })

// seek操作结果回调函数 avPlayer.on(‘seekDone’, (seekDoneTime: number) => { console.info(TAG, AVPlayer seekDone succeeded, seek time is ${seekDoneTime}); })

// 监听setSpeed生效的事件 avPlayer.on(‘speedDone’, (speed: number) => { console.info(TAG, AVPlayer speedDone succeeded, speed is ${speed}); })

// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程 avPlayer.on(‘error’, (err: BusinessError) => { console.error(TAG, Invoke avPlayer failed, code is ${err.code}, message is ${err.message}); avPlayer.reset(); // 调用reset重置资源,触发idle状态 })

// 状态机变化回调函数 avPlayer.on(‘stateChange’, async (state: string, reason: media.StateChangeReason) => { switch (state) { case ‘idle’: // 成功调用reset接口后触发该状态机上报 console.info(TAG, ‘AVPlayer state idle called.’); // 设置播放源,使其进入initialized状态 if (typeof this.curSource === ‘string’) { avPlayer.url = this.curSource; } else { avPlayer.fdSrc = this.curSource; } break; case ‘initialized’: // avplayer 设置播放源后触发该状态上报 console.info(TAG, ‘AVPlayer state initialized called.’); avPlayer.surfaceId = this.surfaceID; avPlayer.prepare(); break; case ‘prepared’: // prepare调用成功后上报该状态机 console.info(TAG, ‘AVPlayer state prepared called.’); this.flag = true; this.duration = avPlayer.duration; this.durationTime = Math.floor(this.duration / 1000); this.durationStringTime = this.secondToTime(this.durationTime); if (this.index == 0 && this.firstFlag) { avPlayer.play(); } this.firstFlag = false; // avPlayer.play(); break; case ‘completed’: // prepare调用成功后上报该状态机 console.info(TAG, ‘AVPlayer state completed called.’); this.isPlaying = false; break; case ‘playing’: // play成功调用后触发该状态机上报 this.isPlaying = true; console.info(TAG, ‘AVPlayer state playing called.’); break; case ‘paused’: // pause成功调用后触发该状态机上报 console.info(TAG, ‘AVPlayer state paused called.’); break; case ‘stopped’: // stop接口成功调用后触发该状态机上报 console.info(TAG, ‘AVPlayer state stopped called.’); break; case ‘released’: console.info(TAG, ‘AVPlayer state released called.’); break; case ‘error’: console.info(TAG, ‘AVPlayer state error called’); break; default: console.info(TAG, ‘AVPlayer state unknown called.’); break; } }) }

setSpeed(speed: number) { try { if (!this.avPlayer) { console.error(TAG, ‘setSpeed failed. avPlayer is undefined.’) return; } let state = this.avPlayer.state; if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) { console.error(TAG, ‘setSpeed failed. state is not prepared/paused/completed.’) return; } this.avPlayer.setSpeed(speed); } catch (err) { console.error(TAG, setSpeed failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}) } }

setSeek(seek: number) { try { if (!this.avPlayer) { console.error(TAG, ‘setSpeed failed. avPlayer is undefined.’) return; } let state = this.avPlayer.state; if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) { console.error(TAG, ‘setSpeed failed. state is not prepared/playing/paused/completed.’) return; } this.avPlayer.seek(seek, media.SeekMode.SEEK_PREV_SYNC); } catch (err) { console.error(TAG, setSpeed failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}) } }

async play() { try { if (!this.avPlayer) { console.error(TAG, ‘play failed. avPlayer is undefined.’) return; } let state = this.avPlayer.state; if (state != StateEnum.prepared && state != StateEnum.paused && state != StateEnum.completed) { console.error(TAG, ‘play failed. state is not prepared/paused/completed.’) return; } await this.avPlayer.play(); console.info(TAG, AVPlayer play successful;); } catch (err) { console.error(TAG, AVPlayer play failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}) } }

async pause() { try { if (!this.avPlayer) { console.error(TAG, ‘pause failed. avPlayer is undefined.’) return; } await this.avPlayer.pause(); console.info(TAG, AVPlayer pause successful;); } catch (err) { console.error(TAG, AVPlayer pause failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}) } }

async stop() { try { if (!this.avPlayer) { console.error(TAG, ‘stop failed. avPlayer is undefined.’) return; } let state = this.avPlayer.state; if (state != StateEnum.prepared && state != StateEnum.playing && state != StateEnum.paused && state != StateEnum.completed) { console.error(TAG, ‘setSpeed failed. state is not prepared/playing/paused/completed.’) return; } await this.avPlayer.stop(); console.info(TAG, AVPlayer stop successful;); } catch (err) { console.error(TAG, AVPlayer stop failed. err is ${<span class="hljs-built_in"><span class="hljs-built_in">JSON</span></span>.stringify(err)}) } }

aboutToDisappear(): void { console.info(TAG, enter aboutToDisappear. <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.curIndex} <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index:${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.index}) if (this.avPlayer) { this.avPlayer.off(‘timeUpdate’); this.avPlayer.off(‘seekDone’); this.avPlayer.off(‘speedDone’); this.avPlayer.off(‘error’); this.avPlayer.off(‘stateChange’); this.avPlayer.release(); } } } //MyDataSource.ets // LazyForEach model. import { media } from ‘@kit.MediaKit’;

class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = [];

public totalCount(): number { return 0; }

public getData(index: number): media.AVFileDescriptor | string | undefined { return undefined; }

registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } }

unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } }

notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }) }

notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }) }

notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index); }) } }

export class MyDataSource1 extends BasicDataSource { public dataArray: media.AVFileDescriptor[] = [];

constructor(ele: Object[]) { super(); for (let index = 0; index < ele.length; index++) { this.dataArray.push(ele[index] as media.AVFileDescriptor); } }

public totalCount(): number { return this.dataArray.length; }

public getData(index: number): media.AVFileDescriptor { return this.dataArray[index]; }

public addData(index: number, data: media.AVFileDescriptor): void { this.dataArray.splice(index, 0); this.notifyDataAdd(index); } }

export class MyDataSource extends BasicDataSource { public dataArray: string[] = [];

constructor(ele: Object[]) { super(); for (let index = 0; index < ele.length; index++) { this.dataArray.push(ele[index] as string); } }

public totalCount(): number { return this.dataArray.length; }

public getData(index: number): string { return this.dataArray[index]; }

public addData(index: number, data: media.AVFileDescriptor): void { this.dataArray.splice(index, 0); this.notifyDataAdd(index); } }

关于HarmonyOS 鸿蒙Next类似抖音首页视频播放效果的demo,以下是一个简要示例说明:

  1. 界面布局

    • 使用Tabbar布局,中间设置发布按钮,其余按钮使用文字。
    • 自定义导航栏,覆盖在视频之上,包含两边的图片按钮和中间可滑动的菜单列表。
  2. 视频播放

    • 使用鸿蒙系统提供的Video组件播放视频。
    • 设置视频的源地址、控制器等属性,并配置自动播放、隐藏控制器、适应模式等选项。
  3. 视频翻页

    • 使用Swiper组件实现视频翻页效果。
    • 配置动画曲线、循环模式等属性,确保翻页流畅自然。
  4. 其他功能

    • 考虑实现画中画播放、后台播放等功能,提升用户体验。
    • 优化折叠屏设备的适配,如全屏播放时将手机半折叠放在桌上,上半屏继续看剧,下半屏进行其他操作。

由于篇幅限制,无法提供完整的代码实现。但你可以基于上述说明,结合鸿蒙系统的开发文档和API进行具体实现。

HarmonyOS鸿蒙Next高级实战已发布,可以先学学https://www.itying.com/goods-1204.html

回到顶部