大师学SwiftUI第18章Part3 - 自定义视频播放器

视频

录制和播放视频对用户来说和拍照、显示图片一样重要。和图片一样,Apple框架中内置了播放视频和创建自定义播放器的工具。

视频播放器

SwiftUI定义了VideoPlayer视图用于播放视频。该视图提供了所有用于播放、停止、前进和后退的控件。视图包含如下初始化方法。

  • VideoPlayer (player : AVPlayer?, videoOverlay : Closure):该初始化方法创建视频播放器来播放通过参数提供的视频。player参数是负责播放的对象,videoOverlay参数提供希望展示在视频上方的视图。

VideoPlayer视图展示用户控制视频的界面,但视频由AVPlayer类的对象播放。该类包含如下初始化方法。

  • AVPlayer (url : URL):该初始化方法创建一个AVPlayer对象来播放url参数所指向URL的媒体。

AVPlayer类还提供通过程序控制视频的属性和方法。

  • volume :该属性设置或返回决定播放器音量的值。值为0.0到1.0之间的Float类型值。
  • isMuted:该属性是一个布尔值,决定播放器的音频是否为静音。
  • rate :该属性设置或返回一个Float值,决定所播放媒体的速度。0.0表示暂停视频,1.0设为常速。
  • play():该方法开启播放。
  • pause():该方法暂停播放。
  • addPeriodicTimeObserver (forInterval : CMTime, queue : DispatchQueue?, using : Closure):该方法添加一个观察者,每隔一定的时间执行闭包的内容。forInterval参数决定执行的间隔,queue参数为闭包所处的队列(推荐用主线程),using参数是希望执行闭包。闭包接收CMTime类型的值,为闭包调用的时间。

VideoPlayer视图需要有AVPlayer对象来播放视频,该对象对过URL加载视频。如果希望播放线上的视频,只需要URL,但如果视频由应用提供,则需要通过包来获取(参见第10章中的Bundle)。在以下的模型中,我们在项目中添加了一个视频videotrees.mp4,通过Bundle对象获取指向该文件的URL,并用该值创建一个AVPlayer对象。

示例18-19:准备待播放的视频

swift 复制代码
import SwiftUI
import Observation
import AVKit

@Observable class ApplicationData {
    var player: AVPlayer!
    
    init() {
        let bundle = Bundle.main
        if let videoURL = bundle.url(forResource: "videotrees", withExtension: "mp4") {
            player = AVPlayer(url: videoURL)
        }
    }
}

VideoPlayer视图和AVPlayer类来自AVKit框架。导入该框架后,我们获取到videotrees.mp4视频的URL,创建AVPlayer对象并将其存储到可观测属性中,以供视图使用。在视图中,我们需要检测该属性并在视频准备就绪后显示VideoPlayer视图。

示例18-20:播放视频

swift 复制代码
import SwiftUI
import AVKit

struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        if appData.player != nil {
            VideoPlayer(player: appData.player)
                .ignoresSafeArea()
        } else {
            Text("Video not available")
        }
    }
}

图18-9:标准视频播放器

✍️跟我一起做:创建一个多平台项目。下载videotrees.mp4并添加至项目中(别忘了在弹窗中选择target)。使用示例18-19 中的代码创建一个Swift模型文件ApplicationData.swift。使用示例18-20 中的代码更新ContentView视图。还要将ApplicationData对象注入应用和预览的环境中(参见第7章示例7-4)。运行应用。点击播放按钮播放视频。

上例中,视频需要由用户点击播放按钮才开始播放。但我们可以实现AVPlayer属性和方法来通过程序控制视频。例如,以下示例在视图加载完后就开始播放视频。

示例18-21:自动播放视频

swift 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        if appData.player != nil {
            VideoPlayer(player: appData.player)
                .onAppear {
                    appData.player.play()
                }
                .ignoresSafeArea()
        } else {
            Text("Video not available")
        }
    }
}

