ios开发整理视频播放器

AVFoundation框架解析(十六)—— 一个简单示例之播放

2018-08-18  本文已影响16人  刀客传奇

版本记录

版本号 时间
V1.0 2018.08.18

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 视频音频的合成(一)
7. AVFoundation框架解析(七)—— 视频组合和音频混合调试
8. AVFoundation框架解析(八)—— 优化用户的播放体验
9. AVFoundation框架解析(九)—— AVFoundation的变化(一)
10. AVFoundation框架解析(十)—— AVFoundation的变化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的变化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的变化(四)
13. AVFoundation框架解析(十三)—— 构建基本播放应用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)

开始

这一篇我们就一起看一下播放、录制以及混合视频。

注意:本文写作环境Swift 4, iOS 11, Xcode 9。

录制视频,并以编程方式播放它们,这是您可以使用手机做的最酷的事情之一,但没有足够的应用程序使用它。 要做到这一点,需要AV Foundation框架,该框架自OS X Lion(10.7)以来一直是macOS的一部分,自2010年iOS 4以来一直是iOS的一部分。

AV Foundation已经有了很大的发展,目前有超过100个类。 本教程介绍了媒体播放和一些轻量级编辑,以帮助您开始使用AV Foundation。 特别是,您将学习:

本工程包括一个storyboard和几个VC,UI用来处理简单的视频播放以及录制。

主屏幕包含下面的三个按钮,它们与其他VC相对应:

Build并Run这个工程,只有初始场景上的三个按钮可以做点事情,但你很快就会改变它!


Select and Play Video - 选择并播放视频

主屏幕上的Select and Play Video按钮将seguesPlayVideoController。 在本教程的这一部分中,您将添加代码以选择视频文件并进行播放。

首先打开PlayVideoViewController.swift,然后在文件顶部添加以下import语句:

import AVKit
import MobileCoreServices

通过导入AVKit,您可以访问播放所选视频的AVPlayer对象。 MobileCoreServices包含预定义的常量,例如kUTTypeMovie,您在选择视频时需要这些常量。

接下来,向下滚动到文件末尾并添加以下类扩展。 确保将这些添加到文件的最底部,在类声明的花括号之外:

// MARK: - UIImagePickerControllerDelegate
extension PlayVideoViewController: UIImagePickerControllerDelegate {
}

// MARK: - UINavigationControllerDelegate
extension PlayVideoViewController: UINavigationControllerDelegate {
}

这些扩展设置PlayVideoViewController以采用UIImagePickerControllerDelegateUINavigationControllerDelegate协议。 您将使用系统提供的UIImagePickerController来允许用户浏览照片库中的视频,并且该类通过这些委托协议与您的应用程序进行通信。 虽然该类被命名为“image picker”,但请放心,它也适用于视频!

接下来,返回PlayVideoViewController的主类定义,并从VideoHelper添加对helper方法的调用以打开图像选择器。 稍后,您将在VideoHelper中添加自己的帮助工具。 将以下代码添加到playVideo(_ :)

VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)

在上面的代码中,您确保点击Play Video将打开UIImagePickerController,允许用户从媒体库中选择视频文件。

要查看此方法的内容,请打开VideoHelper.swift。它执行以下操作:

现在,Build项目并运行。点击第一个屏幕上的Select and Play Video,然后点击第二个屏幕上的Play Video,您应该看到您的视频显示类似于以下屏幕截图。

看到视频列表后,请选择一个。 您将进入另一个详细显示视频的屏幕,以及取消,播放和选择的按钮。 如果您点按播放按钮,视频将会播放。 但是,如果您点击选择按钮,应用程序只会返回播放视频屏幕! 这是因为您没有实现任何委托方法来处理从选择器中选择视频的问题。

回到Xcode,向下滚动到PlayVideoViewController.swift中的UIImagePickerControllerDelegate类扩展,并添加以下委托方法实现:

func imagePickerController(_ picker: UIImagePickerController, 
                           didFinishPickingMediaWithInfo info: [String : Any]) {
  // 1
  guard 
    let mediaType = info[UIImagePickerControllerMediaType] as? String,
    mediaType == (kUTTypeMovie as String),
    let url = info[UIImagePickerControllerMediaURL] as? URL
    else { 
      return 
  }
  
  // 2
  dismiss(animated: true) {
    //3
    let player = AVPlayer(url: url)
    let vcPlayer = AVPlayerViewController()
    vcPlayer.player = player
    self.present(vcPlayer, animated: true, completion: nil)
  }
}

