| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963 |
- import Foundation
- public let kLNBoxSizeLengthInBytes = 4
- public let kLNBoxTypeLengthInBytes = 4
- public let kLNBoxLargeSizeLengthInBytes = 8
- public let kLNBoxLargeSizeFlagLengthInBytes = 1
- private func lnReadU32(_ bytes: UnsafePointer<UInt8>, _ offset: Int) -> UInt32 {
- (UInt32(bytes[offset]) << 24)
- | (UInt32(bytes[offset + 1]) << 16)
- | (UInt32(bytes[offset + 2]) << 8)
- | UInt32(bytes[offset + 3])
- }
- private extension Data {
- func lnU32(at offset: Int) -> UInt32 {
- guard count >= offset + 4 else { return 0 }
- return withUnsafeBytes { rawPtr in
- guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
- return lnReadU32(base, offset)
- }
- }
- func lnU64(at offset: Int) -> UInt64 {
- guard count >= offset + 8 else { return 0 }
- return withUnsafeBytes { rawPtr in
- guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
- return lnReadU64(base.advanced(by: offset))
- }
- }
- }
- private func lnReadU64(_ bytes: UnsafePointer<UInt8>) -> UInt64 {
- let b0 = UInt64(bytes[0]) << 56
- let b1 = UInt64(bytes[1]) << 48
- let b2 = UInt64(bytes[2]) << 40
- let b3 = UInt64(bytes[3]) << 32
- let b4 = UInt64(bytes[4]) << 24
- let b5 = UInt64(bytes[5]) << 16
- let b6 = UInt64(bytes[6]) << 8
- let b7 = UInt64(bytes[7])
- return b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7
- }
- @objc public enum LNMP4CodecType: Int {
- case unknown = 0
- case video
- case audio
- }
- @objc public enum LNMP4TrackType: UInt32 {
- case video = 0x76696465
- case audio = 0x736F756E
- case hint = 0x68696E74
- }
- @objc public enum LNMP4VideoStreamCodecID: Int {
- case unknown = 0
- case h264
- case h265
- }
- @objc public enum LNMP4BoxType: UInt32 {
- case unknown = 0x0
- case ftyp = 0x66747970
- case free = 0x66726565
- case mdat = 0x6D646174
- case moov = 0x6D6F6F76
- case mvhd = 0x6D766864
- case iods = 0x696F6473
- case trak = 0x7472616B
- case tkhd = 0x746B6864
- case edts = 0x65647473
- case elst = 0x656C7374
- case mdia = 0x6D646961
- case mdhd = 0x6D646864
- case hdlr = 0x68646C72
- case minf = 0x6D696E66
- case vmhd = 0x766D6864
- case dinf = 0x64696E66
- case dref = 0x64726566
- case url = 0x0075726C
- case stbl = 0x7374626C
- case stsd = 0x73747364
- case avc1 = 0x61766331
- case avcC = 0x61766343
- case stts = 0x73747473
- case stss = 0x73747373
- case stsc = 0x73747363
- case stsz = 0x7374737A
- case stco = 0x7374636F
- case ctts = 0x63747473
- case udta = 0x75647461
- case meta = 0x6D657461
- case ilst = 0x696C7374
- case data = 0x64617461
- case wide = 0x77696465
- case loci = 0x6C6F6369
- case smhd = 0x736D6864
- case vapc = 0x76617063
- case hvc1 = 0x68766331
- case hvcC = 0x68766343
- }
- public typealias LNMP4BoxDataFetcher = (LNMP4Box) -> Data?
- @objcMembers
- open class LNMP4Box: NSObject {
- public var type: LNMP4BoxType
- public var length: UInt64
- public var startIndexInBytes: UInt64
- public weak var superBox: LNMP4Box?
- public var subBoxes: [LNMP4Box] = []
- @objc(initWithType:startIndex:length:)
- public init(type: LNMP4BoxType, startIndex: UInt64, length: UInt64) {
- self.type = type
- self.startIndexInBytes = startIndex
- self.length = length
- }
- @objc(subBoxOfType:)
- public func subBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
- for subBox in subBoxes {
- if subBox.type == type { return subBox }
- if let box = subBox.subBox(ofType: type) { return box }
- }
- return nil
- }
- @objc(superBoxOfType:)
- public func superBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
- guard let superBox else { return nil }
- if superBox.type == type { return superBox }
- return superBox.superBox(ofType: type)
- }
- open func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {}
- private func description(level: Int) -> String {
- var desc = "Box:\(typeString) offset:\(startIndexInBytes) size:\(length) "
- for _ in 0..<level { desc = "|--\(desc)" }
- desc = "\n\(desc)"
- for sub in subBoxes {
- desc += sub.description(level: level + 1)
- }
- return desc
- }
- public override var description: String {
- description(level: 0)
- }
- private var typeString: String {
- let value = type.rawValue
- if value == 0 { return "unknown" }
- let chars: [UInt8] = [
- UInt8((value >> 24) & 0xFF),
- UInt8((value >> 16) & 0xFF),
- UInt8((value >> 8) & 0xFF),
- UInt8(value & 0xFF)
- ]
- return String(bytes: chars.filter { $0 != 0 }, encoding: .ascii) ?? "unknown"
- }
- }
- @objcMembers public final class LNMP4MdatBox: LNMP4Box {}
- @objcMembers public final class LNMP4AvccBox: LNMP4Box {}
- @objcMembers public final class LNMP4HvccBox: LNMP4Box {}
- @objcMembers public final class LNMP4MvhdBox: LNMP4Box {}
- @objcMembers public final class LNMP4StsdBox: LNMP4Box {}
- @objcMembers public final class LNMP4TrackBox: LNMP4Box {}
- @objcMembers
- public final class LNStscEntry: NSObject {
- public var firstChunk: UInt32 = 0
- public var samplesPerChunk: UInt32 = 0
- public var sampleDescriptionIndex: UInt32 = 0
- }
- @objcMembers
- public final class LNMP4StscBox: LNMP4Box {
- public var entries: [LNStscEntry] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let stscData = dataFetcher(self), stscData.count >= 16 else { return }
- let entryCount = Int(stscData.lnU32(at: 12))
- entries.removeAll(keepingCapacity: true)
- for i in 0..<entryCount {
- let base = 16 + i * 12
- guard stscData.count >= base + 12 else { break }
- let entry = LNStscEntry()
- entry.firstChunk = stscData.lnU32(at: base)
- entry.samplesPerChunk = stscData.lnU32(at: base + 4)
- entry.sampleDescriptionIndex = stscData.lnU32(at: base + 8)
- entries.append(entry)
- }
- }
- }
- @objcMembers
- public final class LNMP4StcoBox: LNMP4Box {
- public var chunkCount: UInt32 = 0
- public var chunkOffsets: [NSNumber] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let stcoData = dataFetcher(self), stcoData.count >= 16 else { return }
- let entryCount = Int(stcoData.lnU32(at: 12))
- chunkCount = UInt32(entryCount)
- chunkOffsets.removeAll(keepingCapacity: true)
- for i in 0..<entryCount {
- let base = 16 + i * 4
- guard stcoData.count >= base + 4 else { break }
- chunkOffsets.append(NSNumber(value: stcoData.lnU32(at: base)))
- }
- }
- }
- @objcMembers
- public final class LNMP4StssBox: LNMP4Box {
- public var syncSamples: [NSNumber] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let stssData = dataFetcher(self), stssData.count >= 16 else { return }
- let sampleCount = Int(stssData.lnU32(at: 12))
- syncSamples.removeAll(keepingCapacity: true)
- for i in 0..<sampleCount {
- let base = 16 + i * 4
- guard stssData.count >= base + 4 else { break }
- let index = Int(stssData.lnU32(at: base)) - 1
- syncSamples.append(NSNumber(value: max(index, 0)))
- }
- }
- }
- @objcMembers
- public final class LNMP4CttsBox: LNMP4Box {
- public var compositionOffsets: [NSNumber] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let cttsData = dataFetcher(self), cttsData.count >= 16 else { return }
- let entryCount = Int(cttsData.lnU32(at: 12))
- compositionOffsets.removeAll(keepingCapacity: true)
- for i in 0..<entryCount {
- let base = 16 + i * 8
- guard cttsData.count >= base + 8 else { break }
- let sampleCount = Int(cttsData.lnU32(at: base))
- let compositionOffset = cttsData.lnU32(at: base + 4)
- for _ in 0..<sampleCount {
- compositionOffsets.append(NSNumber(value: compositionOffset))
- }
- }
- }
- }
- @objcMembers
- public final class LNSttsEntry: NSObject {
- public var sampleCount: UInt32 = 0
- public var sampleDelta: UInt32 = 0
- }
- @objcMembers
- public final class LNMP4SttsBox: LNMP4Box {
- public var entries: [LNSttsEntry] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let sttsData = dataFetcher(self), sttsData.count >= 16 else { return }
- let entryCount = Int(sttsData.lnU32(at: 12))
- entries.removeAll(keepingCapacity: true)
- for i in 0..<entryCount {
- let base = 16 + i * 8
- guard sttsData.count >= base + 8 else { break }
- let entry = LNSttsEntry()
- entry.sampleCount = sttsData.lnU32(at: base)
- entry.sampleDelta = sttsData.lnU32(at: base + 4)
- entries.append(entry)
- }
- }
- }
- @objcMembers
- public final class LNMP4StszBox: LNMP4Box {
- public var sampleCount: UInt32 = 0
- public var sampleSizes: [NSNumber] = []
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let stszData = dataFetcher(self), stszData.count >= 20 else { return }
- let sampleSize = stszData.lnU32(at: 12)
- let sampleCount = Int(stszData.lnU32(at: 16))
- self.sampleCount = UInt32(sampleCount)
- sampleSizes.removeAll(keepingCapacity: true)
- for i in 0..<sampleCount {
- if sampleSize > 0 {
- sampleSizes.append(NSNumber(value: sampleSize))
- } else {
- let base = 20 + i * 4
- guard stszData.count >= base + 4 else { break }
- sampleSizes.append(NSNumber(value: stszData.lnU32(at: base)))
- }
- }
- }
- }
- @objcMembers
- public final class LNMP4HdlrBox: LNMP4Box {
- public var trackType: LNMP4TrackType = .video
- public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
- guard let hdlrData = dataFetcher(self), hdlrData.count >= 20 else { return }
- let trackTypeValue = hdlrData.lnU32(at: 16)
- trackType = LNMP4TrackType(rawValue: trackTypeValue) ?? .video
- }
- }
- @objcMembers
- public final class LNMP4Sample: NSObject {
- public var codecType: LNMP4CodecType = .unknown
- public var sampleDelta: UInt32 = 0
- public var sampleSize: UInt32 = 0
- public var sampleIndex: UInt32 = 0
- public var chunkIndex: UInt32 = 0
- public var streamOffset: UInt32 = 0
- public var pts: UInt64 = 0
- public var dts: UInt64 = 0
- public var isKeySample: Bool = false
- }
- @objcMembers
- public final class LNChunkOffsetEntry: NSObject {
- public var samplesPerChunk: UInt32 = 0
- public var offset: UInt32 = 0
- }
- @objcMembers
- public final class LNCttsEntry: NSObject {
- public var sampleCount: UInt32 = 0
- public var compositionOffset: UInt32 = 0
- }
- @objcMembers
- public final class LNMP4BoxFactory: NSObject {
- @objc(isTypeValueValid:)
- public static func isTypeValueValid(_ type: LNMP4BoxType) -> Bool {
- boxClass(for: type) != nil
- }
- @objc(boxClassForType:)
- public static func boxClass(for type: LNMP4BoxType) -> AnyClass? {
- switch type {
- case .stss:
- return LNMP4StssBox.self
- case .mdat:
- return LNMP4MdatBox.self
- case .avcC:
- return LNMP4AvccBox.self
- case .mdhd:
- return LNMP4MvhdBox.self
- case .stsd:
- return LNMP4StsdBox.self
- case .stsz:
- return LNMP4StszBox.self
- case .hdlr:
- return LNMP4HdlrBox.self
- case .stsc:
- return LNMP4StscBox.self
- case .stts:
- return LNMP4SttsBox.self
- case .stco:
- return LNMP4StcoBox.self
- case .hvcC:
- return LNMP4HvccBox.self
- case .ctts:
- return LNMP4CttsBox.self
- case .trak:
- return LNMP4TrackBox.self
- case .ftyp, .free, .moov, .mvhd, .tkhd, .edts, .elst, .mdia, .minf, .vmhd, .dinf, .dref, .url, .stbl, .avc1, .udta, .meta, .ilst, .data, .iods, .wide, .loci, .smhd, .vapc, .hvc1:
- return LNMP4Box.self
- default:
- return nil
- }
- }
- @objc(createBoxForType:startIndex:length:)
- public static func createBox(for type: LNMP4BoxType, startIndex: UInt64, length: UInt64) -> LNMP4Box {
- switch type {
- case .stss: return LNMP4StssBox(type: type, startIndex: startIndex, length: length)
- case .mdat: return LNMP4MdatBox(type: type, startIndex: startIndex, length: length)
- case .avcC: return LNMP4AvccBox(type: type, startIndex: startIndex, length: length)
- case .mdhd: return LNMP4MvhdBox(type: type, startIndex: startIndex, length: length)
- case .stsd: return LNMP4StsdBox(type: type, startIndex: startIndex, length: length)
- case .stsz: return LNMP4StszBox(type: type, startIndex: startIndex, length: length)
- case .hdlr: return LNMP4HdlrBox(type: type, startIndex: startIndex, length: length)
- case .stsc: return LNMP4StscBox(type: type, startIndex: startIndex, length: length)
- case .stts: return LNMP4SttsBox(type: type, startIndex: startIndex, length: length)
- case .stco: return LNMP4StcoBox(type: type, startIndex: startIndex, length: length)
- case .hvcC: return LNMP4HvccBox(type: type, startIndex: startIndex, length: length)
- case .ctts: return LNMP4CttsBox(type: type, startIndex: startIndex, length: length)
- case .trak: return LNMP4TrackBox(type: type, startIndex: startIndex, length: length)
- default: return LNMP4Box(type: type, startIndex: startIndex, length: length)
- }
- }
- }
- @objc
- public protocol LNMP4ParserDelegate: AnyObject {
- @objc optional func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser)
- @objc(MP4FileDidFinishParse:) optional func mp4FileDidFinishParse(_ parser: LNMP4Parser)
- }
- @objcMembers
- public final class LNMP4Parser: NSObject {
- public var rootBox: LNMP4Box?
- public var fileHandle: FileHandle?
- public weak var delegate: LNMP4ParserDelegate?
- private let filePath: String
- @objc(initWithFilePath:)
- public init(filePath: String) {
- self.filePath = filePath
- self.fileHandle = FileHandle(forReadingAtPath: filePath)
- super.init()
- }
- deinit {
- fileHandle?.closeFile()
- }
- public func parse() {
- guard !filePath.isEmpty, let fileHandle else { return }
- let fileSize = fileHandle.seekToEndOfFile()
- fileHandle.seek(toFileOffset: 0)
- rootBox = LNMP4BoxFactory.createBox(for: .unknown, startIndex: 0, length: UInt64(fileSize))
- guard let rootBox else { return }
- var bfsQueue: [LNMP4Box] = [rootBox]
- while !bfsQueue.isEmpty {
- let calBox = bfsQueue.removeFirst()
- if calBox.length <= UInt64(2 * (kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)) {
- continue
- }
- var offset: UInt64 = calBox.superBox == nil ? 0 : (calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes))
- if shouldResetOffset(calBox.type) {
- calibrateOffset(&offset, boxType: calBox.type)
- }
- while true {
- if offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) > calBox.startIndexInBytes + calBox.length {
- break
- }
- guard let parsed = readBoxTypeAndLength(offset: offset) else { break }
- let type = parsed.type
- let length = parsed.length
- if length == 0 {
- // Invalid box length; avoid infinite loop on malformed data.
- break
- }
- if offset + length > calBox.startIndexInBytes + calBox.length {
- break
- }
- if !LNMP4BoxFactory.isTypeValueValid(type), offset == calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) {
- break
- }
- let subBox = LNMP4BoxFactory.createBox(for: type, startIndex: offset, length: length)
- subBox.superBox = calBox
- calBox.subBoxes.append(subBox)
- bfsQueue.append(subBox)
- didParseBox(subBox)
- offset += length
- }
- }
- didFinishParseFile()
- }
- @objc(readDataForBox:)
- public func readData(for box: LNMP4Box?) -> Data? {
- guard let box, let fileHandle else { return nil }
- fileHandle.seek(toFileOffset: box.startIndexInBytes)
- return fileHandle.readData(ofLength: Int(box.length))
- }
- @objc(readValue:length:)
- public func readValue(_ bytes: UnsafePointer<CChar>, length: Int) -> Int {
- var value = 0
- for i in 0..<length {
- value += (Int(bytes[i]) & 0xff) << ((length - i - 1) * 8)
- }
- return value
- }
- private func readValue(_ bytes: UnsafePointer<UInt8>, length: Int) -> UInt64 {
- var value: UInt64 = 0
- for i in 0..<length {
- value += UInt64(bytes[i] & 0xff) << UInt64((length - i - 1) * 8)
- }
- return value
- }
- private func readBoxTypeAndLength(offset: UInt64) -> (type: LNMP4BoxType, length: UInt64)? {
- guard let fileHandle else { return nil }
- fileHandle.seek(toFileOffset: offset)
- let headerData = fileHandle.readData(ofLength: kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
- guard headerData.count >= kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes else {
- return nil
- }
- let length: UInt64 = headerData.withUnsafeBytes { rawPtr in
- guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
- return readValue(bytes, length: kLNBoxSizeLengthInBytes)
- }
- let typeRaw: UInt32 = headerData.withUnsafeBytes { rawPtr in
- guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
- return UInt32(readValue(bytes.advanced(by: kLNBoxSizeLengthInBytes), length: kLNBoxTypeLengthInBytes))
- }
- var finalLength = length
- let type = LNMP4BoxType(rawValue: typeRaw) ?? .unknown
- if finalLength == UInt64(kLNBoxLargeSizeFlagLengthInBytes) {
- let extendedOffset = offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
- fileHandle.seek(toFileOffset: extendedOffset)
- let largeSizeData = fileHandle.readData(ofLength: kLNBoxLargeSizeLengthInBytes)
- guard largeSizeData.count >= kLNBoxLargeSizeLengthInBytes else {
- return nil
- }
- finalLength = largeSizeData.withUnsafeBytes { rawPtr in
- guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
- return readValue(bytes, length: kLNBoxLargeSizeLengthInBytes)
- }
- if finalLength == 0 { return nil }
- }
- if finalLength == 0 {
- return nil
- }
- return (type, finalLength)
- }
- private func shouldResetOffset(_ type: LNMP4BoxType) -> Bool {
- type == .stsd || type == .avc1 || type == .hvc1
- }
- private func calibrateOffset(_ offset: inout UInt64, boxType type: LNMP4BoxType) {
- switch type {
- case .stsd:
- offset += 8
- case .avc1, .hvc1:
- offset += 78
- default:
- break
- }
- }
- private func didParseBox(_ box: LNMP4Box) {
- box.boxDidParsed { [weak self] in
- self?.readData(for: $0)
- }
- delegate?.didParseMP4Box?(box, parser: self)
- }
- private func didFinishParseFile() {
- delegate?.mp4FileDidFinishParse?(self)
- }
- }
- @objcMembers
- public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
- private var parser: LNMP4Parser
- private var _picWidth: Int = 0
- private var _picHeight: Int = 0
- private var _fps: Int = 0
- private var _duration: Double = 0
- public var spsData: Data?
- public var ppsData: Data?
- private var _videoSamples: [LNMP4Sample]?
- private var _videoSyncSampleIndexes: [NSNumber]?
- public var rootBox: LNMP4Box?
- public var videoTrackBox: LNMP4TrackBox?
- public var audioTrackBox: LNMP4TrackBox?
- public var vpsData: Data?
- public var videoCodecID: LNMP4VideoStreamCodecID = .unknown
- public var picWidth: Int {
- get {
- if _picWidth == 0 {
- _picWidth = readPicWidth()
- }
- return _picWidth
- }
- set {
- _picWidth = newValue
- }
- }
- public var picHeight: Int {
- get {
- if _picHeight == 0 {
- _picHeight = readPicHeight()
- }
- return _picHeight
- }
- set {
- _picHeight = newValue
- }
- }
- public var duration: Double {
- get {
- if _duration == 0 {
- _duration = readDuration()
- }
- return _duration
- }
- set {
- _duration = newValue
- }
- }
- public var videoSamples: [LNMP4Sample] {
- get {
- if _videoSamples == nil {
- _videoSamples = buildVideoSamples()
- }
- return _videoSamples ?? []
- }
- set {
- _videoSamples = newValue
- }
- }
- public var videoSyncSampleIndexes: [NSNumber] {
- get {
- if _videoSyncSampleIndexes == nil {
- if let stss = videoTrackBox?.subBox(ofType: .stss) as? LNMP4StssBox {
- _videoSyncSampleIndexes = stss.syncSamples
- } else {
- _videoSyncSampleIndexes = []
- }
- }
- return _videoSyncSampleIndexes ?? []
- }
- set {
- _videoSyncSampleIndexes = newValue
- }
- }
- public var fps: Int {
- get {
- if _fps == 0 {
- let samples = videoSamples
- let totalDuration = duration
- if samples.isEmpty || totalDuration <= 0 {
- return 0
- }
- _fps = Int(lround(Double(samples.count) / totalDuration))
- }
- return _fps
- }
- set {
- _fps = newValue
- }
- }
- @objc(initWithFilePath:)
- public init(filePath: String) {
- parser = LNMP4Parser(filePath: filePath)
- super.init()
- parser.delegate = self
- }
- public func parse() {
- _picWidth = 0
- _picHeight = 0
- _fps = 0
- _duration = 0
- _videoSamples = nil
- _videoSyncSampleIndexes = nil
- spsData = nil
- ppsData = nil
- vpsData = nil
- parser.parse()
- rootBox = parser.rootBox
- parseVideoDecoderConfigRecord()
- }
- @objc(readPacketOfSample:)
- public func readPacket(ofSample sampleIndex: Int) -> Data? {
- guard sampleIndex >= 0, sampleIndex < videoSamples.count else { return nil }
- guard let fileHandle = parser.fileHandle else { return nil }
- let sample = videoSamples[sampleIndex]
- fileHandle.seek(toFileOffset: UInt64(sample.streamOffset))
- return fileHandle.readData(ofLength: Int(sample.sampleSize))
- }
- @objc(readDataOfBox:length:offset:)
- public func readData(of box: LNMP4Box, length: Int, offset: Int) -> Data? {
- guard length > 0, offset >= 0 else { return nil }
- let end = UInt64(offset) + UInt64(length)
- guard end <= box.length else { return nil }
- guard let fileHandle = parser.fileHandle else { return nil }
- fileHandle.seek(toFileOffset: box.startIndexInBytes + UInt64(offset))
- return fileHandle.readData(ofLength: length)
- }
- private func parseVideoDecoderConfigRecord() {
- if videoCodecID == .h264 {
- spsData = parseAvccSPSData()
- ppsData = parseAvccPPSData()
- } else if videoCodecID == .h265 {
- parseHvccDecoderConfigRecord()
- }
- }
- private func buildVideoSamples() -> [LNMP4Sample] {
- guard let videoTrackBox,
- let stts = videoTrackBox.subBox(ofType: .stts) as? LNMP4SttsBox,
- let stsz = videoTrackBox.subBox(ofType: .stsz) as? LNMP4StszBox,
- let stsc = videoTrackBox.subBox(ofType: .stsc) as? LNMP4StscBox,
- let stco = videoTrackBox.subBox(ofType: .stco) as? LNMP4StcoBox else {
- return []
- }
- let ctts = videoTrackBox.subBox(ofType: .ctts) as? LNMP4CttsBox
- let keySampleIndexSet = Set(videoSyncSampleIndexes.map { $0.intValue })
- var samples: [LNMP4Sample] = []
- var ptsAccumulator: UInt64 = 0
- var stscEntryIndex: UInt32 = 0
- var stscEntrySampleIndex: UInt32 = 0
- var stscEntrySampleOffset: UInt32 = 0
- var sttsEntryIndex: UInt32 = 0
- var sttsEntrySampleIndex: UInt32 = 0
- var stcoChunkLogicIndex: UInt32 = 0
- for i in 0..<Int(stsz.sampleCount) {
- if Int(stscEntryIndex) >= stsc.entries.count
- || Int(sttsEntryIndex) >= stts.entries.count
- || Int(stcoChunkLogicIndex) >= stco.chunkOffsets.count {
- break
- }
- let stscEntry = stsc.entries[Int(stscEntryIndex)]
- let sttsEntry = stts.entries[Int(sttsEntryIndex)]
- let chunkOffset = stco.chunkOffsets[Int(stcoChunkLogicIndex)].uint32Value
- let sampleOffset = chunkOffset + stscEntrySampleOffset
- var cttsValue: UInt32 = 0
- if let ctts, i < ctts.compositionOffsets.count {
- cttsValue = ctts.compositionOffsets[i].uint32Value
- }
- let sample = LNMP4Sample()
- sample.codecType = .video
- sample.sampleIndex = UInt32(i)
- sample.chunkIndex = stcoChunkLogicIndex
- sample.sampleDelta = sttsEntry.sampleDelta
- sample.sampleSize = stsz.sampleSizes[i].uint32Value
- sample.pts = ptsAccumulator + UInt64(cttsValue)
- sample.dts = ptsAccumulator
- sample.streamOffset = sampleOffset
- sample.isKeySample = keySampleIndexSet.contains(i)
- samples.append(sample)
- stscEntrySampleOffset += sample.sampleSize
- ptsAccumulator += UInt64(sample.sampleDelta)
- stscEntrySampleIndex += 1
- if stscEntrySampleIndex >= stscEntry.samplesPerChunk {
- if stcoChunkLogicIndex + 1 < UInt32(stco.chunkOffsets.count) {
- stcoChunkLogicIndex += 1
- }
- stscEntrySampleIndex = 0
- stscEntrySampleOffset = 0
- }
- sttsEntrySampleIndex += 1
- if sttsEntrySampleIndex >= sttsEntry.sampleCount {
- sttsEntrySampleIndex = 0
- if sttsEntryIndex + 1 < UInt32(stts.entries.count) {
- sttsEntryIndex += 1
- }
- }
- if stscEntryIndex + 1 < UInt32(stsc.entries.count) {
- let nextFirstChunk = stsc.entries[Int(stscEntryIndex + 1)].firstChunk
- if stcoChunkLogicIndex >= nextFirstChunk - 1 {
- stscEntryIndex += 1
- }
- }
- }
- return samples
- }
- private func parseHvccDecoderConfigRecord() {
- guard let hvcc = videoTrackBox?.subBox(ofType: .hvcC),
- let extraData = parser.readData(for: hvcc),
- extraData.count > 8 else { return }
- let bytes = [UInt8](extraData)
- var index = 30
- guard index < bytes.count else { return }
- let arrayNum = Int(bytes[index])
- index += 1
- for _ in 0..<arrayNum {
- guard index + 2 <= bytes.count else { return }
- let value = Int(bytes[index])
- index += 1
- let naluType = value & 0x3F
- let naluNum = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
- index += 2
- for _ in 0..<naluNum {
- guard index + 2 <= bytes.count else { return }
- let naluLength = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
- index += 2
- guard index + naluLength <= bytes.count else { return }
- let paramData = Data(bytes[index..<(index + naluLength)])
- if naluType == 32 {
- vpsData = paramData
- } else if naluType == 33 {
- spsData = paramData
- } else if naluType == 34 {
- ppsData = paramData
- }
- index += naluLength
- }
- }
- }
- private func parseAvccSPSData() -> Data? {
- guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
- let extraData = parser.readData(for: avcc),
- extraData.count > 16 else { return nil }
- let bytes = [UInt8](extraData)
- let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
- let naluType = Int(bytes[16] & 0x1F)
- guard spsLength + 16 <= bytes.count, naluType == 7 else { return nil }
- return Data(bytes[16..<(16 + spsLength)])
- }
- private func parseAvccPPSData() -> Data? {
- guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
- let extraData = parser.readData(for: avcc),
- extraData.count > 16 else { return nil }
- let bytes = [UInt8](extraData)
- var spsCount = Int(bytes[13] & 0x1F)
- let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
- var prefixLength = 16 + spsLength
- while spsCount > 1 {
- guard prefixLength + 2 < bytes.count else { return nil }
- let nextSpsLength = (Int(bytes[prefixLength]) << 8) + Int(bytes[prefixLength + 1])
- prefixLength += nextSpsLength
- spsCount -= 1
- }
- guard prefixLength + 3 < bytes.count else { return nil }
- let ppsLength = (Int(bytes[prefixLength + 1]) << 8) + Int(bytes[prefixLength + 2])
- let naluType = Int(bytes[prefixLength + 3] & 0x1F)
- guard naluType == 8, ppsLength + prefixLength + 3 <= bytes.count else { return nil }
- return Data(bytes[(prefixLength + 3)..<(prefixLength + 3 + ppsLength)])
- }
- private func readPicWidth() -> Int {
- guard videoCodecID != .unknown,
- let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
- let fileHandle = parser.fileHandle else { return 0 }
- fileHandle.seek(toFileOffset: box.startIndexInBytes + 32)
- let widthData = fileHandle.readData(ofLength: 2)
- guard widthData.count >= 2 else { return 0 }
- let bytes = [UInt8](widthData)
- return (Int(bytes[0]) << 8) + Int(bytes[1])
- }
- private func readPicHeight() -> Int {
- guard videoCodecID != .unknown,
- let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
- let fileHandle = parser.fileHandle else { return 0 }
- fileHandle.seek(toFileOffset: box.startIndexInBytes + 34)
- let heightData = fileHandle.readData(ofLength: 2)
- guard heightData.count >= 2 else { return 0 }
- let bytes = [UInt8](heightData)
- return (Int(bytes[0]) << 8) + Int(bytes[1])
- }
- private func readDuration() -> Double {
- guard let mvhd = rootBox?.subBox(ofType: .mvhd),
- let mvhdData = parser.readData(for: mvhd),
- mvhdData.count > 24 else { return 0 }
- let version = Int(mvhdData.lnU32(at: 8))
- var timeScaleIndex = 20
- var durationIndex = 24
- var durationLength = 4
- if version == 1 {
- timeScaleIndex = 28
- durationIndex = 32
- durationLength = 8
- }
- guard mvhdData.count >= durationIndex + durationLength else { return 0 }
- let scale = Int(mvhdData.lnU32(at: timeScaleIndex))
- let durationValue: Int
- if durationLength == 4 {
- durationValue = Int(mvhdData.lnU32(at: durationIndex))
- } else {
- durationValue = Int(mvhdData.lnU64(at: durationIndex))
- }
- guard scale != 0 else { return 0 }
- return Double(durationValue) / Double(scale)
- }
- public func mp4FileDidFinishParse(_ parser: LNMP4Parser) {}
- public func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser) {
- switch box.type {
- case .hdlr:
- if let hdlr = box as? LNMP4HdlrBox,
- let trackBox = box.superBox(ofType: .trak) as? LNMP4TrackBox {
- switch hdlr.trackType {
- case .video:
- videoTrackBox = trackBox
- case .audio:
- audioTrackBox = trackBox
- default:
- break
- }
- }
- case .avc1:
- videoCodecID = .h264
- case .hvc1:
- videoCodecID = .h265
- default:
- break
- }
- }
- }
|