flvw.bend source
flvw.bend on the hub · documented module
# Frames into an FLV file: the one container here that holds pictures,# their times, and the audio most cameras send (G.711) together.## video H.264 as FLV has it: a configuration tag made of the SPS and# the PPS, then a tag per picture, its NAL units each after# its size. H.265 the same way under enhanced RTMP's header# ("hvc1"), the configuration made of the VPS, SPS and PPS.# audio G.711 as it is (format 7 for the A-law, 8 for the mu-law);# AAC as a configuration tag and a tag per frame, each frame# without the ADTS header it came with.## No picture is written before a key one whose parameter sets are known:# they come from the stream's description, or with the picture.import Baseimport ./bytes.bend as Bimport ./frame.bend as Fimport ./rtmp_core.bend as K# NAL units# ---------def Nal.push(cur: List<&2, U32>, acc: List<&2, List<&2, U32>>) -> List<&2, List<&2, U32>>: match cur: case Nil{}: acc case Con{h, t}: B.Bytes.rev(h <> t) <> accdef Nal.split.go(bs: List<&2, U32>, cur: List<&2, U32>, acc: List<&2, List<&2, U32>>) -> List<&2, List<&2, U32>>: match bs: case Nil{}: List.reverse(&2, List<&2, U32>, Nal.push(cur, acc)) case Con{0, Con{0, Con{0, Con{1, t}}}}: Nal.split.go(t, Nil{}, Nal.push(cur, acc)) case Con{b, t}: Nal.split.go(t, b <> cur, acc)# The units of an Annex B stream: what is between its start codes (00# 00 00 01, which no unit can hold: a unit escapes two zeros in a row).def Nal.split(bs: List<&2, U32>) -> List<&2, List<&2, U32>>: Nal.split.go(bs, Nil{}, Nil{})# A unit's type: 5 bits of its first byte in H.264, 6 in H.265.def Nal.type(h265: Bool, +b: U32) -> U32: match h265: case True{}: U32.and(U32.shrn(b, 1n), 63) case False{}: U32.and(b, 31)# The first unit of a type, or nothing.def Nal.find(nals: List<&2, List<&2, U32>>, +h265: Bool, +want: U32) -> List<&2, U32>: match nals: case Nil{}: Nil{} case Con{Nil{}, rest}: Nal.find(rest, h265, want) case Con{Con{+b, t}, rest}: Bool.pick(List<&2, U32>, U32.is_eq(Nal.type(h265, b), want), b <> t, Nal.find(rest, h265, want))# Units each after its size in 4 bytes, as a tag holds a picture.def Nal.sized(nals: List<&2, List<&2, U32>>) -> List<&2, U32>: match nals: case Nil{}: Nil{} case Con{+n, rest}: B.Bytes.put32(B.Bytes.len(n), B.Bytes.cat(n, Nal.sized(rest)))# A unit after its size in 2 bytes, as a configuration holds a# parameter set.def Nal.set(+n: List<&2, U32>, rest: List<&2, U32>) -> List<&2, U32>: B.Bytes.put16(B.Bytes.len(n), B.Bytes.cat(n, rest))# The first n bytes of a unit with its escapes taken out (a 03 after# two zeros is not data).def Nal.plain(n: Nat, bs: List<&2, U32>, +zeros: U32) -> List<&2, U32>: match n bs: case 1n+p Con{+b, t}: +skip = (zeros >= 2 : U32) && U32.is_eq(b, 3) +rest = Nal.plain(p, t, Bool.pick(U32, skip, 0, Bool.pick(U32, U32.is_zero(b), U32.add(zeros, 1), 0))) Bool.pick(List<&2, U32>, skip, rest, b <> rest) case _ _: Nil{}# Configurations# --------------# H.264's (an AVCDecoderConfigurationRecord): the profile and level out# of the SPS, sizes of 4 bytes, one SPS, one PPS. Nothing when either# is missing.def Conf.avc(+sps: List<&2, U32>, +pps: List<&2, U32>) -> List<&2, U32>: Bool.pick(List<&2, U32>, U32.is_zero(B.Bytes.len(sps)) || U32.is_zero(B.Bytes.len(pps)), Nil{}, 23 <> 0 <> 0 <> 0 <> 0 <> 1 <> B.Bytes.cat(B.Bytes.take(3n, B.Bytes.drop(1n, sps)), 255 <> 225 <> Nal.set(sps, 1 <> Nal.set(pps, Nil{}))))# H.265's (an HEVCDecoderConfigurationRecord) after enhanced RTMP's# header: the 12 bytes of profile, tier and level out of the SPS (they# follow its 2 bytes of header and one of ids), then what a decoder# reads again in the SPS itself, sizes of 4 bytes, and three arrays of# one: VPS (32), SPS (33), PPS (34). Nothing when one is missing.def Conf.hevc(+vps: List<&2, U32>, +sps: List<&2, U32>, +pps: List<&2, U32>) -> List<&2, U32>: Bool.pick(List<&2, U32>, U32.is_zero(B.Bytes.len(vps)) || U32.is_zero(B.Bytes.len(sps)) || U32.is_zero(B.Bytes.len(pps)), Nil{}, 144 <> 104 <> 118 <> 99 <> 49 <> 1 <> B.Bytes.cat(B.Bytes.take(12n, B.Bytes.drop(3n, Nal.plain(24n, sps, 0))), 240 <> 0 <> 252 <> 253 <> 248 <> 248 <> 0 <> 0 <> 15 <> 3 <> 160 <> 0 <> 1 <> Nal.set(vps, 161 <> 0 <> 1 <> Nal.set(sps, 162 <> 0 <> 1 <> Nal.set(pps, Nil{})))))def Conf.video(h265: Bool, +nals: List<&2, List<&2, U32>>) -> List<&2, U32>: match h265: case True{}: Conf.hevc(Nal.find(nals, True{}, 32), Nal.find(nals, True{}, 33), Nal.find(nals, True{}, 34)) case False{}: Conf.avc(Nal.find(nals, False{}, 7), Nal.find(nals, False{}, 8))# The writer# ----------# first: the file's header is still to write. vconf, aconf: the video's# and the audio's configurations were written. audio: its codec. step:# an AAC frame's length in ms. sets: the parameter sets of the stream's# description.type Fw is Data: Fw{first: Bool, h265: Bool, vconf: Bool, aconf: Bool, audio: String, step: U32, sets: List<&2, List<&2, U32>>}# What a frame gave: bytes to write, and the state after.type Put is Data: Put{bytes: List<&2, U32>, w: Fw}def Fw.new(+i: F.Info) -> Fw: Fw{True{}, F.Info.h265(i), False{}, False{}, F.Info.audio(i), U32.div(1024000, U32.max(F.Info.rate(i), 8000)), Nal.split(F.Info.sets(i))}def Fw.head(first: Bool) -> List<&2, U32>: match first: case True{}: K.Flv.header() case False{}: Nil{}# The tag a picture goes in: H.264's header (key or not, a picture, no# time offset) or H.265's (key or not, a picture with no offset, "hvc1").def Fw.picture(h265: Bool, key: Bool, body: List<&2, U32>) -> List<&2, U32>: match h265: case True{}: Bool.pick(U32, key, 147, 163) <> 104 <> 118 <> 99 <> 49 <> body case False{}: Bool.pick(U32, key, 23, 39) <> 1 <> 0 <> 0 <> 0 <> bodydef Fw.conf.tag(conf: List<&2, U32>, ms: U32) -> List<&2, U32>: match conf: case Nil{}: Nil{} case Con{h, t}: K.Flv.tag(9, ms, h <> t)# A picture. The file's video starts at a key picture, after its# configuration (made from the description's parameter sets or the# picture's own): what comes before one a decoder could not use.def Fw.video(w: Fw, +ms: U32, +key: Bool, data: List<&2, U32>) -> Put: Fw{+first, +h265, +vconf, aconf, audio, step, +sets} = w +nals = Nal.split(data) +conf = Bool.pick(List<&2, U32>, vconf, Nil{}, Conf.video(h265, List.append(&2, List<&2, U32>, sets, nals))) +ready = vconf || (key && Bool.not(U32.is_zero(B.Bytes.len(conf)))) Put{Bool.pick(List<&2, U32>, ready, B.Bytes.cat(Fw.head(first), B.Bytes.cat(Fw.conf.tag(conf, ms), K.Flv.tag(9, ms, Fw.picture(h265, key, Nal.sized(nals))))), Nil{}), Fw{first && Bool.not(ready), h265, ready, aconf, audio, step, sets}}# AAC's configuration out of a frame's ADTS header: profile, rate index# and channels, in 2 bytes, in a tag of its own.def Fw.asc(done: Bool, data: List<&2, U32>, ms: U32) -> List<&2, U32>: match done data: case False{} Con{_, Con{_, Con{+b2, Con{b3, _}}}}: K.Flv.tag(8, ms, [175, 0, U32.or(U32.shln(U32.add(U32.and(U32.shrn(b2, 6n), 3), 1), 3n), U32.shrn(U32.and(U32.shrn(b2, 2n), 15), 1n)), U32.or(U32.shln(U32.and(U32.shrn(b2, 2n), 1), 7n), U32.shln(U32.or(U32.shln(U32.and(b2, 1), 2n), U32.shrn(b3, 6n)), 3n))]) case _ _: Nil{}def Fw.aac.cut(c: B.Cut, +ms: U32, acc: List<&2, U32>, k: List<&2, U32> -> List<&2, U32> -> List<&2, U32>) -> List<&2, U32>: match c: case B.Short{}: acc case B.Cut{raw, rest}: k(rest, B.Bytes.cat(acc, K.Flv.tag(8, ms, 175 <> 1 <> raw)))# Each ADTS frame (a 7-byte header that says its size in 13 bits) as a# tag of the frame alone, step ms after the one before.def Fw.aac(fuel: Nat, bs: List<&2, U32>, +ms: U32, +step: U32, acc: List<&2, U32>) -> List<&2, U32>: match fuel bs: case 1n+p Con{_, Con{_, Con{_, Con{b3, Con{b4, Con{b5, Con{_, rest}}}}}}}: Fw.aac.cut(B.Bytes.cut(U32.sub(U32.max(U32.or(U32.or( U32.shln(U32.and(b3, 3), 11n), U32.shln(b4, 3n)), U32.shrn(b5, 5n)), 7), 7), rest), ms, acc, r => a => Fw.aac(p, r, U32.add(ms, step), step, a)) case _ _: accdef Fw.sound(aac: Bool, alaw: Bool, ulaw: Bool, first: Bool, h265: Bool, vconf: Bool, +aconf: Bool, audio: String, +step: U32, sets: List<&2, List<&2, U32>>, +ms: U32, +data: List<&2, U32>) -> Put: match aac alaw ulaw: case True{} _ _: Put{B.Bytes.cat(Fw.head(first), B.Bytes.cat(Fw.asc(aconf, data, ms), Fw.aac(U32.to_nat(B.Bytes.len(data)), data, ms, step, Nil{}))), Fw{False{}, h265, vconf, True{}, audio, step, sets}} case False{} True{} _: Put{B.Bytes.cat(Fw.head(first), K.Flv.tag(8, ms, 114 <> data)), Fw{False{}, h265, vconf, aconf, audio, step, sets}} case False{} False{} True{}: Put{B.Bytes.cat(Fw.head(first), K.Flv.tag(8, ms, 130 <> data)), Fw{False{}, h265, vconf, aconf, audio, step, sets}} case False{} False{} False{}: Put{Nil{}, Fw{first, h265, vconf, aconf, audio, step, sets}}def Fw.audio(w: Fw, ms: U32, data: List<&2, U32>) -> Put: Fw{first, h265, vconf, aconf, +audio, step, sets} = w Fw.sound(String.eq(audio, "AAC"), String.eq(audio, "PCMA"), String.eq(audio, "PCMU"), first, h265, vconf, aconf, audio, step, sets, ms, data)# A frame into the file; its time in ms.def Fw.put(w: Fw, f: F.Frame) -> Put: match f: case F.Video{pts, key, data}: Fw.video(w, U32.div(pts, 90), key, data) case F.Audio{pts, data}: Fw.audio(w, U32.div(pts, 90), data)