6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[iOS]AVAssetWriter+swifterでiPhone実機からHLS配信

Last updated at Posted at 2021-08-10

iOS で AVAssetWriter + swifter で HLS 配信

  • iOS14 から AVAssetWriter に fragmented MPEG-4 ファイルを出力する機能が追加されました!!!

  • swifter を使って httpサーバを起動し、 AVAssetWriter で作成した fragmented MPEG-4 ファイルを配布することで、iPhone から HLS で映像配信する事が可能です

関連リンク

  • リポジトリはこちら

  • Apple のゲームアプリのテンプレートの画面を配信する事が可能です
  • 参考になりました

実装

fragmented MPEG-4 ファイルの作成

  • AVAssetWriter を作って、fragmented MPEG-4 ファイルを作成します

AVAssetWriter の作成

  • contentType に .mpeg4Movie を指定して、AVAssetWriter を作成します
  • outputFileTypeProfile を .mpeg4AppleHLS に設定します
  • preferredOutputSegmentInterval に期待するセグメントの時間を設定します
    • 今回は、1秒に設定します
  • 作成したセグメントを受け取るために、delegate を設定します
  • 適当に AVAssetWriterInput を作って追加します
let assetWriter = AVAssetWriter(contentType: .mpeg4Movie)
assetWriter.shouldOptimizeForNetworkUse = true
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 1, preferredTimescale: 1)
assetWriter.delegate = self

let settings = [
    AVVideoCodecKey : AVVideoCodecType.h264,
    AVVideoWidthKey : width,
    AVVideoHeightKey : height,
] as [String : Any]
let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
input.expectsMediaDataInRealTime = true
assetWriter.add(input)

配信開始

  • initialSegmentStartTime を設定して、セッションを開始します
func start(time: CMTime) throws {
    assetWriter.initialSegmentStartTime = time
    assetWriter.startWriting()
    assetWriter.startSession(atSourceTime: time)
}

書き込み

  • CMSampleBuffer を AVAssetWriterInput に書き込みます
func write(sample: CMSampleBuffer) throws {
    input.append(sample)
}

セグメントの取得

  • AVAssetWriterDelegate を実装し、セグメントのデータを取得します
  • これで、fragmented MPEG-4 のデータが取得できました
extension MediaWriter: AVAssetWriterDelegate {
    func assetWriter(_ writer: AVAssetWriter,
                     didOutputSegmentData segmentData: Data,
                     segmentType: AVAssetSegmentType,
                     segmentReport: AVAssetSegmentReport?) {
        self.onSegmentData?(segmentData)
    }
}
  • 取得したセグメントデータは、最初のデータと、その後のデータを区別してキャッシュしておきます
    • 理由は後述
  • 順序に並べるために、sequence の index をつけたタプルで配列に格納します
var sequence: Int = -1
var initData: Data? = nil
func onSegmentData(data: Data) {
    sequence += 1
    if sequence == 0 {
        initData = data
        return
    }
    sequences.append((sequence: sequence, data: data))
}

swifter を使ったサーバの起動

swifter を使った HttpServer を起動

  • ハンドラーを設定します
import Swifter

let server = HttpServer()
// ~~ handler の設定 ~~
try! server.start(8080, forceIPv4: true, priority: .default)

ハンドラーの設定

html

  • hls.js を使って再生します
server["/hello"] = { (request: HttpRequest) -> HttpResponse in
    let body = """
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="video" width="240" height="360" autoplay muted></video>
<script>
  var video = document.getElementById('video');
  var videoSrc = 'hls.m3u8';
  if (Hls.isSupported()) {
    var hls = new Hls();
    hls.loadSource(videoSrc);
    hls.attachMedia(video);
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = videoSrc;
  }
</script>
"""
    return .ok(.htmlBody(body))
}

m3u8

  • 最新の3つ前分のファイルを返します
  • 最初のセグメントデータは、EXT-X-MAP で利用します
  • コンテンツタイプは、"application/x-mpegURL" です
server["/hls.m3u8"] = { [weak self] (request: HttpRequest) -> HttpResponse in
    let body: HttpResponseBody = .data(self!.m3u8.data(using: .utf8)!, contentType: "application/x-mpegURL")
    return .ok(body)
}
private var m3u8: String {
    let template = """
#EXTM3U
#EXT-X-TARGETDURATION:1
#EXT-X-VERSION:9
#EXT-X-MEDIA-SEQUENCE:\(sequence - 2)
#EXT-X-MAP:URI="init.mp4"
#EXTINF:1.0,
files/sequence\(sequence - 2).m4s
#EXTINF:1.0,
files/sequence\(sequence - 1).m4s
#EXTINF:1.0,
files/sequence\(sequence).m4s
"""
    return template
}

セグメントデータ

  • 最初のセグメントを返す init.mp4 と、キャッシュした最新のセグメントを返す files/sequenceN.m4s のリクエストの処理を実装します
  • init.mp4 のコンテンツタイプは、"video/mp4" を、files/sequenceN.m4s のコンテンツタイプは、"video/iso.segment" を指定します
server["init.mp4"] = { [weak self] (request: HttpRequest) -> HttpResponse in
    let body: HttpResponseBody = .data(self!.initData!, contentType: "video/mp4")
    return .ok(body)
}
server["/files/:path"] = { [weak self] (request: HttpRequest) -> HttpResponse in
    guard let data = self!.sequences.first(where: { request.path.hasPrefix("/files/sequence\($0.sequence)") })?.data else {
        return .notFound
    }
    let body: HttpResponseBody = .data(data, contentType: "video/iso.segment")
    return .ok(body)
}

動作確認

  • 実機でビルドし、http://{{iPhoneのipアドレス}}:8080/hello にアクセスします
  • 動画が再生されることを確認します

おわりに

  • AVAssetWriter と swifter を使うことで、HLS 配信を容易に実装する事ができました
  • HLS なので遅延は3秒程度発生してしまいます
  • AVAssetWriter は cmaf も対応したようなので、試してみたいです
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?