场景一:视频播放预加载,边下边播

方案

  1. 创建一个沙箱文件,并获取沙箱文件的readFd和writeFd。
  2. 通过.new rcp.Request(DOWNLOAD\_URL)创建网络下载请求request,配置request的TracingConfiguration,在onDataReceive回调中通过fs.writeSync传入沙箱文件的writeFd,将下载的数据流写入本地沙箱文件,将fs.writeSync返回写入字节大小作为网络下载大小downloadSize,根据downloadSize和下载大小(默认1024*1024字节,AVPlayer默认缓存为1M)配置request的transferRange属性,控制网络下载的起始字节和结束字节。
  3. 通过RCP的session.fetch传入request下载获取网络视频资源。
  4. 配置AVPlayer的datasrc属性,在datasrc的回调函数中,通过fs.readSync传入沙箱文件的readFd,将沙箱文件的数据写入内存buffer,沙箱文件大小为0时开启网络下载,当pos(表示填写的数据在资源文件中的位置)小于沙箱文件100kb时,再次开启网络下载进而实现分段下载,该回调函数在AVPlayer解析数据时触发,在边下边播的场景中,会不断触发该回调。
  5. 设置AVPlayer播放资源,将datasrc设置给AVPlayer。

核心代码

控制网络下载的起始字节和结束字节。

function download(length: number) {
  console.info('MineRcp download from = ' + downloadSize + 'to =  ' + (downloadSize + length - 1));
  request.transferRange = {
    from: downloadSize,
    to: downloadSize + length - 1,
  };
  downloadStarted = true;
  session.fetch(request).then(() => {
    downloadStarted = false;
  }).catch(() => {
    downloadStarted = false;
  });
}

onDataReceive回调中通过fs.writeSync传入沙箱文件的writeFd,将下载的数据流写入本地沙箱文件。

request.configuration = {
  tracing: {
    httpEventsHandler: {
      onDataReceive: (incomingData: ArrayBuffer) => {
        const writeLength = fs.writeSync(this.writeFd, GetDecodedBuffer(incomingData));
        downloadSize += writeLength;
        console.info('MineRcp recieve Length = ' + writeLength.toString() + " , recieve = " + downloadSize);
        return incomingData.byteLength;
      }
    }
  }
};

配置AVPlayer的datasrc属性。

let src: media.AVDataSrcDescriptor = {
  fileSize: -1,
  callback: (buf: ArrayBuffer, length: number, pos?: number) => {
    // buffer,ArrayBuffer类型,表示被填写的内存,必选。
    //
    // - length,number类型,表示被填写内存的最大长度,必选。
    //
    // - pos,number类型,表示填写的数据在资源文件中的位置,可选,当fileSize设置为-1时,该参数禁止被使用。
    // - 返回值,number类型,返回要填充数据的长度。
    let num = 0;
    if (buf === undefined || length === undefined) {
      return -1;
    }

    num = fs.readSync(this.readFd, buf);
    console.info('MineRcp cacheBuffer after checkBuffer Length = ' + num.toString() + ', pos: ' + pos +
      ', downloadSize: ' +
      downloadSize);
    if (num > 0) {
      if (pos != undefined && downloadSize - pos < 100 * 1024) {
        console.info('MineRcp data not enough, download more');
        let downloadLength = 1024 * 1024;
        if (this.fileSize - downloadSize <= downloadLength) {
          downloadLength = this.fileSize - downloadSize;
        }
        if (!downloadStarted) {
          download(downloadLength);
        }
      }
      return num;
    }

    if (num === 0) {
      console.info('MineRcp no data read, download more');
      if (!downloadStarted) {
        let downloadLength = 1024 * 1024;
        if (this.fileSize - downloadSize <= downloadLength) {
          downloadLength = this.fileSize - downloadSize;
        }
        download(downloadLength);
      }
      return 0;
    }

    return -1;
  }
}
src.fileSize = this.fileSize;
this.isSeek = false; // 支持seek操作
this.player.dataSrc = src;