以下是您在此方法中所做的事情:

Build并运行。 点击Select and Play Video,然后Play Video,然后从列表中选择一个视频。 您应该能够在媒体播放器中看到视频播放。


Record and Save Video - 录制并保存视频

现在您已经进行了视频播放,现在可以使用设备的相机录制视频并将其保存到媒体库。

打开RecordVideoViewController.swift,并添加以下导入:

import MobileCoreServices

您还需要遵守与PlayVideoViewController相同的协议,方法是在文件末尾添加以下内容:

extension RecordVideoViewController: UIImagePickerControllerDelegate {
}

extension RecordVideoViewController: UINavigationControllerDelegate {
}

将以下代码添加到record(_:)

VideoHelper.startMediaBrowser(delegate: self, sourceType: .camera)

它使用与PlayVideoViewController相同的helper方法,但它访问.camera而不是录制视频。

Build并运行以查看到目前为止您所拥有的内容。

转到Record屏幕,然后点击Record Video。 相机UI将打开,而不是照片库。 当alert对话框询问摄像机权限和麦克风权限时,单击OK。 通过点击屏幕底部的红色记录按钮开始录制视频,并在完成录制后再次点击它。

现在您可以选择使用录制的视频或重新录制。 点按Use Video。 您会注意到它只是取消了视图控制器。 那是因为 - 您猜对了 - 您还没有实现适当的委托方法将录制的视频保存到媒体库。

将以下方法添加到底部的UIImagePickerControllerDelegate类扩展中:

func imagePickerController(_ picker: UIImagePickerController, 
                           didFinishPickingMediaWithInfo info: [String : Any]) {
  dismiss(animated: true, completion: nil)
  
  guard 
    let mediaType = info[UIImagePickerControllerMediaType] as? String,
    mediaType == (kUTTypeMovie as String),
    let url = info[UIImagePickerControllerMediaURL] as? URL,
    UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(url.path)
    else {
      return
  }
  
  // Handle a movie capture
  UISaveVideoAtPathToSavedPhotosAlbum(
    url.path, 
    self, 
    #selector(video(_:didFinishSavingWithError:contextInfo:)), 
    nil)
}

不要担心最后一行代码的错误,你很快就会解决这个问题。

和以前一样,委托方法会为您提供指向视频的URL。 您确认应用程序可以将文件保存到设备的相册中,如果是,请保存。

UISaveVideoAtPathToSavedPhotosAlbum是SDK提供的用于将视频保存到相册的功能。 作为参数,您将路径传递给要保存的视频以及要回调的目标和操作,这将告知您保存操作的状态。

接下来将回调的实现添加到主类定义中:

@objc func video(_ videoPath: String, didFinishSavingWithError error: Error?, contextInfo info: AnyObject) {
  let title = (error == nil) ? "Success" : "Error"
  let message = (error == nil) ? "Video was saved" : "Video failed to save"
  
  let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.cancel, handler: nil))
  present(alert, animated: true, completion: nil)
}

回调方法只是向用户显示alert弹窗,根据错误状态通知是否保存了视频文件。

Build并运行。 录制视频,然后在录制完成后选择Use Video。 如果您被要求获得保存到视频库的权限,请点按OK。 如果弹出Video was saved弹窗,表明您已将视频成功保存到照片库!

现在您可以播放视频和录制视频,现在是时候采取下一步并尝试一些轻量级视频编辑。


Merging Videos - 混合视频

该应用程序的最后一项功能是进行一些编辑。您的用户将从音乐库中选择两个视频和一首歌曲,该应用程序将合并这两个视频并混合音乐。

该项目已在MergeVideoViewController.swift中有一个初始实现。这里的代码类似于您为播放视频而编写的代码。最大的区别在于合并时,用户需要选择两个视频。该部分已经设置好,因此用户可以进行两个选择,这些选择将存储在firstAssetsecondAsset中。

下一步是添加选择音频文件的功能。

UIImagePickerController仅提供从媒体库中选择视频和图像的功能。要从音乐库中选择音频文件,您将使用MPMediaPickerController。它的工作原理与UIImagePickerController基本相同,但它不是图像和视频,而是访问媒体库中的音频文件。

打开MergeVideoViewController.swift并将以下代码添加到loadAudio(_ :)