VideoPlayer视图初始化方法还可以包含一个参数,接收闭包来在视频上添加浮层。下例中,实现的初始化方法在视频的顶部添加标题。

示例18-22:在视频上展示视图

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        if appData.player != nil {
            VideoPlayer(player: appData.player, videoOverlay: {
                VStack {
                    Text("Title: Trees at the park")
                        .font(.title)
                        .padding([.top, .bottom], 8)
                        .padding([.leading, .trailing], 16)
                        .foregroundColor(.black)
                        .background(.ultraThinMaterial)
                        .cornerRadius(10)
                        .padding(.top, 8)
                    Spacer()
                }
            })
                .ignoresSafeArea()
        } else {
            Text("Video not available")
        }
    }
}

闭包返回中的视频位于视频之上和控件之下,因此无法接收用户的输入,但可以使用它来提供额外的信息,就像本例中这样。结果如下所示。

图18-10:浮层视图

自定义视频播放器

除了让VideoPlayer视图正常工作的代码外,AVFoundation框架还提供了创建播放媒体独立组件的功能。有一个负责资源(视频或音频)的类,一个负责将媒体资源发送给播放器的类,一个播放媒体的类以及一个负责在屏幕上显示媒体的类。图18-11描述了这一结构。

图18-11:播放媒体的系统

待播放的媒体以资源形式提供。资源以一个或多个媒体轨道组成,包括视频、音频和字幕等。AVFoundation框架定义了一个AVAsset类来加载资源。该类包含如下初始化方法。

  • AVURLAsset (url : URL):这个初始化方法使用url参数指定位置的资源创建AVURLAsset对象。参数是一个URL结构体,包含本地或远程资源的位置。

资源包含有静态信息,在播放后无法管理自身的状态。框架定义了AVPlayerItem类来控制资源。通过此类我们可以引用资源并管理其时间轴。该类中包含多个初始化方法。以下是最常用的一个。

  • AVPlayerItem (asset : AVAsset):本初始化方法创建一个表示asset参数所指定的资源的AVPlayerItem对象。

AVPlayerItem类还包含一些控制资源状态的属性和方法。以下是最常用的那些。

  • status :该属性返回表示播放项状态的值。这是一个位于AVPlayerItem类中Status枚举。值有unknownreadyToPlayfailed
  • duration :该属性返回表示播放项时长的值。它是一个CMTime类型的结构体。
  • currentTime ():此方法返回播放项当前时间的CMTime值。
  • seek (to : CMTime):这一异步方法将播放游标移动到to参数所指定的时间,返回一个寻址操作是否完成的布尔值。

AVPlayerItem对象管理播放所需的信息,但不会播放媒体,这是由AVPlayer类的实例来处理的。它是稍早我们在VideoPlayer视图中用于加载视频相同的类。该类包含如下通过AVPlayerItem对象创建播放器的初始化方法。

  • AVPlayer (playerItem : AVPlayerItem?):这一初始化方法创建一个AVPlayer对象播放playerItem参数所表示的媒体资源。

系统所需的最后一个对象负责展示媒体资源。它是CALayer的子类AVPlayerLayer,提供了在屏幕上绘制视频帧所需要的代码。该类包含如下创建和配置播放层的初始化方法和属性。

  • AVPlayerLayer (player : AVPlayer):本初始化方法创建一个AVPlayerLayer对象,关联player参数所指定的播放器。
  • videoGravity :此属性定义了如何将视频调整为预览层的大小。它是一个AVLayerVideoGravity结构体,包含类型属性resizeresizeAspectresizeAspectFill

这些类一起定义了用于播放媒体的系统,但还要有方法来控制时间。因浮点数的精度不适合于播放媒体资源,框架还通过旧框架的Core Media实现了CMTime结构体。这一结构体包含了很多以分数表示时间的值。最重要的两个是valuetimescale,分别表示分子和分母。例如,想要创建表示0.5秒的CMTime结构体时,可以指定分子为1、分母为2(1除以2得0.5)。该类包含一些创建这些值的初始化方法和类型属性。以下是最常使用的。

  • CMTime (value : CMTimeValue, timescale : CMTimeScale):此初始化方法通过valuetimescale所指定的值创建一个CMTime结构体。参数分别为整型Int64Int32
  • CMTime (seconds : Double, preferredTimescale : CMTimeScale):此初始化方法通过表示秒数的浮点值创建一个CMTime结构体。seconds参数为赋给结构体的秒数,preferredTimescale参数为希望使用的单位。值为1时保持为第一个参数的秒数。
  • zero :该类型属性返回值为0的CMTime结构体。

CMTime结构体还包含一些设置和获取值的属性。最常用的如下。

  • seconds :该属性以秒数返回CMTime结构体的时间。类型为Double
  • value :该属性返回CMTime结构体的值。
  • timescale :该属性返回CMTime结构体的时间单位。

要自定义视频播放器,我们必须加载资源(AVURLAsset),创建一个管理资源的子项(AVPlayerItem),将子项添加至播放器(AVPlayer),将播放器关联至屏幕上媒体的显示层(AVPlayerLayer)。

就像前面用于显示来自相机的视频的预览层,我们需要将UIView对象提供的显示层转化为预览层(本例中CALayer需要转换为AVPlayerLayer对象)。以下是本例需要实现的表现视图。

示例18-23:构建自定义播放器

swift 复制代码
import SwiftUI
import AVFoundation

class CustomPlayerView: UIView {
    override class var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
}

struct PlayerView: UIViewRepresentable {
    var view = CustomPlayerView()
    
    func makeUIView(context: Context) -> UIView {
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

有了表现视图,下一步就是构建视频播放器,然后在就绪后调用player()方法播放视频。

示例18-24:构建自定义视频播放器

swift 复制代码
import SwiftUI
import Observation
import AVFoundation

class ViewData: NSObject {
    var playerItem: AVPlayerItem?
    var player: AVPlayer?
    var playerLayer: AVPlayerLayer?
    var playerObservation: NSKeyValueObservation?
    
    func setObserver() {
        playerObservation = playerItem?.observe(.status, options: .new, changeHandler: { item, value in
            if item.status == .readyToPlay {
                self.player?.play()
            }
        })
    }
}

@Observable class ApplicationData {
    @ObservationIgnored var customVideoView: PlayerView!
    @ObservationIgnored var viewData: ViewData
    
    init() {
        customVideoView = PlayerView()
        viewData = ViewData()
        
        let bundle = Bundle.main
        let videoURL = bundle.url(forResource: "videotrees", withExtension: "mp4")
        let asset = AVURLAsset(url: videoURL!)
        viewData.playerItem = AVPlayerItem(asset: asset)
        viewData.player = AVPlayer(playerItem: viewData.playerItem)
        
        viewData.playerLayer = customVideoView.view.layer as? AVPlayerLayer
        viewData.playerLayer?.player = viewData.player
        viewData.setObserver()
    }
}

视频并非立马可见,需要进行加载和做好播放准备,因而不能马上播放,需等待其就绪。媒体的状态由AVPlayerItemstatus属性进行上报。因此需要监测该属性的值,公在其值等于readyToPlay时开始播放。这就要使用到观察过生日和。因此,在定义三个属性后,我们需要存储播放项、播放器和播放层,我们定义了一个存储观察者的属性,调用AVPlayerItem对象的observer()方法来跟踪status属性。在当前状态为readyToPlay时播放视频。

为配置视频播放器,我们从bundle中加载视频、创建播放器结构体、将UIView层转换为AVPlayerLayer,将其赋值给player。因所有内容都在模型中进行了准备,界面只需要在展示视图中进行显示。视频填满屏幕、适配屏幕的朝向并在加载视图后进行播放。

示例18-25:显示视频

swift 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        appData.customVideoView
            .ignoresSafeArea()
    }
}

