diff --git a/VideoPlayerSample/MediaService/src/main/ets/controller/AvPlayerController.ets b/VideoPlayerSample/MediaService/src/main/ets/controller/AvPlayerController.ets index a8fd2b555b842e689bb8f6b0e5ae3bfef06f7fe6..3e0727e2324da5cbf3a99fb75716c7fe018f6c0b 100644 --- a/VideoPlayerSample/MediaService/src/main/ets/controller/AvPlayerController.ets +++ b/VideoPlayerSample/MediaService/src/main/ets/controller/AvPlayerController.ets @@ -18,16 +18,13 @@ import { media } from '@kit.MediaKit'; import { audio } from '@kit.AudioKit'; import { avSession } from '@kit.AVSessionKit'; import { BusinessError } from '@kit.BasicServicesKit'; -import { BackgroundTaskManager } from '../utils/BackgroundTaskManager'; import { AVPlayerState, CommonConstants } from '../common/CommonConstants'; import { secondToTime } from '../utils/CommUtils' import { VideoData } from '../model/VideoData'; import { AvSessionController } from './AvSessionController'; import Logger from '../utils/Logger'; -import { hilog } from '@kit.PerformanceAnalysisKit'; const TAG = '[AvPlayerController]'; -const uiContext: UIContext | undefined = AppStorage.get('uiContext'); @Observed export class AvPlayerController { @@ -44,12 +41,10 @@ export class AvPlayerController { private curSource: VideoData; private context: common.UIAbilityContext | undefined = AppStorage.get('context'); private avSessionController: AvSessionController; - private uiContext: UIContext - constructor(curSource: VideoData, context: UIContext) { + constructor(curSource: VideoData) { this.curSource = curSource; this.avSessionController = AvSessionController.getInstance(); - this.uiContext = context; } public initAVPlayer() { @@ -85,6 +80,7 @@ export class AvPlayerController { return; } + const uiContext: UIContext | undefined = AppStorage.get('uiContext'); avPlayer.on('timeUpdate', (time: number) => { if (time > this.currentTime * 1000) { uiContext?.animateTo({ duration: 1000, curve: Curve.Linear }, () => { @@ -116,12 +112,17 @@ export class AvPlayerController { if (!this.avSessionController) { return; } - this.avSessionController.getAvSession()?.on('play', () => this.sessionPlayCallback()); // Set the play command listening event. - this.avSessionController.getAvSession()?.on('pause', () => this.sessionPauseCallback()); // Set the pause command to listen for events. - this.avSessionController.getAvSession()?.on('stop', () => this.sessionStopCallback()); // Set the stop command to listen for events. - this.avSessionController.getAvSession()?.on('fastForward', (time?: number) => this.sessionFastForwardCallback(time)); // Set fast forward command listening events - this.avSessionController.getAvSession()?.on('rewind', (time?: number) => this.sessionRewindCallback(time)); // Set the fast back command to listen for events. - this.avSessionController.getAvSession()?.on('seek', (seekTime: number) => this.sessionSeekCallback(seekTime)); // Set the jump node to listen to events. + try { + this.avSessionController.getAvSession()?.on('play', () => this.sessionPlayCallback()); + this.avSessionController.getAvSession()?.on('pause', () => this.sessionPauseCallback()); + this.avSessionController.getAvSession()?.on('stop', () => this.sessionStopCallback()); + this.avSessionController.getAvSession()?.on('fastForward', + (time?: number) => this.sessionFastForwardCallback(time)); + this.avSessionController.getAvSession()?.on('rewind', (time?: number) => this.sessionRewindCallback(time)); + this.avSessionController.getAvSession()?.on('seek', (seekTime: number) => this.sessionSeekCallback(seekTime)); + } catch (err) { + Logger.error(TAG, `setAvSessionListener failed, code is ${err.code}, message is ${err.message}`); + } } // [End listener1] @@ -239,14 +240,14 @@ export class AvPlayerController { // [Start player3] this.avPlayer.on('audioOutputDeviceChangeWithInfo', (data: audio.AudioStreamDeviceChangeInfo) => { if (data.changeReason === audio.AudioStreamDeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) { - hilog.info(0x0001, TAG, `Device connect: ${data.changeReason}`); + Logger.info(TAG, `Device connect: ${data.changeReason}`); } }); // [End player3] // [Start pause_video] this.avPlayer.on('audioOutputDeviceChangeWithInfo', (data: audio.AudioStreamDeviceChangeInfo) => { if (data.changeReason === audio.AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { - hilog.info(0x0001, TAG, `Device connect: ${data.changeReason}`); + Logger.info(TAG, `Device connect: ${data.changeReason}`); this.pauseVideo(); } }); @@ -429,17 +430,18 @@ export class AvPlayerController { public releaseVideo(index: number) { if (this.avPlayer) { - Logger.info(TAG, - `releaseVideo: state:${this.avPlayer.state} this.curIndex:${this.curIndex} this.index:${index}`); - this.avPlayer.off('timeUpdate'); - this.avPlayer.off('seekDone'); - this.avPlayer.off('speedDone'); - this.avPlayer.off('error'); - this.avPlayer.off('stateChange'); - this.avPlayer.off('audioInterrupt'); - this.avPlayer.off('audioOutputDeviceChangeWithInfo'); - this.avPlayer.release(); - this.avSessionController?.unregisterSessionListener(); + Logger.info(TAG, `releaseVideo: state:${this.avPlayer.state} this.curIndex:${this.curIndex} this.index:${index}`); + try { + this.avPlayer.off('timeUpdate'); + this.avPlayer.off('seekDone'); + this.avPlayer.off('speedDone'); + this.avPlayer.off('error'); + this.avPlayer.off('stateChange'); + this.avPlayer.release(); + this.avSessionController?.unregisterSessionListener(); + } catch (err) { + Logger.error(TAG, `releaseVideo failed, err.code:${err.code}, err.message:${err.message}`); + } } } } \ No newline at end of file diff --git a/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController.ets b/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController.ets index be223b5953009697c0aac2a1879cbe2efd50c2a9..587fc0389df86ed78d4960409d6d8c251f3fb648 100644 --- a/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController.ets +++ b/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController.ets @@ -14,18 +14,19 @@ */ import { common, wantAgent } from '@kit.AbilityKit'; +import { avSession } from '@kit.AVSessionKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { VideoData } from '../model/VideoData'; import Logger from '../utils/Logger'; import { ImageUtil } from '../utils/ImageUtil'; import { BackgroundTaskManager } from '../utils/BackgroundTaskManager'; -import { hilog } from '@kit.PerformanceAnalysisKit'; -import { avSession } from '@kit.AVSessionKit'; + const TAG = 'AvSessionController'; + export class AvSessionController { - private avSession: avSession.AVSession | undefined = undefined; private static instance: AvSessionController | null; private context: common.UIAbilityContext | undefined = undefined; + private avSession: avSession.AVSession | undefined = undefined; private avSessionMetadata: avSession.AVMetadata | undefined = undefined; constructor() { @@ -47,14 +48,18 @@ export class AvSessionController { return; } // [EndExclude init_session] - avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => { - this.avSession = avSession; - hilog.info(0x0001, TAG, `session create successed : sessionId : ${this.avSession.sessionId}`); - // Apply for background long-term tasks - BackgroundTaskManager.startContinuousTask(this.context); - this.setLaunchAbility(); - this.avSession.activate(); - }); + try { + avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => { + this.avSession = avSession; + Logger.info(TAG, `session create successed : sessionId : ${this.avSession.sessionId}`); + // Apply for background long-term tasks + BackgroundTaskManager.startContinuousTask(this.context); + this.setLaunchAbility(); + this.avSession.activate(); + }); + } catch (err) { + Logger.error(TAG, `createAVSession failed, err.code:${err.code}, err.message:${err.message}`); + } } // [End init_session] public getAvSession() { @@ -68,25 +73,29 @@ export class AvSessionController { // [Start meta_data] public async setAVMetadata(curSource: VideoData, duration: number) { // [StartExclude meta_data] - if (curSource === undefined) { - Logger.error(TAG, 'SetAVMetadata Error, curSource is null'); + if (curSource === undefined || this.context ===undefined) { + Logger.error(TAG, 'SetAVMetadata Error, curSource or context is null'); return; } // [EndExclude meta_data] const imagePixMap = await ImageUtil.getPixmapFromMedia(curSource.head); - let metadata: avSession.AVMetadata = { - assetId: `${curSource.index}`, // Media ID - title: this.context?.resourceManager.getStringSync(curSource.name), // title - mediaImage: imagePixMap, // Pixel data or picture path address of a picture. - duration: duration // Media duration, in ms - }; - if (this.avSession) { - this.avSession.setAVMetadata(metadata).then(() => { // Call the set session metadata interface - this.avSessionMetadata = metadata; - Logger.info(TAG, "SetAVMetadata successfully"); - }).catch((err: BusinessError) => { - Logger.error(TAG, `SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`); - }); + try { + let metadata: avSession.AVMetadata = { + assetId: `${curSource.index}`, // Media ID + title: this.context.resourceManager.getStringSync(curSource.name.id), // title + mediaImage: imagePixMap, // Pixel data or picture path address of a picture. + duration: duration // Media duration, in ms + }; + if (this.avSession) { + this.avSession.setAVMetadata(metadata).then(() => { // Call the set session metadata interface + this.avSessionMetadata = metadata; + Logger.info(TAG, "SetAVMetadata successfully"); + }).catch((err: BusinessError) => { + Logger.error(TAG, `SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`); + }); + } + } catch (err) { + Logger.error(TAG, `setAVMetadata failed, code: ${err.code}, message: ${err.message}`); } } // [End meta_data] @@ -113,6 +122,8 @@ export class AvSessionController { if (this.avSession) { this.avSession.setLaunchAbility(agent); } + }).catch((err: BusinessError) => { + Logger.error(TAG, `getWantAgent failed: code: ${err.code}, message: ${err.message}`); }); } // [End launch] @@ -137,13 +148,17 @@ export class AvSessionController { if (!this.avSession) { return; } - this.avSession.off('play'); - this.avSession.off('pause'); - this.avSession.off('playNext'); - this.avSession.off('playPrevious'); - this.avSession.off('setLoopMode'); - this.avSession.off('seek'); - this.avSession.off('toggleFavorite'); + try { + this.avSession.off('play'); + this.avSession.off('pause'); + this.avSession.off('playNext'); + this.avSession.off('playPrevious'); + this.avSession.off('setLoopMode'); + this.avSession.off('seek'); + this.avSession.off('toggleFavorite'); + } catch (err) { + Logger.error(TAG, `unregisterSessionListener failed: code: ${err.code}, message: ${err.message}`); + } // [EndExclude init_session] // Destroy background long-term tasks BackgroundTaskManager.stopContinuousTask(this.context); diff --git a/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController1.ets b/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController1.ets index 98913979a8c3a104d7f033895697c09fa580bf84..172eaabd03baa0e68949a3bd54eea0d394829719 100644 --- a/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController1.ets +++ b/VideoPlayerSample/MediaService/src/main/ets/controller/AvSessionController1.ets @@ -1,8 +1,24 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { hilog } from '@kit.PerformanceAnalysisKit'; import { common } from '@kit.AbilityKit'; // [Start session_controller1] import { avSession } from '@kit.AVSessionKit'; const TAG = 'AvSessionController'; + export class AvSessionController { private avSession: avSession.AVSession | undefined = undefined; private context: common.UIAbilityContext | undefined = undefined; diff --git a/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManager.ets b/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManager.ets index a580a959b8c12ded3f11a58c5e68e7e09254c8dd..73a7b42f6bd1ec5ffc363ada12aadf8d3d06c3fa 100644 --- a/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManager.ets +++ b/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManager.ets @@ -16,7 +16,7 @@ import { common, wantAgent } from '@kit.AbilityKit'; import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; import { BusinessError } from '@kit.BasicServicesKit'; -import { hilog } from '@kit.PerformanceAnalysisKit'; +import Logger from './Logger'; const TAG = '[BackgroundTaskManager]'; @@ -43,10 +43,12 @@ export class BackgroundTaskManager { wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => { backgroundTaskManager.startBackgroundRunning(context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => { - hilog.info(0x0001, TAG, "startBackgroundRunning succeeded"); + Logger.info(TAG, 'startBackgroundRunning succeeded'); }).catch((err: BusinessError) => { - hilog.error(0x0001, TAG, `startBackgroundRunning failed Cause: ${JSON.stringify(err)}`); + Logger.error(TAG, `startBackgroundRunning failed Cause: ${JSON.stringify(err)}`); }); + }).catch((err: BusinessError) => { + Logger.error(TAG, `getWantAgent failed, err.code:${err.code}, err.message:${err.message}`); }); } @@ -56,9 +58,9 @@ export class BackgroundTaskManager { return; } backgroundTaskManager.stopBackgroundRunning(context).then(() => { - hilog.info(0x0001, TAG, "stopBackgroundRunning succeeded"); + Logger.info(TAG, 'stopBackgroundRunning succeeded'); }).catch((err: BusinessError) => { - hilog.error(0x0001, TAG, `stopBackgroundRunning failed Cause: ${JSON.stringify(err)}`); + Logger.error(TAG, `stopBackgroundRunning failed Cause: ${JSON.stringify(err)}`); }); } } diff --git a/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManagerDocs.ets b/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManagerDocs.ets deleted file mode 100644 index fcc6b276ce69550ecc43b5a4e915af76cf5b006c..0000000000000000000000000000000000000000 --- a/VideoPlayerSample/MediaService/src/main/ets/utils/BackgroundTaskManagerDocs.ets +++ /dev/null @@ -1,48 +0,0 @@ -import { common, wantAgent } from '@kit.AbilityKit'; -import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; -import { BusinessError } from '@kit.BasicServicesKit'; -import { hilog } from '@kit.PerformanceAnalysisKit'; - -const TAG = '[BackgroundTaskManager]'; - -/** - * Background task tool class. - */ -export class BackgroundTaskManager { - public static startContinuousTask(context?: common.UIAbilityContext): void { - if (!context) { - return; - } - let wantAgentInfo: wantAgent.WantAgentInfo = { - wants: [ - { - bundleName: context.abilityInfo.bundleName, - abilityName: context.abilityInfo.name - } - ], - operationType: wantAgent.OperationType.START_ABILITY, - requestCode: 0, - wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] - }; - - wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => { - backgroundTaskManager.startBackgroundRunning(context, - backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => { - hilog.info(0x0001, TAG, "startBackgroundRunning succeeded"); - }).catch((err: BusinessError) => { - hilog.error(0x0001, TAG, `startBackgroundRunning failed Cause: ${JSON.stringify(err)}`); - }); - }); - } - - public static stopContinuousTask(context?: common.UIAbilityContext): void { - if (!context) { - return; - } - backgroundTaskManager.stopBackgroundRunning(context).then(() => { - hilog.info(0x0001, TAG, "stopBackgroundRunning succeeded"); - }).catch((err: BusinessError) => { - hilog.error(0x0001, TAG, `stopBackgroundRunning failed Cause: ${JSON.stringify(err)}`); - }); - } -} \ No newline at end of file diff --git a/VideoPlayerSample/MediaService/src/main/ets/utils/ImageUtil.ets b/VideoPlayerSample/MediaService/src/main/ets/utils/ImageUtil.ets index 7dd6f2decfa98882675b8d62cf19ac6e89d3fab8..3ff4b5cb4809555899dc085d46db5167c54a55c1 100644 --- a/VideoPlayerSample/MediaService/src/main/ets/utils/ImageUtil.ets +++ b/VideoPlayerSample/MediaService/src/main/ets/utils/ImageUtil.ets @@ -14,21 +14,22 @@ */ import { image } from '@kit.ImageKit'; - -const uiContext: UIContext | undefined = AppStorage.get('uiContext'); +import Logger from './Logger'; export class ImageUtil { public static async getPixmapFromMedia(resource: Resource) { - let unit8Array = await uiContext?.getHostContext()?.resourceManager?.getMediaContent({ - bundleName: resource.bundleName, - moduleName: resource.moduleName, - id: resource.id - }); - let imageSource = image.createImageSource(unit8Array?.buffer.slice(0, unit8Array?.buffer.byteLength)); - let createPixelMap: image.PixelMap = await imageSource.createPixelMap({ - desiredPixelFormat: image.PixelMapFormat.RGBA_8888 - }); - await imageSource.release(); - return createPixelMap; + const uiContext: UIContext | undefined = AppStorage.get('uiContext'); + try { + let unit8Array = uiContext?.getHostContext()?.resourceManager?.getMediaContentSync(resource.id); + let imageSource = image.createImageSource(unit8Array?.buffer.slice(0, unit8Array?.buffer.byteLength)); + let createPixelMap: image.PixelMap = await imageSource.createPixelMap({ + desiredPixelFormat: image.PixelMapFormat.RGBA_8888 + }); + await imageSource.release(); + return createPixelMap; + } catch (err) { + Logger.error('ImageUtil', `getPixmapFromMedia failed, err.code:${err.code}, err.message:${err.message}`); + return undefined; + } } } \ No newline at end of file diff --git a/VideoPlayerSample/README.md b/VideoPlayerSample/README.md index 251204c73a93cfe7b34b4ca7a5baf53e4d884824..49eeb8ba7b1dd4f64a64b94ccc56515d026945aa 100644 --- a/VideoPlayerSample/README.md +++ b/VideoPlayerSample/README.md @@ -1,24 +1,26 @@ -# 视频播放类应用 +# 实现视频流畅播放且支持后台与焦点打断功能 ### 简介 -本示例主要展示通过HarmonyOS提供的系统播放器AVPlayer和媒体会话等能力,实现视频类应用的开发。 +本示例从用户交互和音频流状态变更两个维度,基于HarmonyOS提供的媒体(AVPlayer)和ArkUI等能力,实现长/短视频的流畅播放,视频支持前后台播放控制、播放形态切换、音频焦点切换、播放设备切换等场景,可以为视频播放应用提供灵活的交互体验和良好的观看效果。 ### 效果预览 -![](screenshots/videoPlayer.gif) + -使用说明: +### 使用说明 -1.启动应用后显示视频播放列表及首个视频自动播放 +1. 启动应用后显示视频播放列表及首个视频自动播放。 -2.点击页面按钮可切换竖屏或横屏模式 +2. 点击页面按钮可切换竖屏或横屏模式。 -3.上下滑动切换视频,并能看到历史播放记录 +3. 上下滑动切换视频,并能看到历史播放记录。 -4.拖动进度条或全屏手势滑动可调节播放进度 +4. 拖动进度条或全屏手势滑动可调节播放进度。 -5.应用切换到后台可以持续播放并且可以播控中心进行控制 +5. 应用切换到后台可以持续播放并且可以播控中心进行控制。 + +6. 全屏播放视频场景下,在屏幕左侧上下滑动可调节音量,在屏幕右侧上下滑动可调节亮度。 ### 工程目录 @@ -39,35 +41,35 @@ │ ├──VideoList.ets // 首页视频列表 │ ├──VideoSide.ets // 视频滑动组件 │ └──VideoDetails.ets // 视频详情信息组件 -│──entry/src/main/resources // 应用资源目录 -│ -└──MediaService/src/main/ets - ├──common - │ └──CommonConstants.ets //常量类 - ├──controller - │ ├──AvPlayerController.ets //视频播放控制 - │ └──AvSessionController.ets //媒体会话控制 - ├──model - │ └──VideoData.ets //视频数据类 - └──utils - ├──BackgroundTaskManager.ets // 后台播放功能 - ├──CommUtils.ets // 工具类 - ├──ImageUtil.ets // 图片像素处理类 - └──Logger.ets // 日志 +├──entry/src/main/resources // 应用资源目录 +├──MediaService/src/main/ets +│ ├──common +│ │ └──CommonConstants.ets //常量类 +│ ├──controller +│ │ ├──AvPlayerController.ets //视频播放控制 +│ │ └──AvSessionController.ets //媒体会话控制 +│ ├──model +│ │ └──VideoData.ets //视频数据类 +│ └──utils +│ ├──BackgroundTaskManager.ets // 后台播放功能 +│ ├──CommUtils.ets // 工具类 +│ ├──ImageUtil.ets // 图片像素处理类 +│ └──Logger.ets // 日志 +└──MediaService/src/main/resources // 应用资源目录 ``` ### 相关权限 -1.后台任务权限:ohos.permission.KEEP_BACKGROUND_RUNNING。 +1. 后台任务权限:ohos.permission.KEEP_BACKGROUND_RUNNING。 -2.Internet网络权限:ohos.permission.INTERNET。 +2. Internet网络权限:ohos.permission.INTERNET。 ### 约束与限制 -1.本示例仅支持标准系统上运行,支持设备:华为手机。 +1. 本示例仅支持标准系统上运行,支持设备:华为手机。 -2.HarmonyOS系统:HarmonyOS NEXT Beta1及以上。 +2. HarmonyOS系统:HarmonyOS 5.0.5 Release及以上。 -3.DevEco Studio版本:DevEco Studio NEXT Beta1及以上。 +3. DevEco Studio版本:DevEco Studio 5.0.5 Release及以上。 -4.HarmonyOS SDK版本:HarmonyOS NEXT Beta1 SDK及以上。 +4. HarmonyOS SDK版本:HarmonyOS 5.0.5 Release SDK及以上。 diff --git a/VideoPlayerSample/README_EN.md b/VideoPlayerSample/README_EN.md new file mode 100644 index 0000000000000000000000000000000000000000..d4b44d558e36f9100f248624b3a2a5daa26dff6a --- /dev/null +++ b/VideoPlayerSample/README_EN.md @@ -0,0 +1,73 @@ +# Video Player + +### Overview + +This sample demonstrates how to develop a video app using the AVPlayer and AVSession capabilities provided by HarmonyOS. + +### Preview + + + + +### How to Use + +1. After the app is launched, the video playlist is displayed and the first video is automatically played. + +2. Touch the button on the screen to switch between portrait and landscape modes. + +3. Swipe up or down to switch between videos, and view historical playback records. + +4. Drag the progress bar or swipe in full-screen mode to adjust the playback progress. + +5. When the app is switched to the background, the playback can continue. You can control the playback in the Media Controller. + + +### Project Directory + +``` +├──entry/src/main/ets +│ ├──entryability +│ │ └──EntryAbility.ets // Entry ability +│ ├──model +│ │ ├──BasicDataSource.ets // Lazy loading data sources +│ │ └──DataModel.ets // Data classes +│ ├──pages +│ │ └──IndexPage.ets // Home page +│ ├──utils +│ │ └──WindowUtil.ets // Window utility class +│ └──view +│ ├──AVPlayer.ets // Video component +│ ├──VideoList.ets // Video list on the home page +│ ├──VideoSide.ets // Video swiper components +│ └──VideoDetails.ets // Video details components +├──entry/src/main/resources // Static resources +└──MediaService/src/main/ets + ├──common + │ └──CommonConstants.ets // Common constants + ├──controller + │ ├──AvPlayerController.ets // Video playback control + │ └──AvSessionController.ets // AVSession control + ├──model + │ └──VideoData.ets // Video data class + └──utils + ├──BackgroundTaskManager.ets // Background playback + ├──CommUtils.ets // Utility class + ├──ImageUtil.ets // Image pixel processing class + └──Logger.ets // Log utility +``` + +### Required Permissions + +1. **ohos.permission.KEEP_BACKGROUND_RUNNING**: allows an app to run in the background. + +2. **ohos.permission.INTERNET**: allows an app to access the Internet. + +### Constraints + +1. The sample is only supported on Huawei phones with standard systems. + +2. The HarmonyOS version must be HarmonyOS 5.0.5 Release or later. + +3. The DevEco Studio version must be DevEco Studio 5.0.5 Release or later. + +4. The HarmonyOS SDK version must be HarmonyOS 5.0.5 Release SDK or later. diff --git a/VideoPlayerSample/build-profile.json5 b/VideoPlayerSample/build-profile.json5 index 48cac0c694203e81fb01ea6d1f80b7e26cd82d1c..608af2db45a2c790dce9df5019757f84cf8dde27 100644 --- a/VideoPlayerSample/build-profile.json5 +++ b/VideoPlayerSample/build-profile.json5 @@ -5,7 +5,8 @@ { "name": "default", "signingConfig": "default", - "compatibleSdkVersion": "5.0.0(12)", + "targetSdkVersion": "5.0.5(17)", + "compatibleSdkVersion": "5.0.5(17)", "runtimeOS": "HarmonyOS", } ], diff --git a/VideoPlayerSample/entry/obfuscation-rules.txt b/VideoPlayerSample/entry/obfuscation-rules.txt index 272efb6ca3f240859091bbbfc7c5802d52793b0b..d59018fe596092b6b264883fe853c40519c4e470 100644 --- a/VideoPlayerSample/entry/obfuscation-rules.txt +++ b/VideoPlayerSample/entry/obfuscation-rules.txt @@ -17,7 +17,7 @@ # -keep-property-name: specifies property names that you want to keep # -keep-global-name: specifies names that you want to keep in the global scope --enable-property-obfuscation +#-enable-property-obfuscation -enable-toplevel-obfuscation --enable-filename-obfuscation +#-enable-filename-obfuscation -enable-export-obfuscation \ No newline at end of file diff --git a/VideoPlayerSample/entry/src/main/ets/entryability/EntryAbility.ets b/VideoPlayerSample/entry/src/main/ets/entryability/EntryAbility.ets index ecede635c6b2338f2664c6399991924e92524502..6bfb7b26bf89a5b8b8a409ddfee2b84ef34ca313 100644 --- a/VideoPlayerSample/entry/src/main/ets/entryability/EntryAbility.ets +++ b/VideoPlayerSample/entry/src/main/ets/entryability/EntryAbility.ets @@ -33,25 +33,25 @@ export default class EntryAbility extends UIAbility { // [StartExclude stage_creat] // Main window is created, set main page for this ability hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); - let windowClass: window.Window = windowStage.getMainWindowSync(); - AppStorage.setOrCreate('windowStage', windowStage); - // [EndExclude stage_creat] - // [StartExclude stage_creat] - windowClass.setWindowLayoutFullScreen(true); - windowClass.setWindowSystemBarProperties({ - statusBarContentColor: '#e6ffffff' - }); - windowStage.loadContent('pages/IndexPage', (err) => { - AppStorage.setOrCreate('uiContext', windowStage.getMainWindowSync().getUIContext()); - if (err.code) { - hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); - return; - } - hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); - // [EndExclude stage_creat] - WindowUtil.getInstance().setWindowStage(windowStage); - // [StartExclude stage_creat] - }); + try { + let windowClass: window.Window = windowStage.getMainWindowSync(); + AppStorage.setOrCreate('windowStage', windowStage); + windowClass.setWindowLayoutFullScreen(true); + windowClass.setWindowSystemBarProperties({ + statusBarContentColor: '#e6ffffff' + }); + windowStage.loadContent('pages/IndexPage', (err) => { + AppStorage.setOrCreate('uiContext', windowStage.getMainWindowSync().getUIContext()); + WindowUtil.getInstance().setWindowStage(windowStage); + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); + }); + } catch (err) { + hilog.error(0x0000, 'testTag', `onWindowStageCreate failed, err.code:${err.code}, err.message:${err.message}`); + } // [EndExclude stage_creat] } // [End stage_creat] diff --git a/VideoPlayerSample/entry/src/main/ets/pages/IndexPage.ets b/VideoPlayerSample/entry/src/main/ets/pages/IndexPage.ets index bff56c87790abb004aa967cd23daae9c4eda7cab..f3cbc386706550c49d5bdf25add189db0f5146d2 100644 --- a/VideoPlayerSample/entry/src/main/ets/pages/IndexPage.ets +++ b/VideoPlayerSample/entry/src/main/ets/pages/IndexPage.ets @@ -29,8 +29,10 @@ const TAG = '[IndexPage]'; @Component struct IndexPage { @State isFloatWindow: boolean = false; - @StorageProp('deviceHeight') @Watch('onWindowSizeChange') deviceHeight: number = AppStorage.get('deviceHeight') || 0; + @StorageProp('deviceHeight') @Watch('onWindowSizeChange') deviceHeight: number = + AppStorage.get('deviceHeight') || 0; // [StartExclude index_page1] + @StorageProp('statusBarHeight') statusBarHeight: number = 0; @State @Watch('onWindowSizeChange') isFullScreen: boolean = false; @State @Watch('onWindowSizeChange') isFullLandscapeScreen: boolean = false; @State sources: VideoData[] = SOURCES; @@ -44,10 +46,12 @@ struct IndexPage { // Turn on window size monitoring in aboutToAppear async aboutToAppear(): Promise { let context = this.getUIContext().getHostContext() as Context; - let windowClass = await window.getLastWindow(context); - // [StartExclude about_appear] - await windowClass.setWindowKeepScreenOn(true); - // [StartExclude about_appear] + try { + let windowClass = await window.getLastWindow(context); + await windowClass.setWindowKeepScreenOn(true); + } catch (err) { + Logger.error(TAG, `aboutToAppear failed, err.code:${err.code}, err.message:${err.message}`); + } // Register window size monitoring this.windowUtil.registerOnWindowSizeChange((size) => { if (size.width > size.height) { diff --git a/VideoPlayerSample/entry/src/main/ets/utils/BreakpointSystem.ets b/VideoPlayerSample/entry/src/main/ets/utils/BreakpointSystem.ets new file mode 100644 index 0000000000000000000000000000000000000000..6b193fd47ebc32d1c79f7a5e0c4d8f5677f8ed33 --- /dev/null +++ b/VideoPlayerSample/entry/src/main/ets/utils/BreakpointSystem.ets @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { window } from '@kit.ArkUI'; +import type { BusinessError } from '@kit.BasicServicesKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; + +const TAG: string = '[BreakpointSystem]'; + +export enum BreakpointTypeEnum { + XS = 'xs', + SM = 'sm', + MD = 'md', + LG = 'lg', + XL = 'xl', +} + +export interface BreakpointTypes { + xs?: T; + sm: T; + md: T; + lg: T; + xl?: T; +} + +export class BreakpointType { + private xs: T; + private sm: T; + private md: T; + private lg: T; + private xl: T; + + public constructor(param: BreakpointTypes) { + this.xs = param.xs || param.sm; + this.sm = param.sm; + this.md = param.md; + this.lg = param.lg; + this.xl = param.xl || param.lg; + } + + public getValue(currentBreakpoint: string): T { + if (currentBreakpoint === BreakpointTypeEnum.XS) { + return this.xs; + } + if (currentBreakpoint === BreakpointTypeEnum.SM) { + return this.sm; + } + if (currentBreakpoint === BreakpointTypeEnum.MD) { + return this.md; + } + if (currentBreakpoint === BreakpointTypeEnum.XL) { + return this.xl; + } + return this.lg; + } +} + +export class BreakpointSystem { + private static instance: BreakpointSystem; + private currentBreakpoint: BreakpointTypeEnum = BreakpointTypeEnum.MD; + + private constructor() { + } + + public static getInstance(): BreakpointSystem { + if (!BreakpointSystem.instance) { + BreakpointSystem.instance = new BreakpointSystem(); + AppStorage.setOrCreate('currentBreakpoint', BreakpointTypeEnum.MD); + } + return BreakpointSystem.instance; + } + + public updateCurrentBreakpoint(breakpoint: BreakpointTypeEnum): void { + if (this.currentBreakpoint !== breakpoint) { + this.currentBreakpoint = breakpoint; + AppStorage.setOrCreate('currentBreakpoint', this.currentBreakpoint); + } + } + + public onWindowSizeChange(window: window.Window): void { + this.updateWidthBp(window); + } + + public updateWidthBp(window: window.Window): void { + try { + const mainWindow: window.WindowProperties = window.getWindowProperties(); + const windowWidth: number = mainWindow.windowRect.width; + const windowWidthVp = window.getUIContext().px2vp(windowWidth); + let widthBp: BreakpointTypeEnum = BreakpointTypeEnum.MD; + if (windowWidthVp < 320) { + widthBp = BreakpointTypeEnum.XS; + } else if (windowWidthVp >= 320 && windowWidthVp < 600) { + widthBp = BreakpointTypeEnum.SM; + } else if (windowWidthVp >= 600 && windowWidthVp < 840) { + widthBp = BreakpointTypeEnum.MD; + } else if (windowWidthVp >= 840 && windowWidthVp < 1440) { + widthBp = BreakpointTypeEnum.LG; + } else { + widthBp = BreakpointTypeEnum.XL; + } + this.updateCurrentBreakpoint(widthBp); + } catch (error) { + const err: BusinessError = error as BusinessError; + hilog.error(0x0000, TAG, `UpdateBreakpoint fail, error code: ${err.code}, message: ${err.message}`); + } + } +} \ No newline at end of file diff --git a/VideoPlayerSample/entry/src/main/ets/utils/WindowUtil.ets b/VideoPlayerSample/entry/src/main/ets/utils/WindowUtil.ets index a8a4a4dc08d076d54ccabcc5660c24c5194f61a4..1fd1a6e93728864e9cac0e5f9e37e189cdaffa1e 100644 --- a/VideoPlayerSample/entry/src/main/ets/utils/WindowUtil.ets +++ b/VideoPlayerSample/entry/src/main/ets/utils/WindowUtil.ets @@ -14,12 +14,12 @@ */ import { window } from '@kit.ArkUI'; -import { CommonConstants, Logger } from '@ohos/MediaService'; +import { Logger } from '@ohos/MediaService'; import { BusinessError } from '@kit.BasicServicesKit'; -import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BreakpointSystem } from './BreakpointSystem'; const TAG: string = '[WindowUtil]'; -const uiContext: UIContext | undefined = AppStorage.get('uiContext'); + export class WindowUtil { private static instance: WindowUtil; @@ -35,6 +35,7 @@ export class WindowUtil { // [Start set_stage1] public setWindowStage(windowStage: window.WindowStage): void { + const uiContext: UIContext | undefined = AppStorage.get('uiContext'); this.windowStage = windowStage; this.windowStage.getMainWindow((err, windowClass: window.Window) => { // [StartExclude set_stage1] @@ -56,11 +57,25 @@ export class WindowUtil { // [StartExclude set_stage1] AppStorage.setOrCreate('statusBarHeight', uiContext?.px2vp(area.topRect.height)); AppStorage.setOrCreate('navBarHeight', uiContext?.px2vp(naviBarArea.bottomRect.height)); + windowClass.on('windowSizeChange', () => BreakpointSystem.getInstance().onWindowSizeChange(windowClass)); + windowClass.on('avoidAreaChange', (avoidAreaOption) => { + WindowUtil.setAvoidArea(avoidAreaOption.type, avoidAreaOption.area); + }); // [EndExclude set_stage1] }); } // [End set_stage1] + // Get status bar height and indicator height. + public static setAvoidArea(type: window.AvoidAreaType, area: window.AvoidArea) { + const uiContext: UIContext = AppStorage.get('uiContext')!; + if (type === window.AvoidAreaType.TYPE_SYSTEM) { + AppStorage.setOrCreate('statusBarHeight', uiContext?.px2vp(area.topRect.height)); + } else { + AppStorage.setOrCreate('navBarHeight', uiContext?.px2vp(area.bottomRect.height)); + } + } + // [Start set_main] // Select the corresponding parameters according to the rotation strategy of the application itself. setMainWindowOrientation(orientation: window.Orientation, callback?: Function): void { @@ -74,7 +89,7 @@ export class WindowUtil { this.mainWindowClass.setPreferredOrientation(orientation).then(() => { callback?.(); }).catch((err: BusinessError) => { - hilog.error(0x0001, TAG, `Failed to set the ${orientation} of main window. Code:${err.code}, message:${err.message}`); + Logger.error(`Failed to set the ${orientation} of main window. Code:${err.code}, message:${err.message}`); }); } // [End set_main] @@ -85,7 +100,11 @@ export class WindowUtil { return; } // Set the status bar and navigation bar to be invisible in full-screen mode. - this.mainWindowClass.setWindowSystemBarEnable([]); + try { + this.mainWindowClass.setWindowSystemBarEnable([]); + } catch (err) { + Logger.error(TAG, `disableWindowSystemBar failed, err.code:${err.code}, err.message:${err.message}`); + } } enableWindowSystemBar(): void { @@ -93,7 +112,11 @@ export class WindowUtil { Logger.error(`MainWindowClass is undefined`); return; } - this.mainWindowClass.setWindowSystemBarEnable(['status', 'navigation']); + try { + this.mainWindowClass.setWindowSystemBarEnable(['status', 'navigation']); + } catch (err) { + Logger.error(TAG, `enableWindowSystemBar failed, err.code:${err.code}, err.message:${err.message}`); + } } setLandscapeMultiWindow(enable: boolean) { @@ -101,10 +124,14 @@ export class WindowUtil { Logger.error(`MainWindowClass is undefined`); return; } - if (enable) { - this.mainWindowClass?.enableLandscapeMultiWindow(); - } else { - this.mainWindowClass?.disableLandscapeMultiWindow(); + try { + if (enable) { + this.mainWindowClass?.enableLandscapeMultiWindow(); + } else { + this.mainWindowClass?.disableLandscapeMultiWindow(); + } + } catch (err) { + Logger.error(TAG, `setLandscapeMultiWindow failed, err.code:${err.code}, err.message:${err.message}`); } } // [Start size_change] diff --git a/VideoPlayerSample/entry/src/main/ets/view/AVPlayer.ets b/VideoPlayerSample/entry/src/main/ets/view/AVPlayer.ets index 5ac93eef4a14a2aef9bc98eafa5600c2b0a96ebe..79faaa81a8a788b0d19574a2fbd6d663a9f039c2 100644 --- a/VideoPlayerSample/entry/src/main/ets/view/AVPlayer.ets +++ b/VideoPlayerSample/entry/src/main/ets/view/AVPlayer.ets @@ -27,6 +27,7 @@ const TAG = '[VideoPlayer]'; @Component export struct VideoPlayer { @Consume('pageInfo') pageInfo: NavPathStack; + @StorageProp('navBarHeight') navBarHeight: number = 0; @BuilderParam videoRightSide?: () => void; @BuilderParam videoDes?: () => void; @Prop isFullLandscapeScreen: boolean = false; @@ -38,7 +39,7 @@ export struct VideoPlayer { @Prop @Watch('onIndexChange') curIndex: number = CommonConstants.CURINDEX_DEFAULT_NUM; @State isTimeDisplay: number = 0; @State trackThicknessSize: number = CommonConstants.TRACK_SIZE_MIN; - @State avPlayerController: AvPlayerController = new AvPlayerController(this.curSource, this.getUIContext()); + @State avPlayerController: AvPlayerController = new AvPlayerController(this.curSource); @State sliderStyle: SliderStyle = SliderStyle.NONE; @State isShowTips: boolean = false; @State isSliderDragging: boolean = false; @@ -69,7 +70,7 @@ export struct VideoPlayer { aboutToAppear(): void { let windowClass: window.Window | undefined = undefined; - const context: Context = this.getUIContext().getHostContext()!; + const context: Context | undefined = AppStorage.get('context'); settings.getValue(context, settings.display.SCREEN_BRIGHTNESS_STATUS, settings.domainName.DEVICE_SHARED) .then((value) => { hilog.info(0x0000, 'AVPlayer', `Promise:value -> ${JSON.stringify(value)}`); @@ -77,14 +78,14 @@ export struct VideoPlayer { }) try { - window.getLastWindow(this.getUIContext().getHostContext()!, (err, data) => { - if (err) { - hilog.error(0x0000, 'AVPlayer', - `Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`); - } - windowClass = data; + window.getLastWindow(this.getUIContext().getHostContext()).then((window: window.Window) => { + windowClass = window; this.screenHeight = windowClass.getWindowProperties().windowRect.height; - }) + }).catch((err: BusinessError) => { + hilog.error(0x0000, 'AVPlayer', + `Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`); + }); + } catch (exception) { hilog.error(0x0000, 'AVPlayer', `Failed to obtain the top window. Cause code: ${exception.code}, message: ${exception.message}`); @@ -248,7 +249,6 @@ export struct VideoPlayer { }) } .width('50%') - // [End panel] // [Start screen5] @@ -288,7 +288,6 @@ export struct VideoPlayer { }) .height('100%') .width('50%') - // [End screen5] } .height('100%') @@ -365,7 +364,7 @@ export struct VideoPlayer { }) .visibility(!this.isFullScreen || this.isFullLandscapeScreen || this.isFloatWindow || this.isSliderDragging ? Visibility.None : Visibility.Visible) - .margin({ bottom: this.isFullScreen ? (AppStorage.get('navBarHeight') || 0) : 0 }) + .margin({ bottom: this.isFullScreen ? this.navBarHeight : 0 }) .width(CommonConstants.WIDTH_FULL_PERCENT) } .zIndex(CommonConstants.Z_INDEX_MAX) @@ -396,7 +395,6 @@ export struct VideoPlayer { }) ) } - // [End build] // The popup constructor defines the content of the dialog box @@ -557,8 +555,7 @@ export struct VideoPlayer { left: $r('app.float.padding_16'), right: $r('app.float.padding_16'), bottom: this.isSliderDragging ? $r('app.float.space_48') : - this.isFullLandscapeScreen && !this.isFloatWindow ? (AppStorage.get('navBarHeight') || 0) - : $r('app.float.margin_small') + this.isFullLandscapeScreen && !this.isFloatWindow ? this.navBarHeight : $r('app.float.margin_small') }) } @@ -602,9 +599,13 @@ export struct VideoPlayer { this.avPlayerController.setIsPlaying(false); this.isTimeDisplay = 0; this.trackThicknessSize = CommonConstants.TRACK_SIZE_MIN; - let context = this.getUIContext().getHostContext()!; - let windowClass = await window.getLastWindow(context); - await windowClass.setWindowKeepScreenOn(false); + let context: Context = AppStorage.get('context')!; + try { + let windowClass = await window.getLastWindow(context); + await windowClass.setWindowKeepScreenOn(false); + } catch (err) { + Logger.error(TAG, `iconOnclick isPlaying failed, err.code:${err.code}, err.message:${err.message}`); + } return; } if (this.avPlayerController.isReady === true) { @@ -623,9 +624,13 @@ export struct VideoPlayer { } }, CommonConstants.TIMER_INTERVAL); } - let context = this.getUIContext().getHostContext()!; - let windowClass = await window.getLastWindow(context); - await windowClass.setWindowKeepScreenOn(true); + let context = this.getUIContext().getHostContext() as Context; + try { + let windowClass = await window.getLastWindow(context); + await windowClass.setWindowKeepScreenOn(true); + } catch (err) { + Logger.error(TAG, `iconOnclick failed, err.code:${err.code}, err.message:${err.message}`); + } } aboutToDisappear(): void { diff --git a/VideoPlayerSample/entry/src/main/ets/view/VideoDetails.ets b/VideoPlayerSample/entry/src/main/ets/view/VideoDetails.ets index 6975ed951c4eaa5c5870958e0f4cd602ec2314c1..ea87fc01515d08da485f52036a86ef753a26d354 100644 --- a/VideoPlayerSample/entry/src/main/ets/view/VideoDetails.ets +++ b/VideoPlayerSample/entry/src/main/ets/view/VideoDetails.ets @@ -13,7 +13,6 @@ * limitations under the License. */ import { CommonConstants as Const, VideoData } from '@ohos/MediaService'; -import { promptAction } from '@kit.ArkUI'; @Component({ freezeWhenInactive: true }) export struct VideoDetails { diff --git a/VideoPlayerSample/entry/src/main/ets/view/VideoList.ets b/VideoPlayerSample/entry/src/main/ets/view/VideoList.ets index 065e1e007e62ee48eb5d6add30a7deea6e6173f2..8d3694696b0241cb026aed43281caa18c96346b7 100644 --- a/VideoPlayerSample/entry/src/main/ets/view/VideoList.ets +++ b/VideoPlayerSample/entry/src/main/ets/view/VideoList.ets @@ -17,6 +17,7 @@ import { CommonConstants, VideoData } from '@ohos/MediaService'; @Component({ freezeWhenInactive: true }) export struct VideoList { + @StorageProp('navBarHeight') navBarHeight: number = 0; @Link currentIndex: number; @Prop sources: VideoData[]; onItemClick?: (index: number) => void = () => { @@ -45,8 +46,6 @@ export struct VideoList { Text() { if (index === this.currentIndex) { SymbolSpan($r('sys.symbol.play_round_rectangle_fill')) - .width($r('app.float.size_20')) - .height($r('app.float.size_20')) .fontColor([$r('app.color.button_fill_color')]) .effectStrategy(SymbolEffectStrategy.HIERARCHICAL) } @@ -94,7 +93,7 @@ export struct VideoList { .listDirection(Axis.Vertical) .margin({ top: $r('app.float.padding_12'), - bottom: (AppStorage.get('navBarHeight') || 0) + CommonConstants.SPACE_16 + bottom: this.navBarHeight + CommonConstants.SPACE_16 }) .scrollBar(BarState.Off) } diff --git a/VideoPlayerSample/entry/src/main/ets/view/VideoSide.ets b/VideoPlayerSample/entry/src/main/ets/view/VideoSide.ets index 912aa84f180b641f9781e3097105536dd0bd02e5..33d23c5d06b017536ed83b5b4fa27e677c98a6a2 100644 --- a/VideoPlayerSample/entry/src/main/ets/view/VideoSide.ets +++ b/VideoPlayerSample/entry/src/main/ets/view/VideoSide.ets @@ -14,7 +14,6 @@ */ import { CommonConstants as Const, VideoData } from '@ohos/MediaService'; -import { promptAction } from '@kit.ArkUI'; @Component export struct RightSide { diff --git a/VideoPlayerSample/screenshots/videoPlayer.gif b/VideoPlayerSample/screenshots/videoPlayer.gif index 98cf15192ed5098fb3cb9e0b673b51d1ca594d86..b264f13de1caa4bd48d631398841fa3514d4b863 100644 Binary files a/VideoPlayerSample/screenshots/videoPlayer.gif and b/VideoPlayerSample/screenshots/videoPlayer.gif differ diff --git a/VideoPlayerSample/screenshots/videoPlayer_EN.gif b/VideoPlayerSample/screenshots/videoPlayer_EN.gif new file mode 100644 index 0000000000000000000000000000000000000000..d534be23b83737fca439292f690a088125edbb01 Binary files /dev/null and b/VideoPlayerSample/screenshots/videoPlayer_EN.gif differ