场景二:同一页面内视频播放横竖屏全屏切换无缝转场

方案

设置竖屏和全屏两个按钮,分别添加点击事件。

  1. 首先设置窗口方向,通过window的setPreferredOrientation接口来设置屏幕方向,接口文档:setPreferredOrientation
  2. 设置沉浸式窗口,为使视屏能充斥整个屏幕,防止屏幕上下两边的任务栏等区域影响全屏观看效果,通过window的setWindowLayoutFullScreen接口设置窗口沉浸式,接口文档:setWindowLayoutFullScreen
  3. 根据屏幕状态点击全屏按钮切换到相应屏幕。

核心代码

// 设置窗口方向
setR(orientation: number) {
  window.getLastWindow(getContext(this)).then((win) => {
    win.setPreferredOrientation(orientation).then((data) => {
      console.log('setWindowOrientation: ' + orientation + ' Succeeded. Data: ' + JSON.stringify(data));
    }).catch((err: string) => {
      console.log('setWindowOrientation: Failed. Cause: ' + JSON.stringify(err));
    });
  }).catch((err: string) => {
    console.log('setWindowOrientation: Failed to obtain the top window. Cause: ' + JSON.stringify(err));
  });
}

//设置沉浸式窗口
setFullScreen(isLayoutFullScreen: boolean) {
  window.getLastWindow(getContext(this)).then((win) => {
    win.setWindowLayoutFullScreen(isLayoutFullScreen, (err: BusinessError) => {
      const errCode: number = err.code;
      if (errCode) {
        console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(err));
        return;
      }
      console.info('Succeeded in setting the window layout to full-screen mode.');
    });
  }).catch((err: string) => {
    console.log('setWindowOrientation: Failed to obtain the top window. Cause: ' + JSON.stringify(err));
  });
}

// 横屏按钮
async switchFullScreen() {
  if (this.isFullScreen) {
    // 切换正常播放
    await   this.setR(1);
    await   this.setFullScreen(false)
    this.isFullScreen=false;
    return
  } else {
    // 切换横屏
    if (this.isVerticalScreen) {
      this.isVerticalScreen=!  this.isVerticalScreen;
    }
    this.setR(2);
    this.setFullScreen(true)
    this.isFullScreen=! this.isFullScreen
  }
}

// 竖屏全屏按钮
async switchVerticalFullScreen() {
  if (this.isVerticalScreen) {
    //切换正常播放
    // await   this.setR(1);
    await this.setFullScreen(false)
    this.isVerticalScreen = !this.isVerticalScreen
    // this.isVerticalScreen=!this.isVerticalScreen
    return
  } else {
    if (this.isFullScreen) {
      //切换 竖屏
      this.isFullScreen = !this.isFullScreen
    }
    await this.setR(1);
    await this.setFullScreen(true)
    this.isVerticalScreen = !this.isVerticalScreen
  }
}

场景三:跨页面视频播放无缝转场

方案

  1. 在page1页面通过GlobalContext将AVPlayer当做全局单例变量放到Map<string, media.AVPlayer\>里面。
  2. 通过router跳转到page2页面,通过Map<string, media.AVPlayer\>获取单例AVPlayer,将page2页面的Xcomponent的SurfaceId设置给AVPlayer。

核心代码

import { media } from '@kit.MediaKit';
import { router } from '@kit.ArkUI';

export class GlobalContext {
  private static instance: GlobalContext;
  private _objects = new Map<string, media.AVPlayer>();

  public static getContext(): GlobalContext {
    if (!GlobalContext.instance) {
      GlobalContext.instance = new GlobalContext();
    }
    return GlobalContext.instance;
  }

  getObject(value: string): media.AVPlayer | undefined {
    return this._objects.get(value);
  }