let mediaPickerController = MPMediaPickerController(mediaTypes: .any)
mediaPickerController.delegate = self
mediaPickerController.prompt = "Select Audio"
present(mediaPickerController, animated: true, completion: nil)

上面的代码创建了一个新的MPMediaPickerController实例,并将其显示为模态视图控制器。

Build并运行。 现在点击Merge Video,然后点击Load Audio以访问设备上的音频库。 当然,您需要在设备上使用一些音频文件。 否则,列表将为空。 歌曲也必须实际存在于设备上,因此请确保您没有尝试从云中加载歌曲。

如果您从列表中选择一首歌曲,您会发现没有任何反应。 那就对了! MPMediaPickerController需要委托方法! 在文件底部找到MPMediaPickerControllerDelegate类扩展,并添加以下两个方法:

func mediaPicker(_ mediaPicker: MPMediaPickerController, 
                 didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
  
  dismiss(animated: true) {
    let selectedSongs = mediaItemCollection.items
    guard let song = selectedSongs.first else { return }
    
    let url = song.value(forProperty: MPMediaItemPropertyAssetURL) as? URL
    self.audioAsset = (url == nil) ? nil : AVAsset(url: url!)
    let title = (url == nil) ? "Asset Not Available" : "Asset Loaded"
    let message = (url == nil) ? "Audio Not Loaded" : "Audio Loaded"
    
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler:nil))
    self.present(alert, animated: true, completion: nil)
  }
}

func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
  dismiss(animated: true, completion: nil)
}

该代码与UIImagePickerController的委托方法非常相似。 在确保它是有效的媒体项目后,您可以根据通过MPMediaPickerController选择的媒体项目设置音频资源。 请注意,在dismiss当前控制器之后仅显示新的视图控制器很重要,这就是为什么将上面的代码包装在完成处理程序中的原因。

Build并运行。 转到Merge Videos屏幕。 选择一个音频文件,如果没有错误,您应该看到“Audio Loaded”消息。

您现在可以正确加载所有资源。 是时候将各种媒体文件合并到一个文件中了。 但是在你进入代码之前,你必须做一些设置。

1. Export and Merge - 导出和合并

合并资源的代码需要完成处理程序才能将最终视频导出到相册。

将以下代码添加到MergeVideoViewController

func exportDidFinish(_ session: AVAssetExportSession) {
  
  // Cleanup assets
  activityMonitor.stopAnimating()
  firstAsset = nil
  secondAsset = nil
  audioAsset = nil
  
  guard 
    session.status == AVAssetExportSessionStatus.completed,
    let outputURL = session.outputURL 
    else {
      return
  }
  
  let saveVideoToPhotos = {
    PHPhotoLibrary.shared().performChanges({ 
      PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
    }) { saved, error in
      let success = saved && (error == nil)
      let title = success ? "Success" : "Error"
      let message = success ? "Video saved" : "Failed to save video"
      
      let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.cancel, handler: nil))
      self.present(alert, animated: true, completion: nil)
    }
  }
  
  // Ensure permission to access Photo Library
  if PHPhotoLibrary.authorizationStatus() != .authorized {
    PHPhotoLibrary.requestAuthorization { status in
      if status == .authorized {
        saveVideoToPhotos()
      }
    }
  } else {
    saveVideoToPhotos()
  }
}

导出成功完成后,上面的代码会将新导出的视频保存到相册中。 您只需在AssetBrowser中显示输出视频,但将输出视频复制到相册会更容易,这样您就可以看到最终输出。

现在,添加以下代码到merge(_:)

guard 
  let firstAsset = firstAsset, 
  let secondAsset = secondAsset 
  else {
    return
}

activityMonitor.startAnimating()

// 1 - Create AVMutableComposition object. This object will hold your AVMutableCompositionTrack instances.
let mixComposition = AVMutableComposition()

// 2 - Create two video tracks
guard 
  let firstTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, 
                                                  preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) 
  else {
    return
}
do {
  try firstTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, firstAsset.duration), 
                                 of: firstAsset.tracks(withMediaType: AVMediaType.video)[0], 
                                 at: kCMTimeZero)
} catch {
  print("Failed to load first track")
  return
}

guard 
  let secondTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, 
                                                   preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
  else {
    return
}
do {
  try secondTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, secondAsset.duration), 
                                  of: secondAsset.tracks(withMediaType: AVMediaType.video)[0], 
                                  at: firstAsset.duration)
} catch {
  print("Failed to load second track")
  return
}

