基于XComponent的视频播放器高性能体验

Heiang
发布于 2024-9-24 08:36
浏览
0收藏

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

方案

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; 
      } 
    } 
  })

分类
收藏
回复
举报
回复
    相关推荐