  setObject(key: string, objectClass: media.AVPlayer): void {
    this._objects.set(key, objectClass);
  }
}
// ...
onJumpClick(): void {
  router.replaceUrl({
    url: 'pages/player' // 目标url
  }, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  })
}
// ...

将AVPlayer放进全局map。

if (this.player) {
  GlobalContext.getContext().setObject('value', this.player);
}
.onLoad(() => {
  this.mXComponentController.setXComponentSurfaceSize({ surfaceWidth: this.xComponentWidth, surfaceHeight: this.xComponentHeight });
  this.surfaceID=this.mXComponentController.getXComponentSurfaceId();
    console.info('onLoad '+this.surfaceID)
  //取出全局map里面的AVPlayer
  avPlayer=GlobalContext.getContext().getObject('value');
  if (avPlayer) {
    avPlayer.surfaceId=this.surfaceID;
  }
})

场景四:视频截图并保存到相册

方案

  1. 通过createPixelMapFromSurfaceSync传入surfaceID和region获取pixelMap。
  2. 通过imagePackerApi.packToFile将pixelMap编码保存到沙箱图片。
  3. 通过saveButton将图片保存到相册。

核心代码

//视频截图
async getVideoFrame(): Promise<PixelMap> {
  // let rect: SurfaceRect = this.xComponentController?.getXComponentSurfaceRect() as SurfaceRect;
  if (this.player) {
  region = {
    x: 0 as number,
    y: 0 as number,
    size: { width: Math.trunc(this.player?.width), height: Math.trunc(this.player?.height) }
  };
}
this.pixma = image.createPixelMapFromSurfaceSync(this.surfaceID, region);
return this.pixma;
}
//保存到相册
async saveToAlbum() {
  let file = fs.openSync(fileUri, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
  const imagePackerApi = image.createImagePacker();
  let packOpts: image.PackingOption = { format: "image/jpeg", quality: 100 };
  imagePackerApi.packToFile(this.pixma, file.fd, packOpts).then(async () => {
    // 直接打包进文件
    try {
      let context = getContext();
      let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
      // 需要确保fileUri对应的资源存在
      let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
        photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, fileUri);
      await phAccessHelper.applyChanges(assetChangeRequest);
      console.info('createAsset successfully, uri: ' + assetChangeRequest.getAsset().uri);
    } catch (err) {
      console.error(`create asset failed with error: ${err.code}, ${err.message}`);
    }
  }).catch((error: BusinessError) => {
    console.error('Failed to pack the image. And the error is: ' + error);
  })
}

场景五:切换播放hdrvivid视频

方案

  1. 通过reset方法使avplayer进入adle状态。
  2. 设置fdSrc属性,重置hdrvivid视频播放资源。

核心代码

async resetHdrVivid() {
  let context = getContext(this) as common.UIAbilityContext;
  let fileDescriptor = await context.resourceManager.getRawFd('hdrVivid.mp4');
  let avFileDescriptor: media.AVFileDescriptor =
    { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };

  // 为fdSrc赋值触发initialized状态机上报
  if (this.player) {
    this.player.fdSrc = avFileDescriptor;
  }
}

场景六:视频同步缩放

方案

  1. 通过xComponentController.getXComponentSurfaceRect()获取当前视频surface大小。
  2. xComponentController!.setXComponentSurfaceRect设置当前视频surface大小。

核心代码

let rect: SurfaceRect = this.xComponentController?.getXComponentSurfaceRect() as SurfaceRect;
if (this.ScalingFlag) {
  this.xComponentController!.setXComponentSurfaceRect({
    offsetX: 0,
    offsetY: 0,
    surfaceWidth:rect.surfaceWidth,
    surfaceHeight: rect.surfaceHeight/2
  });
  this.ScalingFlag=!this.ScalingFlag
}else {
  this.xComponentController!.setXComponentSurfaceRect({
    offsetX: 0,
    offsetY: 0,
    surfaceWidth:rect.surfaceWidth,
    surfaceHeight: rect.surfaceHeight*2
  });
  this.ScalingFlag=!this.ScalingFlag
}

