iOS で AVAssetWriter + swifter で HLS 配信
- iOS14 から AVAssetWriter に fragmented MPEG-4 ファイルを出力する機能が追加されました!!!
- swifter を使って httpサーバを起動し、 AVAssetWriter で作成した fragmented MPEG-4 ファイルを配布することで、iPhone から HLS で映像配信する事が可能です
関連リンク
- リポジトリはこちら
- Apple のゲームアプリのテンプレートの画面を配信する事が可能です
iPhoneからHLS配信できた! pic.twitter.com/9GFckF9tsP
— ふじき (@fzkqi) August 13, 2021
- 参考になりました
実装
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 も対応したようなので、試してみたいです