diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 1e61122f..016603a2 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -70,8 +70,7 @@ var defaults = map[string]string{ "aac": "-c:a aac", // keep sample rate and channels "aac/16000": "-c:a aac -ar:a 16000 -ac:a 1", "mp3": "-c:a libmp3lame -q:a 8", - "pcm": "-c:a pcm_s16be", - "pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1", + "pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1", "pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1", "pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1", diff --git a/cmd/mp4/ws.go b/cmd/mp4/ws.go index 1ea4d235..8366916c 100644 --- a/cmd/mp4/ws.go +++ b/cmd/mp4/ws.go @@ -110,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) { case mp4.MimeAAC: codec := &core.Codec{Name: core.CodecAAC} audios = append(audios, codec) + case mp4.MimeFlac: + audios = append(audios, + &core.Codec{Name: core.CodecPCMA}, + &core.Codec{Name: core.CodecPCMU}, + &core.Codec{Name: core.CodecPCM}, + ) case mp4.MimeOpus: codec := &core.Codec{Name: core.CodecOpus} audios = append(audios, codec) diff --git a/go.mod b/go.mod index 6a0a8307..beae7f79 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require ( github.com/pion/stun v0.4.0 github.com/pion/webrtc/v3 v3.1.58 github.com/rs/zerolog v1.29.0 + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 + github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.8.2 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index f62d2bc1..20a2e4ee 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 67c6d2cb..f3de809f 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -99,6 +99,14 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { case "8": c.Name = CodecPCMA c.ClockRate = 8000 + case "10": + c.Name = CodecPCM + c.ClockRate = 44100 + c.Channels = 2 + case "11": + c.Name = CodecPCM + c.ClockRate = 44100 + c.Channels = 1 case "14": c.Name = CodecMP3 c.ClockRate = 44100 diff --git a/pkg/core/core.go b/pkg/core/core.go index 1a429d98..72d32b78 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -27,7 +27,8 @@ const ( CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III CodecPCM = "L16" // Linear PCM - CodecELD = "ELD" // AAC-ELD + CodecELD = "ELD" // AAC-ELD + CodecFLAC = "FLAC" CodecAll = "ALL" CodecAny = "ANY" diff --git a/pkg/core/media.go b/pkg/core/media.go index 697e9806..8c15b5b5 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -93,7 +93,7 @@ func GetKind(name string) string { switch name { case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: return KindVideo - case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD: + case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC: return KindAudio } return "" diff --git a/pkg/iso/atoms.go b/pkg/iso/atoms.go index 919e6c22..6a4c9fe7 100644 --- a/pkg/iso/atoms.go +++ b/pkg/iso/atoms.go @@ -32,6 +32,16 @@ const ( Mdat = "mdat" ) +const ( + sampleIsNonSync = 0x10000 + sampleDependsOn1 = 0x1000000 + sampleDependsOn2 = 0x2000000 + + SampleVideoIFrame = sampleDependsOn2 + SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync + SampleAudio = sampleIsNonSync +) + func (m *Movie) WriteFileType() { m.StartAtom(Ftyp) m.WriteString("iso5") @@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann m.EndAtom() // TRAK } -func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) { +func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) { m.StartAtom(Moof) m.StartAtom(MoofMfhd) @@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) TfhdDefaultSampleFlags | TfhdDefaultBaseIsMoof, ) - m.WriteUint32(tid) // track id - m.WriteUint32(duration) // default sample duration - m.WriteUint32(size) // default sample size - m.WriteUint32(0x2000000) // default sample flags + m.WriteUint32(tid) // track id + m.WriteUint32(duration) // default sample duration + m.WriteUint32(size) // default sample size + m.WriteUint32(flags) // default sample flags m.EndAtom() m.StartAtom(MoofTrafTfdt) @@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) { m.StartAtom(Mdat) m.Write(b) m.EndAtom() - } diff --git a/pkg/iso/codecs.go b/pkg/iso/codecs.go index fe1d6093..1ddd28f3 100644 --- a/pkg/iso/codecs.go +++ b/pkg/iso/codecs.go @@ -2,6 +2,7 @@ package iso import ( "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/pcm" ) func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { @@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) { switch codec { case core.CodecAAC, core.CodecMP3: - m.StartAtom("mp4a") + m.StartAtom("mp4a") // supported in all players and browsers + case core.CodecFLAC: + m.StartAtom("fLaC") // supported in all players and browsers case core.CodecOpus: - m.StartAtom("Opus") + m.StartAtom("Opus") // supported in Chrome and Firefox case core.CodecPCMU: m.StartAtom("ulaw") case core.CodecPCMA: @@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con default: panic("unsupported iso audio: " + codec) } + + if channels == 0 { + channels = 1 + } + m.Skip(6) m.WriteUint16(1) // data_reference_index m.Skip(2) // version @@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con m.WriteEsdsAAC(conf) case core.CodecMP3: m.WriteEsdsMP3() + case core.CodecFLAC: + m.StartAtom("dfLa") + m.Write(pcm.FLACHeader(false, sampleRate)) + m.EndAtom() case core.CodecOpus: // don't know what means this magic m.StartAtom("dOps") diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index a720c8ac..13542c58 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/pion/rtp" "sync" ) @@ -54,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media { func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { trackID := byte(len(c.senders)) - handler := core.NewSender(media, track.Codec) + codec := track.Codec.Clone() + handler := core.NewSender(media, codec) switch track.Codec.Name { case core.CodecH264: @@ -112,38 +114,33 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv handler.Handler = h265.RTPDepay(track.Codec, handler.Handler) } - case core.CodecAAC: - handler.Handler = func(packet *rtp.Packet) { - if c.wait != waitNone { - return - } - - c.mu.Lock() - buf := c.muxer.Marshal(trackID, packet) - c.Fire(buf) - c.send += len(buf) - c.mu.Unlock() - } - - if track.Codec.IsRTP() { - handler.Handler = aac.RTPDepay(handler.Handler) - } - - case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: - handler.Handler = func(packet *rtp.Packet) { - if c.wait != waitNone { - return - } - - c.mu.Lock() - buf := c.muxer.Marshal(trackID, packet) - c.Fire(buf) - c.send += len(buf) - c.mu.Unlock() - } - default: - panic("unsupported codec") + handler.Handler = func(packet *rtp.Packet) { + if c.wait != waitNone { + return + } + + c.mu.Lock() + buf := c.muxer.Marshal(trackID, packet) + c.Fire(buf) + c.send += len(buf) + c.mu.Unlock() + } + + switch track.Codec.Name { + case core.CodecAAC: + if track.Codec.IsRTP() { + handler.Handler = aac.RTPDepay(handler.Handler) + } + case core.CodecOpus, core.CodecMP3: // no changes + case core.CodecPCMA, core.CodecPCMU, core.CodecPCM: + handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler) + codec.Name = core.CodecFLAC + + default: + println("ERROR: MP4 unsupported codec: " + track.Codec.Name) + return nil + } } handler.HandleRTP(track) diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 067902a8..485d8fc6 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -15,12 +15,14 @@ type Muxer struct { fragIndex uint32 dts []uint64 pts []uint32 + codecs []*core.Codec } const ( MimeH264 = "avc1.640029" MimeH265 = "hvc1.1.6.L153.B0" MimeAAC = "mp4a.40.2" + MimeFlac = "flac" MimeOpus = "opus" ) @@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string { s += MimeAAC case core.CodecOpus: s += MimeOpus + case core.CodecFLAC: + s += MimeFlac } } @@ -108,14 +112,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b, ) - case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: + case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC: mv.WriteAudioTrack( uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, ) } - m.pts = append(m.pts, 0) m.dts = append(m.dts, 0) + m.pts = append(m.pts, 0) + m.codecs = append(m.codecs, codec) } mv.StartAtom(iso.MoovMvex) @@ -138,28 +143,49 @@ func (m *Muxer) Reset() { } func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte { - // important before increment - time := m.dts[trackID] + codec := m.codecs[trackID] + + duration := packet.Timestamp - m.pts[trackID] + m.pts[trackID] = packet.Timestamp + + // minumum duration important for MSE in Apple Safari + if duration == 0 || duration > codec.ClockRate { + duration = codec.ClockRate/1000 + 1 + m.pts[trackID] += duration + } + + size := len(packet.Payload) + + // flags important for Apple Finder video preview + var flags uint32 + switch codec.Name { + case core.CodecH264: + if h264.IsKeyframe(packet.Payload) { + flags = iso.SampleVideoIFrame + } else { + flags = iso.SampleVideoNonIFrame + } + case core.CodecH265: + if h265.IsKeyframe(packet.Payload) { + flags = iso.SampleVideoIFrame + } else { + flags = iso.SampleVideoNonIFrame + } + default: + flags = iso.SampleAudio // not important + } m.fragIndex++ - var duration uint32 - newTime := packet.Timestamp - if m.pts[trackID] > 0 { - duration = newTime - m.pts[trackID] - m.dts[trackID] += uint64(duration) - } else { - // important, or Safari will fail with first frame - duration = 1 - } - m.pts[trackID] = newTime - - mv := iso.NewMovie(1024 + len(packet.Payload)) + mv := iso.NewMovie(1024 + size) mv.WriteMovieFragment( - m.fragIndex, uint32(trackID+1), duration, - uint32(len(packet.Payload)), time, + m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID], ) mv.WriteData(packet.Payload) + //log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload)) + + m.dts[trackID] += uint64(duration) + return mv.Bytes() } diff --git a/pkg/pcm/flac.go b/pkg/pcm/flac.go new file mode 100644 index 00000000..cd72016d --- /dev/null +++ b/pkg/pcm/flac.go @@ -0,0 +1,138 @@ +// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container: +// - only 1 channel +// - only 16 bit per sample +// - only 8000, 16000, 24000, 48000 sample rate +package pcm + +import ( + "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" + "github.com/sigurn/crc16" + "github.com/sigurn/crc8" + "unicode/utf8" +) + +func FLACHeader(magic bool, sampleRate uint32) []byte { + b := make([]byte, 42) + + if magic { + copy(b, "fLaC") // [0..3] + } + + // https://xiph.org/flac/format.html#metadata_block_header + b[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit) + b[7] = 0x22 // [5..7] blockLength=34 (24 bit) + + // Important for Apple QuickTime player: + // 1. Both values should be same + // 2. Maximum value = 32768 + binary.BigEndian.PutUint16(b[8:], 32768) // [8..9] info.BlockSizeMin=16 (16 bit) + binary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit) + + // [12..14] info.FrameSizeMin=0 (24 bit) + // [15..17] info.FrameSizeMax=0 (24 bit) + + b[18] = byte(sampleRate >> 12) + b[19] = byte(sampleRate >> 4) + b[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit) + + b[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit) + + // [26..41] MD5sum (16 bytes) + + return b +} + +var table8 *crc8.Table +var table16 *crc16.Table + +func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + if codec.Channels >= 2 { + return nil + } + + var sr byte + switch codec.ClockRate { + case 8000: + sr = 0b0100 + case 16000: + sr = 0b0101 + case 24000: + sr = 0b0111 + case 48000: + sr = 0b1010 + default: + return nil + } + + if table8 == nil { + table8 = crc8.MakeTable(crc8.CRC8) + } + if table16 == nil { + table16 = crc16.MakeTable(crc16.CRC16_BUYPASS) + } + + var sampleNumber int32 + + return func(packet *rtp.Packet) { + samples := uint16(len(packet.Payload)) + + if codec.Name == core.CodecPCM { + samples /= 2 + } + + // https://xiph.org/flac/format.html#frame_header + buf := make([]byte, samples*2+30) + + // 1. Frame header + buf[0] = 0xFF + buf[1] = 0xF9 // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit) + buf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit) + buf[3] = 0x08 // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit) + + n := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max + sampleNumber += int32(samples) + + // this is wrong but very simple frame block size value + binary.BigEndian.PutUint16(buf[n:], samples-1) + n += 2 + + buf[n] = crc8.Checksum(buf[:n], table8) + n += 1 + + // 2. Subframe header + buf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit) + n += 1 + + // 3. Subframe + switch codec.Name { + case core.CodecPCMA: + for _, b := range packet.Payload { + s16 := PCMAtoPCM(b) + buf[n] = byte(s16 >> 8) + buf[n+1] = byte(s16) + n += 2 + } + case core.CodecPCMU: + for _, b := range packet.Payload { + s16 := PCMUtoPCM(b) + buf[n] = byte(s16 >> 8) + buf[n+1] = byte(s16) + n += 2 + } + case core.CodecPCM: + n += copy(buf[n:], packet.Payload) + } + + // 4. Frame footer + crc := crc16.Checksum(buf[:n], table16) + binary.BigEndian.PutUint16(buf[n:], crc) + n += 2 + + clone := *packet + clone.Payload = buf[:n] + + handler(&clone) + } +} diff --git a/pkg/pcm/pcma.go b/pkg/pcm/pcma.go new file mode 100644 index 00000000..3e1ef112 --- /dev/null +++ b/pkg/pcm/pcma.go @@ -0,0 +1,53 @@ +// Package pcm +// https://www.codeproject.com/Articles/14237/Using-the-G711-standard +package pcm + +const alawMax = 0x7FFF + +func PCMAtoPCM(alaw byte) int16 { + alaw ^= 0xD5 + + data := int16(((alaw & 0x0F) << 4) + 8) + exponent := (alaw & 0x70) >> 4 + + if exponent != 0 { + data |= 0x100 + } + + if exponent > 1 { + data <<= exponent - 1 + } + + // sign + if alaw&0x80 == 0 { + return data + } else { + return -data + } +} + +func PCMtoPCMA(pcm int16) byte { + var alaw byte + + if pcm < 0 { + pcm = -pcm + alaw = 0x80 + } + + if pcm > alawMax { + pcm = alawMax + } + + exponent := byte(7) + for expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 { + exponent-- + } + + if exponent == 0 { + alaw |= byte(pcm>>4) & 0x0F + } else { + alaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F) + } + + return alaw ^ 0xD5 +} diff --git a/pkg/pcm/pcmu.go b/pkg/pcm/pcmu.go new file mode 100644 index 00000000..954d8a99 --- /dev/null +++ b/pkg/pcm/pcmu.go @@ -0,0 +1,51 @@ +// Package pcm +// https://www.codeproject.com/Articles/14237/Using-the-G711-standard +package pcm + +const bias = 0x84 // 132 or 1000 0100 +const ulawMax = alawMax - bias + +func PCMUtoPCM(ulaw byte) int16 { + ulaw = ^ulaw + + exponent := (ulaw & 0x70) >> 4 + data := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias + + // sign + if ulaw&0x80 == 0 { + return data + } else if data == 0 { + return -1 + } else { + return -data + } +} + +func PCMtoPCMU(pcm int16) byte { + var ulaw byte + + if pcm < 0 { + pcm = -pcm + ulaw = 0x80 + } + + if pcm > ulawMax { + pcm = ulawMax + } + + pcm += bias + + exponent := byte(7) + for expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 { + exponent-- + } + + // mantisa + ulaw |= byte(pcm>>(exponent+3)) & 0x0F + + if exponent > 0 { + ulaw |= exponent << 4 + } + + return ^ulaw +} diff --git a/pkg/pcm/v1/pcm.go b/pkg/pcm/v1/pcm.go new file mode 100644 index 00000000..e1652350 --- /dev/null +++ b/pkg/pcm/v1/pcm.go @@ -0,0 +1,155 @@ +// Package v1 +// http://web.archive.org/web/20110719132013/http://hazelware.luggle.com/tutorials/mulawcompression.html +package v1 + +const cBias = 0x84 +const cClip = 32635 + +var MuLawCompressTable = [256]byte{ + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, +} + +func LinearToMuLawSample(sample int16) byte { + sign := byte(sample>>8) & 0x80 + if sign != 0 { + sample = -sample + } + + if sample > cClip { + sample = cClip + } + sample = sample + cBias + + exponent := MuLawCompressTable[(sample>>7)&0xFF] + mantissa := byte(sample>>(exponent+3)) & 0x0F + + compressedByte := ^(sign | (exponent << 4) | mantissa) + + return compressedByte +} + +var ALawCompressTable = [128]byte{ + 1, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, +} + +func LinearToALawSample(sample int16) byte { + sign := byte((^sample)>>8) & 0x80 + if sign == 0 { + sample = -sample + } + + if sample > cClip { + sample = cClip + } + + var compressedByte byte + if sample >= 256 { + exponent := ALawCompressTable[(sample>>8)&0x7F] + mantissa := byte(sample>>(exponent+3)) & 0x0F + compressedByte = (exponent << 4) | mantissa + } else { + compressedByte = byte(sample >> 4) + } + compressedByte ^= sign ^ 0x55 + return compressedByte +} + +var MuLawDecompressTable = [256]int16{ + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, -1, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0, +} + +var ALawDecompressTable = [256]int16{ + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944, + -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, + -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, + -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848, +} diff --git a/pkg/pcm/v1/pcm_test.go b/pkg/pcm/v1/pcm_test.go new file mode 100644 index 00000000..2db5d95c --- /dev/null +++ b/pkg/pcm/v1/pcm_test.go @@ -0,0 +1,39 @@ +package v1 + +import ( + v2 "github.com/AlexxIT/go2rtc/pkg/pcm" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPCMUtoPCM(t *testing.T) { + for pcmu := byte(0); pcmu < 255; pcmu++ { + pcm1 := MuLawDecompressTable[pcmu] + pcm2 := v2.PCMUtoPCM(pcmu) + require.Equal(t, pcm1, pcm2) + } +} + +func TestPCMAtoPCM(t *testing.T) { + for pcma := byte(0); pcma < 255; pcma++ { + pcm1 := ALawDecompressTable[pcma] + pcm2 := v2.PCMAtoPCM(pcma) + require.Equal(t, pcm1, pcm2) + } +} + +func TestPCMtoPCMU(t *testing.T) { + for pcm := int16(-32768); pcm < 32767; pcm++ { + pcmu1 := LinearToMuLawSample(pcm) + pcmu2 := v2.PCMtoPCMU(pcm) + require.Equal(t, pcmu1, pcmu2) + } +} + +func TestPCMtoPCMA(t *testing.T) { + for pcm := int16(-32768); pcm < 32767; pcm++ { + pcma1 := LinearToALawSample(pcm) + pcma2 := v2.PCMtoPCMA(pcm) + require.Equal(t, pcma1, pcma2) + } +} diff --git a/www/links.html b/www/links.html index 6faa4637..94f5e4df 100644 --- a/www/links.html +++ b/www/links.html @@ -67,9 +67,9 @@

H264/H265 source

  • stream.html WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari
  • -
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC / +OPUS in Chrome and Firefox
  • +
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox
  • stream.mp4 MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC
  • -
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMU, PCMA
  • +
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM
  • frame.mp4 snapshot in MP4-format / browsers: all / codecs: H264, H265*
  • stream.m3u8 HLS/TS / browsers: Safari all, Chrome Android / codecs: H264
  • stream.m3u8 HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC
  • diff --git a/www/video-rtc.js b/www/video-rtc.js index 445aa94d..11ec30be 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -27,7 +27,8 @@ export class VideoRTC extends HTMLElement { "hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra) "mp4a.40.2", // AAC LC "mp4a.40.5", // AAC HE - "opus", // OPUS Chrome + "flac", // FLAC (PCM compatible) + "opus", // OPUS Chrome, Firefox ]; /**