场景七:视频滑动调整音量、亮度

方案

  1. 添加视频音量,亮度滑块进度条。
  2. 将音量,屏幕的亮度和滑块的value实现双向绑定。
  3. XComponent左侧添加垂直拖动手势,根据滑动偏移量,通过player.setVolume调整音量。
  4. XComponent右侧添加垂直拖动手势,根据滑动偏移量,通过window.setWindowBrightness调整亮度。
  5. 通过触摸点的X轴坐标控制音量和亮度滑块的生效区域。

核心代码

音量滑块。

Slider({
  value: this.currentVolume,
  min: 0,
  max: 1,
  style: SliderStyle.OutSet,
  step: 0.01
})
  .width('60%')
  .id('Slider')
  .blockColor(Color.White)
  .trackColor(Color.Gray)
  .selectedColor($r('app.color.slider_selected'))
  .showTips(false)
  .onChange((value: number, mode: SliderChangeMode) => {

  })

添加拖动手势,动态调整音量。

.onActionUpdate((event: GestureEvent | undefined) => {
  if (event) {
    if (this.isFullScreen&&this.currentPositionX<150) {
      //todo  现因控制
      this.isVolume= Visibility.Visible;
      let preVolume = this.currentVolume - event.offsetY / 8000
      if (preVolume > 1) {
        preVolume = 1
      }
      if (preVolume < 0) {
        preVolume = 0
      }
      this.currentVolume = preVolume
      this.player?.setVolume(preVolume);
    }
    if (!this.isFullScreen&&this.currentPositionX<80) {
      //todo  现因控制
      this.isVolume= Visibility.Visible;
      let preVolume = this.currentVolume - event.offsetY / 8000
      if (preVolume > 1) {
        preVolume = 1
      }
      if (preVolume < 0) {
        preVolume = 0
      }
      this.currentVolume = preVolume
      this.player?.setVolume(preVolume);
    }
  }
})
  .onActionEnd(() => {
    this.isVolume = Visibility.None;
  })

显隐控制亮度滑块。

Slider({
  value: this.windowBrightness,
  min: 0,
  max: 1,
  style: SliderStyle.OutSet,
  step: 0.1
})
  .width('60%')
  .id('Slider')
  .blockColor(Color.White)
  .visibility(this.isBrightness)
  .trackColor(Color.Gray)
  .selectedColor($r('app.color.slider_selected'))
  .showTips(false)
  .onChange((value: number, mode: SliderChangeMode) => {
  })

添加拖动手势,动态调整亮度。

PanGesture(this.panOption)
  .onActionStart((event: GestureEvent | undefined) => {
    console.info('Pan start');
    this.isBrightness = Visibility.Visible;
  })// 当触发拖动手势时,根据回调函数修改组件的布局位置信息
  .onActionUpdate((event: GestureEvent | undefined) => {
    if (event) {
      let preWindowBrightness = this.windowBrightness - event.offsetY / 10000
      if (preWindowBrightness > 1) {
        preWindowBrightness = 1
      }
      if (preWindowBrightness < 0) {
        preWindowBrightness = 0
      }
      this.windowBrightness = preWindowBrightness
      this.setWindowBrightness(preWindowBrightness);
      console.log('preWindowBrightness' + event.offsetY / 10000);
    }
  })
  .onActionEnd(() => {
    this.isBrightness = Visibility.None;
  })

场景八:视频长按快进

方案

  1. 给XComponent组件添加长按手势。
  2. 长按动作触发时通过setSpeed设置为2倍速,长按动作结束设置为1倍速。

核心代码

//长按手势
LongPressGesture({ repeat: false })// 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
  .onAction((event: GestureEvent) => {
    this.player?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
  })// 长按动作一结束触发
  .onActionEnd((event: GestureEvent) => {
    this.player?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
  })

场景九:视频滑动seek,实现进度预览

方案

  1. 添加视频滑块进度条。
  2. 将进度条的值和avplayer的当前时间实现双向绑定。
  3. 定时任务刷新视频当前播放时间。
  4. 给XComponent添加水平拖动手势实现seek。
  5. 通过用createAVImageGenerator()创建AVImageGenerator对象,设置AVImageGenerator对象的属性fdSrc,fdSrc需要和AVPlayer的视频源保持一致。
  6. 在滑动手势触发时,根据偏移量生成seekTime,AVPlayer正常播放,通过AVImageGenerator对象的fetchFrameByTime方法传入seekTime(注意换算成微秒)生成pixelMap,实现进度预览。
  7. 滑动手势结束,AVPlayer根据最终的seektime进行seek。
  8. 拖动滑块时,滑块移动中,根据滑块value值调用fetchFrameByTime生成pixelMap,实现进度预览。
  9. 拖动滑块结束,AVPlayer根据滑块最终的value值进行seek。

核心代码

Row() {
  Image(this.pauseFlag ? $r('app.media.ic_video_play') : $r('app.media.ic_video_pause'))// 暂停/播放
    .id('play')
    .width($r('app.float.size_40'))
    .height($r('app.float.size_40'))
    .onClick(() => {
      if (this.pauseFlag) {
        this.avPlayManage.pause();
        this.pauseFlag = false;
      } else {
        this.avPlayManage.play();
        this.pauseFlag = true;
      }
    })
  // 左侧时间
  Text(timeConvert(this.currentTime))
    .fontColor(Color.White)
    .textAlign(TextAlign.End)
    .fontWeight(FontWeight.Regular)
    .margin({ left: $r('app.float.size_10') })
}

Row() {
  Slider({
    value: this.currentTime,
    min: 0,
    max: this.durationTime,
    style: SliderStyle.OutSet
  })
    .id('Slider')
    .blockColor(Color.White)
    .trackColor(Color.Gray)
    .selectedColor($r('app.color.slider_selected'))
    .showTips(false)

}
.layoutWeight(1)

Row() {
  // 右侧时间
  Text(timeConvert(this.durationTime))
    .fontColor(Color.White)
    .fontWeight(FontWeight.Regular)
}
case 'prepared': // prepare调用成功后上报该状态机
console.info('AVPlayer state prepared called.');
if (this.player) {
  this.player.loop = true;
  this.durationTime = this.player.duration;
  this.currentTime = this.player.currentTime;
}
setInterval(() => { // 更新当前时间
  if (!this.isSwiping && this.player?.currentTime) {
    this.currentTime = this.player?.currentTime;
  }
}, SET_INTERVAL);

添加拖动手势,实现seek。

PanGesture(this.panOptionSeek)
  .onActionStart((event: GestureEvent | undefined) => {
    console.info('Pan start');

    this.isPullBar=Visibility.Visible;
    this.isPullBarFlag=true;
    this.isSwiping=true
    console.info('this.isPullBar st '+this.isPullBar)
    this.lastFetch= Math.trunc(this.currentTime/1000)
  })// 当触发拖动手势时,根据回调函数修改组件的布局位置信息
  .onActionUpdate(async (event: GestureEvent | undefined) => {
    if (event&&this.player) {
      console.log("currentTime log"+ this.currentTime);
      this.seekTime = this.player.currentTime + event.offsetX*25
      if (this.seekTime > this.durationTime) {
        this.seekTime = this.durationTime
      }
      if (this.seekTime < 0) {
        this.seekTime = 0
      }
      console.log('currentTime seek '+this.seekTime+'off '+event.offsetX);
      console.log('currentTime  seekTime seek '+timeConvert(this.seekTime));
      this.MayFetch=Math.trunc(this.seekTime/1000);
      if (this.MayFetch!=this.lastFetch) {
        this.testFetchFrameByTime(this.MayFetch*1000*1000);

        this.lastFetch=this.MayFetch;
      }
      // this.playerSeek?.seek(this.seekTime,2);
      console.log("this.currentTime" + this.currentTime);
      console.log("event.offsetX" + event.offsetX);
      console.log("this.durationTime" + this.durationTime);
    }
  })
  .onActionEnd(async () => {

    if (this.player) {
      this.player.seek(this.lastFetch*1000,2);
    }
    //seektime换成秒

    this.isSwiping=false;
    console.info('this.isPullBar End'+this.isPullBar)
    console.info('this.isPullBar End'+this.isPullBarFlag)
    // 定时任务,三秒后隐藏进度条
    clearInterval(this.intervalID);
    this.interval();
  })