✍️跟我一起做:创建一个多平台项目。下载videotrees.mp4 ,添加至项目中(记住要勾选Add to Target 选项)。使用示例18-23 中的代码创建CustomPlayerView.swift文件,使用示例18-24中的创建模型文件ApplicationData.swift。再用示例18-25中的代码更新ContentView视图。运行应用,视频应该会在应用启动后立即播放。

上例中进行了视频的播放,但没为用户提供任何控件工具。AVPlayer类包含有播放、暂停和检查媒体状态的方法,但需要我们来创建界面。下例中我们会创建一个带有按钮和进度条的界面,这样用户可以播放、暂停并查看视频的进度。

图18-12:自定义视频播放器的控件

如何控制流程以及对界面进行响应取决于应用的要求。例如,我们决定定义两个状态,一个表示视频是否在播放,另一个表示进度条的位置。以下是对模型所做的修改,让用户可以播放、暂停视频以及拖动进度条。

示例18-26:准备视频播放器

swift 复制代码
import SwiftUI
import Observation
import AVFoundation

class ViewData: NSObject {
    var playerItem: AVPlayerItem?
    var player: AVPlayer?
    var playerLayer: AVPlayerLayer?
}

@Observable class ApplicationData {
    var playing: Bool = false
    var progress: CGFloat = 0
    @ObservationIgnored var customVideoView: PlayerView!
    @ObservationIgnored var viewData: ViewData
    
    init() {
        customVideoView = PlayerView()
        viewData = ViewData()
        
        let bundle = Bundle.main
        let videoURL = bundle.url(forResource: "videotrees", withExtension: "mp4")
        let asset = AVURLAsset(url: videoURL!)
        viewData.playerItem = AVPlayerItem(asset: asset)
        viewData.player = AVPlayer(playerItem: viewData.playerItem)
        
        viewData.playerLayer = customVideoView.view.layer as? AVPlayerLayer
        viewData.playerLayer?.player = viewData.player
       
        let interval = CMTime(value: 1, timescale: 2)
        viewData.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { time in
            if let duration = self.viewData.playerItem?.duration {
                let position = time.seconds / duration.seconds
                self.progress = CGFloat(position)
            }
        })
    }
    func playVideo() {
        if viewData.playerItem?.status == .readyToPlay {
            if playing {
                viewData.player?.pause()
                playing = false
            } else {
                viewData.player?.play()
                playing = true
            }
        }
    }
}

本例中,我们添加了一个playVideo()方法,在用户点击Play按钮时执行。该方法检测是否可以播放媒体,然后根据playing属性的值执行操作。如果视频在播放就暂停,如果在暂停就播放。playing属性的值会进行更新来反映新的状态。

要计算进度条的长度,必须要实现一个观察者。但不是像之前所实现的KVO观察者。常规的观察者不够快,所以AVFoundation框架自带了一个addPeriodicTimeObserver()方法创建提供更精准响应的观察者。该方法需要一个CMTime值来指定执行任务的频率、一个主队列指针以及一个带每次触发观察者执行代码的闭包。本例中,我们创建一个表示0.5秒时长的CMTime值,然后使用它调用addPeriodicTimeObserver()方法来注册观察者。之后,传递给观察者的闭包在播放期间每0.5秒执行一次。在闭包中,我们获取到了当前时间以及视频时长秒数,通过将秒数转换成0.0到1.0之间的值来计算进度,稍后可转化成点数在屏幕上显示进度条。

注意:addPeriodicTimeObserver()方法无法用于Swift的并发。而是需要将线程定义在DispatchQueue对象中。这是由Dispatch框架定义的老类,用于创建异步任务。该类包含一个类型属性main,定义一个主队列任务(Main Actor),这正是确保赋给这一方法的闭包在主线程中运行的方式。

播放器已就绪,是时修改定义界面了。在这个场景中,我们需要在ZStack中展示表现视图,这样可以在上层显示工具栏(参见图18-12)。

示例18-27:播放及暂停视频

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        ZStack {
            appData.customVideoView
                .ignoresSafeArea()
            VStack {
                Spacer()
                HStack {
                    Button(appData.playing ? "Pause" : "Play") {
                        appData.playVideo()
                    }.frame(width: 70)
                        .foregroundColor(.black)
                    GeometryReader { geometry in
                        HStack {
                            Rectangle()
                                .fill(Color(red: 0, green: 0.4, blue: 0.8, opacity: 0.8))
                                .frame(width: geometry.size.width * appData.progress, height: 20)
                            Spacer()
                        }
                    }
                    .padding(.top, 15)
                }
                .padding([.leading, .trailing])
                .frame(height: 50)
                .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 0.8))
            }
        }
    }
}

工具栏包含一个按钮和一个表示进度条的Rectangle视图。按钮的标签取决于playing属性的值。在视频播放时显示Pause ,暂停时显示Play 。为计算表示进度条的Rectangle视图的长度,我们嵌入了GeometryReader视图,然后用其宽度乘上progress属性。因该属性包含一个0.0到1.0之间的值,这一运算返回一个设置进度条宽度的值,在屏幕上显示出进度。

✍️跟我一起做:使用示例18-26 中的代码更新模型,用示例18-27 中的代码更新ContentView视图。运行程序,就会看到图18-12中所示的视频播放器。

通过addPeriodicTimeObserver()方法添加的观察者不是获取播放器的信息的唯一方式。AVPlayerItem类还定义了一些通知用于报告媒体播放期间发生的事件。例如,我们可以监听AVPlayerItemDidPlayToEndTime通知来了解视频何时停止播放。为此,我们需要在模型中定义一个方法监听并响应该通知,并添加一个任务在展示视图创建时调用该方法。以下是我们需要在ApplicationData类的初始化方法添加的任务。

示例18-28:执行异步方法监测视频结束

scss 复制代码
        Task(priority: .background) {
            await rewindVideo()
        }

rewindVideo()方法中,我们必须监听AVPlayerItemDidPlayToEndTime通知,并准备再次播放视频。为此,AVPlayerItem类提供了seek()方法。该方法将播放进度移到参数所指定的时间,并在处理完成后执行一个闭包。本例中我们将使用值为0的CMTime将播放器移到视频开头,然后重置playingprogress属性允许用户重新播放视频。

示例18-29:重新播放视频

swift 复制代码
    func rewindVideo() async {
        let center = NotificationCenter.default
        let name = NSNotification.Name.AVPlayerItemDidPlayToEndTime
        for await _ in center.notifications(named: name, object: nil) {
            if let finished = await viewData.playerItem?.seek(to: CMTime.zero), finished {
                await MainActor.run {
                    playing = false
                    progress = 0
                }
            }
        }
    }

✍️跟我一起做:将示例18-28 中的任务添加到ApplicationData初始化方法的最后。将示例18-29 中的方法添加到ApplicationData类的最后。运行程序。点击播放,等待视频结束。播放器应该会重置,可以再次播放视频。

如果希望按顺序播放多个视频,我们可以使用AVPlayerItemDidPlayToEndTime通知将新资源赋值给AVPlayer对象,但框架提供了AVPlayer类的子类AVQueuePlayer,专门上用于管理视频列表。该类通过AVPlayerItem对象数组创建一个播放列表。以下为初始化方法和其中的一些方法。

  • AVQueuePlayer (items : [AVPlayerItem]):该方法通过items参数指定的播放项创建一个播放列表。
  • advanceToNextItem():该方法将播放内容递进至列表中的下一项。
  • insert (AVPlayerItem, after: AVPlayerItem?):该方法在列表中插入一个新项。
  • remove(AVPlayerItem):该方法从列表中删除一项。

AVQueuePlayer对象替换用于展示媒体资源的AVPlayer对象。播放视频序列我闪只需要每个视频创建一个AVPlayerItem对象,以及将我们一直使用的AVPlayer对象替换为AVQueuePlayer对象,如下例所示。

示例18-30:播放视频列表

