【SwiftUI】VideoPlayerに再生速度変更機能を追加してみた

2022.08.19

ビデオプレイヤーを簡単に実装できるAVKitとUIKitのAVPlayerViewControllerやSwiftUIのVideoPlayerですが、スロー再生機能をつけたいなと思ったのですが既存の機能には無さそうだったので再生速度を変更する機能を追加してみました。

環境

  • Xcode 13.3
  • iOS 15.5

はじめに

今回はSwiftUIのVideoPlayerに再生速度を変更するメニューボタンを付ける簡単なものになります。Player自体をフルカスタムして作成するようなものではありません。

作ったもの

VideoPlayer画面 再生速度変更メニュー表示

実装

PlaySpeed

まずは再生速度のrateと表示ラベル用にenumPlaySpeedを作成します。

今回は、0.25~1.75倍速の幅で用意しました。

enum PlaySpeed: String, CaseIterable, Identifiable {
    case x0_25 = "0.25倍速"
    case x0_5 = "0.5倍速"
    case x0_75 = "0.75倍速"
    case x1 = "標準"
    case x1_25 = "1.25倍速"
    case x1_5 = "1.5倍速"
    case x1_75 = "1.75倍速"

    var id: String { rawValue }

    var rate: Float {
        switch self {
        case .x0_25:
            return 0.25
        case .x0_5:
            return 0.5
        case .x0_75:
            return 0.75
        case .x1:
            return 1
        case .x1_25:
            return 1.25
        case .x1_5:
            return 1.5
        case .x1_75:
            return 1.75
        }
    }

    var displayTitle: String {
        return "再生速度・" + self.rawValue
    }
}

AVPlayerの再生速度を変更するにはrateに変更したい値を入力する必要がある為、それぞれの倍数にあったrateを返しています。

ViewModel

SpeedAdjustableVideoPlayerViewModelというObservableObjectのクラスを作成しました。

import AVKit
import Combine

class SpeedAdjustableVideoPlayerViewModel: ObservableObject {

    @Published var player: AVPlayer
    @Published var playSpeed = PlaySpeed.x1
    @Published var displaySpeedTitle = PlaySpeed.x1.displayTitle
    private var lastPlaySpeed = PlaySpeed.x1

    private var cancellable: AnyCancellable?

    init(player: AVPlayer) {
        self.player = player
        observeRate()
    }

    func observeRate() {
        cancellable = player.publisher(for: \.rate).sink(receiveValue: { [weak self] rate in
            guard let self = self else { return }
            if rate == 1.0,
               self.lastPlaySpeed != self.playSpeed {
                // 停止ボタンを押して、再生を押すと自動的にrateが1.0に戻ってしまい
                // playSpeedもx1に戻ってしまう為、
                // 最後に設定していたSpeedにrateを変更する
                self.changeRate(into: self.lastPlaySpeed)
            }
        })
    }

    func changeRate(into speed: PlaySpeed) {
        lastPlaySpeed = speed
        player.rate = speed.rate
        displaySpeedTitle = speed.displayTitle
    }
}

プロパティ

@Published var player: AVPlayer
@Published var playSpeed = PlaySpeed.x1
@Published var displaySpeedTitle = PlaySpeed.x1.displayTitle
private var lastPlaySpeed = PlaySpeed.x1

private var cancellable: AnyCancellable?
  • player
    • パブリッシュする動画を再生する為のAVPlayer
  • playSpeed
    • パブリッシュする再生速度
  • displaySpeedTitle
    • パブリッシュする画面表示用の再生速度タイトル
  • lastPlaySpeed
    • 最後に変更された再生速度
  • cancellable
    • サブスクライブのキャンセルに必要

init

init(player: AVPlayer) {
    self.player = player
    observeRate()
}

引数として渡ってきたplayerを自身のplayerに代入し、playerrateを監視する為にobserveRate()を呼んでいます。

observeRate()

playerratesinkで変更がある度に行いたい処理を書いています。

func observeRate() {
    cancellable = player.publisher(for: \.rate).sink(receiveValue: { [weak self] rate in
        guard let self = self else { return }
        if rate == 1.0,
           self.lastPlaySpeed != self.playSpeed {
            // 停止ボタンを押して、再生を押すと自動的にrateが1.0に戻ってしまい
            // playSpeedもx1に戻ってしまう為、
            // 最後に設定していたSpeedにrateを変更する
            self.changeRate(into: self.lastPlaySpeed)
        }
    })
}

rateを変更していた場合でも、VideoPlayerで停止ボタンが押された際は、rate0.0になり、再生ボタンが押された際は、rate1.0になりました。さらにplaySpeedも標準に戻ってしまう動きになってしまっていた為、rate1.0に変更された際にlastPlaySpeedplaySpeedが一致しない場合は、lastPlaySpeedで再生レートをchangeRate(into:)関数を使用して変更するようにしました。

changeRate(into:)

func changeRate(into speed: PlaySpeed) {
    lastPlaySpeed = speed
    player.rate = speed.rate
    displaySpeedTitle = speed.displayTitle
}

引数で渡されたPlaySpeedを使用して、再生レートと表示用のタイトルを更新。rateが更新されたのでlastPlaySpeedの値もspeedの値で更新しています。

SpeedAdjustableVideoPlayer

再生速度を変更して調整可能なVideoPlayerのViewです。

import SwiftUI
import AVKit

struct SpeedAdjustableVideoPlayer: View {

    @StateObject var viewModel: SpeedAdjustableVideoPlayerViewModel

    init(player: AVPlayer) {
        _viewModel = StateObject(wrappedValue: SpeedAdjustableVideoPlayerViewModel(player: player))
    }

    var body: some View {

        VStack {
            VideoPlayer(player: viewModel.player)

            HStack {
                Spacer()

                Menu(viewModel.displaySpeedTitle) {
                    ForEach(PlaySpeed.allCases) { speed in
                        Button {
                            viewModel.changeRate(into: speed)
                        } label: {
                            Text(speed.rawValue)
                        }
                    }
                }
            }
        }
        .background(.black)
    }
}

init

init(player: AVPlayer) {
    _viewModel = StateObject(wrappedValue: SpeedAdjustableVideoPlayerViewModel(player: player))
}

initAVPlayerを受け取って、そのplayerSpeedAdjustableVideoPlayerViewModelに渡してViewModelを生成しています。

body

シンプルにVStackVideoPlayerと再生速度変更用のMenuを並べています。

var body: some View {

    VStack {
        VideoPlayer(player: viewModel.player)

        HStack {
            Spacer()

            Menu(viewModel.displaySpeedTitle) {
                ForEach(PlaySpeed.allCases) { speed in
                    Button {
                        viewModel.changeRate(into: speed)
                    } label: {
                        Text(speed.rawValue)
                    }
                }
            }
        }
    }
    .background(.black)
}

VideoPlayerの背景色と合わせるために.background.blackを設定しています。

ContentView

あとは、SpeedAdjustableVideoPlayerに再生したいurlが渡されたAVPlayerをセットするだけです。

import SwiftUI
import AVKit

struct ContentView: View {

    let url = Bundle.main.url(forResource: "sample",
                              withExtension: "mp4")!

    var body: some View {
        SpeedAdjustableVideoPlayer(player: AVPlayer(url: url))
    }
}

おわりに

無事に簡単な再生速度調整機能を追加することが出来ました。

コードはGitHubに載せております。

動画再生するだけならVideoPlayerだけで再生プレイヤーを表示できるのでとても助かりますね!

参考