滑块组件滑动实现seek。

.onChange(async (value: number, mode: SliderChangeMode) => {
  if (!this.isSwiping) {
    if (mode == SliderChangeMode.Begin) {
      console.info('mode Begin' + mode)
      this.isSeeking = true;
      this.isPullBarFlag=true;
      this.isPullBar=Visibility.Visible;
      this.lastFetch= Math.trunc(value/1000)
      return
    }
    if (mode == SliderChangeMode.Moving) {
      console.info('mode Moving '+ mode)
      this.seekTime=value;
      this.MayFetch=Math.trunc(this.seekTime/1000);
      if (this.MayFetch!=this.lastFetch) {
        this.testFetchFrameByTime(this.MayFetch*1000*1000);
        this.lastFetch=this.MayFetch;
        return
      }
      return
    }
    if (mode == SliderChangeMode.End) {
      console.info('mode End '+ mode)
      this.isSeeking = false
      this.avPlayManage?.seek(this.lastFetch*1000, 2)
      //定时任务,三秒后隐藏进度条
      clearInterval(this.intervalID);
      this.interval();
      return
    }
    if (mode == SliderChangeMode.Click) {
      console.info('mode Click'+ mode)
      this.avPlayManage.seek(value, 2)
    }
  }
})

场景十:视频单击显示进度条,双击暂停

方案

  1. 给Xcomponent添加单击手势,控制视频进度条组件显隐。
  2. 添加定时任务,单击手势结束3秒后隐藏视频进度条。
  3. 给Xcomponent添加双击手势,实现视频的播放和暂停。

核心代码

// 绑定count为1的TapGesture
TapGesture({ count: 1 })
  .onAction((event: GestureEvent|undefined) => {
    if(event&&!this.isSwiping){
      // this.value = JSON.stringify(event.fingerList[0]);
      this.isPullBarFlag=!this.isPullBarFlag
      this.pullBar()
    }
  })
pullBar():void{
  if (this.isPullBarFlag) {
  //拉起
  this.isPullBar=Visibility.Visible;
  //定时任务3秒后关闭
  this.interval();

}else {
  this.isPullBar=Visibility.None;
  this.isPullBarFlag=false;
}
}
interval():void{
  if (!this.intervalID){
  this.intervalID=setTimeout(() => { //
    if (!this.isSeeking! &&this.isSwiping && this.isPullBarFlag) {
      this.isPullBar=Visibility.None
      this.isPullBarFlag=false;
    }
  }, 3000);

}else {
  clearInterval(this.intervalID)
  this.intervalID=setTimeout(() => { //
    if (!this.isSeeking!&&!this.isSwiping && this.isPullBarFlag) {
      this.isPullBar=Visibility.None
      this.isPullBarFlag=false;
    }
  }, 3000);
}
}
TapGesture({ count: 2 })
  .onAction((event: GestureEvent|undefined) => {
    if(event){
      // this.value = JSON.stringify(event.fingerList[0]);
      console.info('TapGesture'+this.isPlayingFlag)
      if (this.isPlayingFlag) {
        this.player?.pause();
        this.isPlayingFlag = false;
      }else {
        this.player?.play()
        this.isPlayingFlag = true;
      }
    }
  })

HarmonyOS码上奇行
6.7k 声望2.7k 粉丝