// 3 - Audio track
if let loadedAudioAsset = audioAsset {
  let audioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: 0)
  do {
    try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, 
                                                    CMTimeAdd(firstAsset.duration, 
                                                              secondAsset.duration)),
                                    of: loadedAudioAsset.tracks(withMediaType: AVMediaType.audio)[0] ,
                                    at: kCMTimeZero)
  } catch {
    print("Failed to load Audio track")
  }
}

// 4 - Get path
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, 
                                                       in: .userDomainMask).first else {
  return
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: Date())
let url = documentDirectory.appendingPathComponent("mergeVideo-\(date).mov")

// 5 - Create Exporter
guard let exporter = AVAssetExportSession(asset: mixComposition, 
                                          presetName: AVAssetExportPresetHighestQuality) else {
  return
}
exporter.outputURL = url
exporter.outputFileType = AVFileType.mov
exporter.shouldOptimizeForNetworkUse = true

// 6 - Perform the Export
exporter.exportAsynchronously() {
  DispatchQueue.main.async {
    self.exportDidFinish(exporter)
  }
}

以下是上述代码的逐步细分:

请注意,insertTimeRange(_:ofTrack:atStartTime :)允许您将视频的一部分插入主要合成而不是整个视频。这样,您可以将视频剪裁到您选择的时间范围。

在这种情况下,您要插入整个视频,以便创建从kCMTimeZero到视频资源持续时间的时间范围。 atStartTime参数允许您将视频/音频轨道放置在合成中的任何位置。注意代码如何在零时插入firstAsset,并在第一个视频的末尾插入secondAsset。本教程假设您希望视频资源一个接一个。但您也可以通过播放时间范围来重叠资源。

要处理时间范围,请使用CMTime结构。 CMTime结构是表示时间的非不透明可变结构,其中时间可以是时间戳或持续时间。

AVComposition实例组合来自多个基于文件的源的媒体数据。 在顶层,AVComposition是一组轨道,每个轨道都呈现特定类型的媒体,如音频或视频。 AVCompositionTrack的实例表示单个轨道。

类似地,AVMutableCompositionAVMutableCompositionTrack也提供了用于构造合成的更高级别的接口。 这些对象提供了您已经看过的插入,移除和缩放操作。

继续,Build并运行您的项目!

选择两个视频和一个音频文件并合并所选文件。 如果合并成功,您应该看到AVComposition实例组合来自多个基于文件的源的媒体数据。 在顶层,AVComposition是一组轨道,每个轨道都呈现特定类型的媒体,如音频或视频。 AVCompositionTrack的实例表示单个轨道。

类似地,AVMutableComposition和AVMutableCompositionTrack也提供了用于构造组合的更高级别的接口。 这些对象提供了您已经看过的插入,移除和缩放操作,并且会再次出现。

继续,构建并运行您的项目!

选择两个视频和一个音频文件并合并所选文件。 如果合并成功,您应该看到“视频已保存”消息。 此时,您的新视频应该出现在相册中。消息。 此时,您的新视频应该出现在相册中。

转到相册,或使用应用程序中的Select and Play Video屏幕进行浏览。 您可能会注意到,虽然应用程序合并了视频,但仍存在一些方向问题。 纵向视频处于横向模式,有时视频会上下颠倒。

这是由于默认的AVAsset方向。 使用默认iPhone相机应用程序录制的所有电影和图像文件都将视频帧设置为横向,因此iPhone会以横向模式保存媒体。

2. Video Orientation - 视频方向

AVAsset具有包含媒体方向信息的preferredTransform属性,只要您使用Photos应用程序或QuickTime查看媒体文件,它就会将其应用于媒体文件。 在上面的代码中,您尚未将变换应用于AVAsset对象,因此导致问题。

您可以通过将必要的变换应用于AVAsset对象来轻松纠正此问题。 但是,由于您的两个视频文件可能具有不同的方向,因此您需要使用两个单独的AVMutableCompositionTrack实例,而不是您最初使用的实例。

在执行此操作之前,请将以下helper方法添加到VideoHelper

static func orientationFromTransform(_ transform: CGAffineTransform) 
  -> (orientation: UIImageOrientation, isPortrait: Bool) {
  var assetOrientation = UIImageOrientation.up
  var isPortrait = false
  if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
    assetOrientation = .right
    isPortrait = true
  } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
    assetOrientation = .left
    isPortrait = true
  } else if transform.a == 1.0 && transform.b == 0 && transform.c == 0 && transform.d == 1.0 {
    assetOrientation = .up
  } else if transform.a == -1.0 && transform.b == 0 && transform.c == 0 && transform.d == -1.0 {
    assetOrientation = .down
  }
  return (assetOrientation, isPortrait)
}

