Swift 動画再生をAVPlayerでDIYする

動画を再生する方法、二通りを試しましたのでメモしておきます。
・AVPlayerViewControllerをそのまま使用
　ソースコード量が少ない、便利
・AVPlayerでDIY実装
　再生／停止ボタン、ProgressBar、Silder、TimeすべてDIY可能

実装する前に、httpのURL動画を扱うために、info.plistをOpen as source codeで開いて
</dict>の箇所で以下を追加します。

```swift
<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
```

・AVPlayerViewController
　AVKitをImportする必要がある
　URLはネット動画のURL
　ローカル動画のfilePathを使います。

```Swift
let videoURL = URL(fileURLWithPath: "/Users/yourname/Library/Developer/CoreSimulator/Devices/BB015206-A61D-43F8-8A6B-9C8AD07B827A/data/Media/DCIM/100APPLE/IMG_0008.MP4")
```

```Swift
    let videoURL = URL(string: "http://0000.mp4")
    let player = AVPlayer(url: videoURL!)
    let playerViewController = AVPlayerViewController()
    playerViewController.player = player
    self.present(playerViewController, animated: true) {
        playerViewController.player!.play()
    }
```
完成イメージ縦画面
![Simulator Screen Shot - iPhone 8 Plus - 2020-11-27 at 14.56.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644086/65467ec9-3109-41f1-5eae-f272995d7466.png)
横画面
![Simulator Screen Shot - iPhone 8 Plus - 2020-11-27 at 14.57.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644086/eaa1ce19-c5e6-8ff1-22a0-a69e6057289d.png)

AVPlayerでDIYする場合、以下三つのパーツが欠かせません。
・AVPlayerItem　どの動画を再生するか、対象やデータを管理したりする。
・AVPlayer　再生、停止などの操作系。
・AVPlayerLayer 動画を表示する役割。

いろいろなパーツをDIYすることを考えると、PlayerViewを作り、
ViewControllerでPlayerViewを使うようにします。

では、まずPlayerViewを作ります。
Layoutはxibファイル内で制御します。

```Swift
import UIKit
import AVFoundation

class PlayerView: UIView {

    var playerLayer:AVPlayerLayer?
    
    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer?.frame = self.bounds
    }
}
```

次にStoryBoardでViewControllerを作り、新規ViewをPlayerViewタイプに指定します。
AVPlayerItem、AVPlayer、AVPlayerLayerも作ります。

```Swift
class ViewController: UIViewController {

    var playerItem:AVPlayerItem!
    var avplayer:AVPlayer!
    var playerLayer:AVPlayerLayer!
    
    @IBOutlet var playerView: PlayerView!
}
```

次にViewDidLoad（画面表示してからすぐ再生）の中にどんどん追加していきます。

```swift
override func viewDidLoad() {
        super.viewDidLoad()
        
        let url = URL(fileURLWithPath: "/Users/name/Library/Developer/CoreSimulator/Devices/BB015206-A61D-43F8-8A6B-9C8AD07B827A/data/Media/DCIM/100APPLE/IMG_0008.MP4")

        playerItem = AVPlayerItem(url: url) //誰を再生するか決める
        
        // 状態監視
        playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.new, context: nil)
        playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)

        self.avplayer = AVPlayer(playerItem: playerItem)
        playerLayer = AVPlayerLayer(player: avplayer)
        // 表示モードの設定
        playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
        playerLayer.contentsScale = UIScreen.main.scale

        self.playerView.playerLayer = self.playerLayer
        self.playerView.layer.insertSublayer(playerLayer, at: 0)
    }

    //画面消える時にremove
    deinit{
        playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
        playerItem.removeObserver(self, forKeyPath: "status")
    }
    
   //監視イベント
   //Unknown 、ReadyToPlay 、 Failed状態があり、readyToPlayの際のみ再生
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let playerItem = object as? AVPlayerItem else { return }
            if keyPath == "loadedTimeRanges"{
                // TODO
            }else if keyPath == "status"{
                if playerItem.status == AVPlayerItem.Status.readyToPlay{
                    self.avplayer.play()
                }else{
                    print("error")
                }
            }
    }
```
これで再生できるはずです。でも、ボタンもProgressBarも何もないので引き続き実装に進みます。

●Time
PlayerViewにてCurrentTimeとTotalTimeを表示するtimeLabelを作成
ViewControllerにて秒数をStringへ変換するメソッドを作成

```Swift
func formatPlayTime(secounds:TimeInterval)->String{
        if secounds.isNaN{
            return "00:00"
        }
        let Min = Int(secounds / 60)
        let Sec = Int(secounds.truncatingRemainder(dividingBy: 60))
        return String(format: "%02d:%02d", Min, Sec)
    }
```
常に時間の変化を表示するため、CADisPlayLinkを生成して使います。
動画のコマごとにCADisPlayLinkが呼ばれます。

```Swift
self.link = CADisplayLink(target: self, selector: #selector(update))
self.link.add( to: RunLoop.main, forMode: RunLoop.Mode.default)
```
updateの中身はこのように書きます。これで時間が表示できるようになります。

```Swift
@objc func update(){
    let currentTime = CMTimeGetSeconds(self.avplayer.currentTime())
    let totalTime   = NSTimeInterval(playerItem.duration.value) / NSTimeInterval(playerItem.duration.timescale)
    let timeStr = "\(formatPlayTime(currentTime))/\(formatPlayTime(totalTime))" 
    playerView.timeLabel.text = timeStr 
}
```

●ProgressBar
次に再生の進捗によってProgressBarも動くようにUISilderを使います。
StoryBoardでUISilderを追加したら、以下のように設定します。![スクリーンショット 2020-11-28 8.56.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644086/85c299b8-fa34-61e5-054c-21cf35362c4a.png)

SilderのPinのImageを変えるため、以下を書きます。

```Swift
@IBOutlet weak var slider: UISlider!{
    didSet{
        slider.setThumbImage(UIImage(systemName: "bolt.fill"), for: UIControl.State.normal)
    }
}
```

Pinが動くようにUpdateメソッドに以下を追加します。

```Swift
self.testPlayerView.slider.value = Float(currentTime/totalTime)
```
●ProgressBarの動作
この時点ではProgressBarをドラッグしたりできませんので次に動作の処理をします。
awakeFromNibの中で追加します。念のため指を離した時は三つイベントを監視します。

```Swift
// タップした時
slider.addTarget(self, action: #selector(sliderTouchDown), for: UIControl.Event.touchDown)
// 指を離した時
slider.addTarget(self, action: #selector(sliderTouchUpOut), for: UIControl.Event.touchUpOutside)
slider.addTarget(self, action: #selector(sliderTouchUpOut), for: UIControl.Event.touchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUpOut), for: UIControl.Event.touchCancel)
```

ドラッグしている間では動画の再生進捗を変え続けたくないので、sliding を追加します。
ここでProtocalが登場します。

```Swift
protocol TestPlayerViewDelegate:NSObjectProtocol {
    func testPlayer(playerView:PlayerView,sliderTouchUpOut slider:UISlider)
}
```
PlayerViewの中でdelegateを使います。

```Swift
weak var delegate: TestPlayerViewDelegate?
```
前のsliderTouchDown、sliderTouchUpOutのメソッドを書きます。

```Swift
@objc func sliderTouchDown(slider:UISlider){
    self.sliding = true
}

@objc func sliderTouchUpOut(slider:UISlider){
    delegate?.testPlayer(playerView: self, sliderTouchUpOut: slider)
}
```

そしてViewControllerの中で実際に指を離した時の処理を書きます。

```Swift
extension ViewController: TestPlayerViewDelegate{
    
    func testPlayer(playerView: PlayerView, sliderTouchUpOut slider: UISlider) {
        let duration = slider.value * Float(CMTimeGetSeconds(self.avplayer.currentItem!.duration))
        let seekTime = CMTimeMake(value: Int64(duration), timescale: 1)
        // 位置を特定
        self.avplayer.seek(to: seekTime, completionHandler: { (b) in
            // sliding状態を戻す
            playerView.sliding = false
        })
    }
}
```
ViewControllerのviewDidLoadでdelegateを忘れずに

```Swift
self.playerView.delegate = self
```

UpdateメソッドのSilderが動く処理にもsliding状態の判断を追加します。
これでProgressBarはドラッグできるようになります。

```Swift
if !self.playerView.sliding{
    // 播放进度
    self.playerView.slider.value = Float(currentTime/totalTime)
}
```

●PlayAndPauseボタン
最後に再生／停止ボタンの処理を書きます。
Sliderの処理と似ていています。StoryBaordでボタンを設置、Layoutを設定します。
awakeFromNibの中で以下を追加します。

```Swift
playAndPauseBtn.addTarget(self, action: #selector(playAndPause) , for: UIControl.Event.touchUpInside)
```
メソッドを追加します。
再生と停止の処理もDelegateの中で書きます。

```Swift
@objc func playAndPause(btn:UIButton){
    let tmp = !playing
    playing = tmp 

    if playing {
        playAndPauseBtn.setImage(UIImage(systemName: "stop.circle"), for: UIControl.State.normal)
    }else{
        playAndPauseBtn.setImage(UIImage(systemName: "play.circle"), for: UIControl.State.normal)
    }

    delegate?.testPlayer(playerView: self, playAndPause: btn)
}
```

```Swift
protocol TestPlayerViewDelegate:NSObjectProtocol {
    func testPlayer(playerView:PlayerView,sliderTouchUpOut slider:UISlider)
    func testPlayer(playerView:PlayerView,playAndPause playBtn:UIButton)
}
```
ViewControllerでメソッドを書きます。これで再生や停止ができるようになります。

```Swift
func testPlayer(playerView: PlayerView, playAndPause playBtn: UIButton) {
    if !playerView.playing{
        self.avplayer.pause()
    }else{
        if self.avplayer.status == AVPlayer.Status.readyToPlay{
            self.avplayer.play()
        }
    }
}
```
完成イメージ
![Simulator Screen Shot - iPod touch (7th generation) - 2020-11-28 at 09.51.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644086/f724b5cb-f986-69dd-5b7b-abbc5a302464.png)
![Simulator Screen Shot - iPod touch (7th generation) - 2020-11-28 at 09.51.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644086/a9dcab26-a2e0-8507-82cc-0713abfe4324.png)