swift 复制代码
import SwiftUI
import Observation
import AVFoundation

class ViewData: NSObject {
    var playerItem1: AVPlayerItem!
    var playerItem2: AVPlayerItem!
    var player: AVQueuePlayer!
    var playerLayer: AVPlayerLayer?
    var playerObservation: NSKeyValueObservation?
    
    func setObserver() {
        playerObservation = playerItem1.observe(.status, options: .new, changeHandler: { item, value in
            if item.status == .readyToPlay {
                self.player.play()
            }
        })
    }
}

@Observable class ApplicationData {
    var playing: Bool = false
    var progress: CGFloat = 0
    @ObservationIgnored var customVideoView: PlayerView!
    @ObservationIgnored var viewData: ViewData
    
    init() {
        customVideoView = PlayerView()
        viewData = ViewData()
        
        let bundle = Bundle.main
        let videoURL1 = bundle.url(forResource: "videotrees", withExtension: "mp4")
        let videoURL2 = bundle.url(forResource: "videobeaches", withExtension: "mp4")
        
        let asset1 = AVURLAsset(url: videoURL1!)
        let asset2 = AVURLAsset(url: videoURL2!)
        viewData.playerItem1 = AVPlayerItem(asset: asset1)
        viewData.playerItem2 = AVPlayerItem(asset: asset2)
        viewData.player = AVQueuePlayer(items: [viewData.playerItem1, viewData.playerItem2])
        
        viewData.playerLayer = customVideoView.view.layer as? AVPlayerLayer
        viewData.playerLayer?.player = viewData.player
       
        viewData.setObserver()
    }
}

本例中,我们使用的是示例18-25 中的ContentView视图。代码中加载了两个视频,videotrees.mp4videobeaches.mp4 ,然后创建两个AVURLAsset对象以及两个用于展示它们的AVPlayerItem对象。接着定义AVQueuePlayer对象来按顺序播放这两个视频。注意因为在本例中我们使用的界面没带播放按钮,我们对第一个视频添加了一个观察者,在准备就绪后调用play()方法。

✍️跟我一起做:使用示例18-30 中的代码更新ApplicationData.swift文件。结合示例18-25 中的ContentView视图使用。下载videotrees.mp4videobeaches.mp4 视频添加至项目中(记得勾选Add to Target)。运行应用,视频应该会逐一播放。

颜色拾取器

SwiftUI自带了ColorPicker视图来允许用户选取颜色。该视图创建一个按钮,打开预定义界面,自带有选取和配置颜色的工具。以下是该视图的初始化方法。

  • ColorPicker (String, selection : Binding, supportsOpacity : Bool):本初始化方法创建一个颜色拾取器。第一个参数为显示在按钮旁的标签,selection参数是一个绑定属性,存储用户所选颜色的Color视图,supportsOpacity参数指定是否允许用户设置透明度。默认值为true

颜色拾取器的实现非常简单。我们用Color视图定义一个@State属性,然后使用它初始化ColorPicker视图,这样每次用户选择颜色时,就会存储到该属性中,我们可以使用它来修改其它视图。在下例中,我们使用该属性的值来修改界面的背景色。

示例18-31:显示颜色拾取器

scss 复制代码
struct ContentView: View {
    @State private var selectedColor: Color = .white
    
    var body: some View {
        VStack {
            ColorPicker("Select a Color", selection: $selectedColor)
                .padding()
            Spacer()
        }.background(selectedColor)
    }
}

ColorPicker视图展示一个按键,打开用户选择颜色的界面。用户选取颜色后,颜色会自动赋给@State属性。这意味着用户可以按意愿多次修改选择,但只有最后一次所选的颜色保存到该属性中。

图18-13:颜色拾取器

✍️跟我一起做:创建一个多平台项目。使用示例18-31 中的代码更新ContentView视图。运行应用、点击颜色拾取器按钮。选择颜色,会看到界面颜色的变化,如图18-13所示。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》

相关推荐
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频