此代码分析仿射变换affine transform以确定输入视频的方向。

接下来,向该类添加一个helper方法:

static func videoCompositionInstruction(_ track: AVCompositionTrack, asset: AVAsset) 
  -> AVMutableVideoCompositionLayerInstruction {
  let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
  let assetTrack = asset.tracks(withMediaType: .video)[0]
  
  let transform = assetTrack.preferredTransform
  let assetInfo = orientationFromTransform(transform)
  
  var scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.width
  if assetInfo.isPortrait {
    scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.height
    let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
    instruction.setTransform(assetTrack.preferredTransform.concatenating(scaleFactor), at: kCMTimeZero)
  } else {
    let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
    var concat = assetTrack.preferredTransform.concatenating(scaleFactor)
      .concatenating(CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.width / 2))
    if assetInfo.orientation == .down {
      let fixUpsideDown = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
      let windowBounds = UIScreen.main.bounds
      let yFix = assetTrack.naturalSize.height + windowBounds.height
      let centerFix = CGAffineTransform(translationX: assetTrack.naturalSize.width, y: yFix)
      concat = fixUpsideDown.concatenating(centerFix).concatenating(scaleFactor)
    }
    instruction.setTransform(concat, at: kCMTimeZero)
  }
  
  return instruction
}

此方法获取轨道和资源,并返回AVMutableVideoCompositionLayerInstruction,其封装了使视频正面朝上所需的仿射变换。这是正在做的事情,一步一步看一下:

设置好helper方法后,找到merge(_ :)并在#2和#3部分之间插入以下内容:

// 2.1
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, 
                                            CMTimeAdd(firstAsset.duration, secondAsset.duration))

// 2.2
let firstInstruction = VideoHelper.videoCompositionInstruction(firstTrack, asset: firstAsset)
firstInstruction.setOpacity(0.0, at: firstAsset.duration)
let secondInstruction = VideoHelper.videoCompositionInstruction(secondTrack, asset: secondAsset)

// 2.3
mainInstruction.layerInstructions = [firstInstruction, secondInstruction]
let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(1, 30)
mainComposition.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

首先,设置两个单独的AVMutableCompositionTrack实例。这意味着您需要将AVMutableVideoCompositionLayerInstruction应用于每个轨道,以便分别固定方向。

2.1:首先,设置mainInstruction来包装整套instructions。请注意,此处的总时间是第一个资源的持续时间和第二个资源的持续时间之和。

2.2:接下来,使用您之前定义的helper方法设置两条指令 - 每条资源一条。第一个视频的指令需要额外添加一个:在最后将其不透明度设置为0,以便在第二个视频启动时变为不可见。

2.3:现在您已经拥有了第一个和第二个轨道的AVMutableVideoCompositionLayerInstruction实例,只需将它们添加到主AVMutableVideoCompositionInstruction对象即可。接下来,将mainInstruction对象添加到AVMutableVideoComposition实例的指令属性中。您还可以将合成的帧速率设置为30帧/秒。

现在您已经配置了AVMutableVideoComposition对象,您只需将其分配给导出器即可。在第5节末尾插入以下代码(就在exportAsynchronously():之前。

exporter.videoComposition = mainComposition

Build并运行您的项目。 如果您通过组合两个视频(以及可选的音频文件)来创建新视频,则会在播放新的合并视频时看到方向问题消失。

如果一直细心的看到这里,您现在应该很好地了解如何在您的应用中播放视频,录制视频以及合并多个视频和音频。

AV Foundation在播放视频时为您提供了很大的灵活性。 您还可以应用任何类型的CGAffineTransform来合并,缩放或定位视频。

如果您还不会或者不了解,我建议您查看AV Foundation上WWDC videos,例如AV Foundation播放中的WWDC 2016 session 503 Advanced。 另外,请务必查看 Apple AV Foundation Framework documentation

题外话:今天是我的生日,祝自己生日快乐吧!希望以后的日子会像数字880818一样幸运和吉祥,谢谢大家一如既往的支持和鼓励~~~

后记

本篇主要讲述了播放、录制以及混合视频,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读