Skip to content

Instantly share code, notes, and snippets.

@ivanopcode
Created August 18, 2023 13:00
Show Gist options
  • Save ivanopcode/4cebbbee868f65ad2e3f3ec76830f5a2 to your computer and use it in GitHub Desktop.
Save ivanopcode/4cebbbee868f65ad2e3f3ec76830f5a2 to your computer and use it in GitHub Desktop.
ffmpeg_swift_wrapper_transcode_hls_example
//
// Transcoder.swift
//
// Copyright (c) 2020 Changbeom Ahn, 2023 Ivan Oparin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
import FFmpegSupport
public typealias TimeRange = Range<TimeInterval>
public enum FFmpegError: Error {
case exit(code: Int)
case fileNotFound
}
open class Transcoder: ObservableObject {
open var progressBlock: ((Double) -> Void)?
public init(progressBlock: ((Double) -> Void)? = nil) {
self.progressBlock = progressBlock
}
open func convertToHLS(fileName: String, segmentTime: Int = 10) throws {
guard let from = Bundle.main.url(forResource: fileName, withExtension: "MP4") else {
throw FFmpegError.fileNotFound
}
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let outputPath = documentsDirectory.appendingPathComponent(fileName)
// Create output directory if it doesn't exist
do {
try FileManager.default.createDirectory(at: outputPath, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Couldn't create directory \(outputPath)")
throw error
}
let args = [
"FFmpeg-iOS",
"-i", from.path,
"-f", "segment",
"-segment_time", "\(segmentTime)",
"-segment_format", "mpegts",
"-segment_list", outputPath.appendingPathComponent("playlist.m3u8").path,
"\(outputPath.path)/segment%03d.ts"
]
let code = ffmpeg(args)
print(#function, code)
guard code == 0 else {
throw FFmpegError.exit(code: code)
}
print("success")
}
@available(iOS 13.0, *)
open func transcode(from: URL, to url: URL, timeRange: TimeRange?, bitRate: Double?) throws {
let pipe = Pipe()
Task {
if #available(iOS 15.0, *) {
var info = [String: String]()
let maxTime: Double
if let timeRange = timeRange {
maxTime = (timeRange.upperBound - timeRange.lowerBound) * 1_000_000
} else {
maxTime = 1_000_000 // FIXME: probe?
}
print(#function, "await lines", pipe.fileHandleForReading)
for try await line in pipe.fileHandleForReading.bytes.lines {
// print(#function, line)
let components = line.split(separator: "=")
assert(components.count == 2)
let key = String(components[0])
info[key] = String(components[1])
if key == "progress" {
// print(#function, info)
if let time = Int(info["out_time_us"] ?? ""),
time >= 0 { // FIXME: reset global variable(s) causing it
let progress = Double(time) / maxTime
print(#function, "progress:", progress
// , info["out_time_us"] ?? "nil", time
)
progressBlock?(progress)
}
guard info["progress"] != "end" else { break }
info.removeAll()
}
}
print(#function, "no more lines?", pipe.fileHandleForReading)
} else {
// Fallback on earlier versions
}
}
var args = [
"FFmpeg-iOS",
"-progress", "pipe:\(pipe.fileHandleForWriting.fileDescriptor)",
"-nostats",
]
if let timeRange = timeRange {
args += [
"-ss", "\(timeRange.lowerBound)",
"-t", "\(timeRange.upperBound - timeRange.lowerBound)",
]
}
args += [
"-i", from.path,
]
if let bitRate = bitRate {
args += [
"-b:v", "\(Int(bitRate))k",
]
}
args += [
"-c:v", "h264_videotoolbox",
url.path,
]
let code = ffmpeg(args)
print(#function, code)
try pipe.fileHandleForWriting.close()
guard code == 0 else {
throw FFmpegError.exit(code: code)
}
}
}
public func format(_ seconds: Int) -> String? {
guard seconds >= 0 else {
print(#function, "invalid seconds:", seconds)
return nil
}
let (minutes, sec) = seconds.quotientAndRemainder(dividingBy: 60)
var string = "\(sec)"
guard minutes > 0 else {
return string
}
let (hours, min) = minutes.quotientAndRemainder(dividingBy: 60)
string = "\(min):" + (sec < 10 ? "0" : "") + string
guard hours > 0 else {
return string
}
return "\(hours):" + (min < 10 ? "0" : "") + string
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment