Add utilities/ivfreader from 804a12fed3/pkg/media/ivfreader
This commit is contained in:
parent
7f00890636
commit
e447e95974
11
go.mod
11
go.mod
|
@ -2,4 +2,13 @@ module git.gammaspectra.live/S.O.N.G/Ignite
|
|||
|
||||
go 1.19
|
||||
|
||||
require golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
|
||||
require (
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
21
go.sum
21
go.sum
|
@ -1,2 +1,19 @@
|
|||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
|
||||
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
21
utilities/ivfreader/LICENSE
Normal file
21
utilities/ivfreader/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018
|
||||
|
||||
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.
|
3
utilities/ivfreader/README.md
Normal file
3
utilities/ivfreader/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ivfreader
|
||||
|
||||
Taken from [github.com/pion/webrtc](https://github.com/pion/webrtc/tree/804a12fed3ee45637aa08fd70023cfb2a44b651a/pkg/media/ivfreader) implementation.
|
145
utilities/ivfreader/ivfreader.go
Normal file
145
utilities/ivfreader/ivfreader.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
// Package ivfreader implements IVF media container reader
|
||||
package ivfreader
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
ivfFileHeaderSignature = "DKIF"
|
||||
ivfFileHeaderSize = 32
|
||||
ivfFrameHeaderSize = 12
|
||||
)
|
||||
|
||||
var (
|
||||
errNilStream = errors.New("stream is nil")
|
||||
errIncompleteFrameHeader = errors.New("incomplete frame header")
|
||||
errIncompleteFrameData = errors.New("incomplete frame data")
|
||||
errIncompleteFileHeader = errors.New("incomplete file header")
|
||||
errSignatureMismatch = errors.New("IVF signature mismatch")
|
||||
errUnknownIVFVersion = errors.New("IVF version unknown, parser may not parse correctly")
|
||||
)
|
||||
|
||||
// IVFFileHeader 32-byte header for IVF files
|
||||
// https://wiki.multimedia.cx/index.php/IVF
|
||||
type IVFFileHeader struct {
|
||||
signature string // 0-3
|
||||
version uint16 // 4-5
|
||||
headerSize uint16 // 6-7
|
||||
FourCC string // 8-11
|
||||
Width uint16 // 12-13
|
||||
Height uint16 // 14-15
|
||||
TimebaseDenominator uint32 // 16-19
|
||||
TimebaseNumerator uint32 // 20-23
|
||||
NumFrames uint32 // 24-27
|
||||
unused uint32 // 28-31
|
||||
}
|
||||
|
||||
// IVFFrameHeader 12-byte header for IVF frames
|
||||
// https://wiki.multimedia.cx/index.php/IVF
|
||||
type IVFFrameHeader struct {
|
||||
FrameSize uint32 // 0-3
|
||||
Timestamp uint64 // 4-11
|
||||
}
|
||||
|
||||
// IVFReader is used to read IVF files and return frame payloads
|
||||
type IVFReader struct {
|
||||
stream io.Reader
|
||||
bytesReadSuccesfully int64
|
||||
}
|
||||
|
||||
// NewWith returns a new IVF reader and IVF file header
|
||||
// with an io.Reader input
|
||||
func NewWith(in io.Reader) (*IVFReader, *IVFFileHeader, error) {
|
||||
if in == nil {
|
||||
return nil, nil, errNilStream
|
||||
}
|
||||
|
||||
reader := &IVFReader{
|
||||
stream: in,
|
||||
}
|
||||
|
||||
header, err := reader.parseFileHeader()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return reader, header, nil
|
||||
}
|
||||
|
||||
// ResetReader resets the internal stream of IVFReader. This is useful
|
||||
// for live streams, where the end of the file might be read without the
|
||||
// data being finished.
|
||||
func (i *IVFReader) ResetReader(reset func(bytesRead int64) io.Reader) {
|
||||
i.stream = reset(i.bytesReadSuccesfully)
|
||||
}
|
||||
|
||||
// ParseNextFrame reads from stream and returns IVF frame payload, header,
|
||||
// and an error if there is incomplete frame data.
|
||||
// Returns all nil values when no more frames are available.
|
||||
func (i *IVFReader) ParseNextFrame() ([]byte, *IVFFrameHeader, error) {
|
||||
buffer := make([]byte, ivfFrameHeaderSize)
|
||||
var header *IVFFrameHeader
|
||||
|
||||
bytesRead, err := io.ReadFull(i.stream, buffer)
|
||||
headerBytesRead := bytesRead
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil, nil, errIncompleteFrameHeader
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
header = &IVFFrameHeader{
|
||||
FrameSize: binary.LittleEndian.Uint32(buffer[:4]),
|
||||
Timestamp: binary.LittleEndian.Uint64(buffer[4:12]),
|
||||
}
|
||||
|
||||
payload := make([]byte, header.FrameSize)
|
||||
bytesRead, err = io.ReadFull(i.stream, payload)
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil, nil, errIncompleteFrameData
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
i.bytesReadSuccesfully += int64(headerBytesRead) + int64(bytesRead)
|
||||
return payload, header, nil
|
||||
}
|
||||
|
||||
// parseFileHeader reads 32 bytes from stream and returns
|
||||
// IVF file header. This is always called before ParseNextFrame()
|
||||
func (i *IVFReader) parseFileHeader() (*IVFFileHeader, error) {
|
||||
buffer := make([]byte, ivfFileHeaderSize)
|
||||
|
||||
bytesRead, err := io.ReadFull(i.stream, buffer)
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil, errIncompleteFileHeader
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := &IVFFileHeader{
|
||||
signature: string(buffer[:4]),
|
||||
version: binary.LittleEndian.Uint16(buffer[4:6]),
|
||||
headerSize: binary.LittleEndian.Uint16(buffer[6:8]),
|
||||
FourCC: string(buffer[8:12]),
|
||||
Width: binary.LittleEndian.Uint16(buffer[12:14]),
|
||||
Height: binary.LittleEndian.Uint16(buffer[14:16]),
|
||||
TimebaseDenominator: binary.LittleEndian.Uint32(buffer[16:20]),
|
||||
TimebaseNumerator: binary.LittleEndian.Uint32(buffer[20:24]),
|
||||
NumFrames: binary.LittleEndian.Uint32(buffer[24:28]),
|
||||
unused: binary.LittleEndian.Uint32(buffer[28:32]),
|
||||
}
|
||||
|
||||
if header.signature != ivfFileHeaderSignature {
|
||||
return nil, errSignatureMismatch
|
||||
} else if header.version != uint16(0) {
|
||||
return nil, fmt.Errorf("%w: expected(0) got(%d)", errUnknownIVFVersion, header.version)
|
||||
}
|
||||
|
||||
i.bytesReadSuccesfully += int64(bytesRead)
|
||||
return header, nil
|
||||
}
|
167
utilities/ivfreader/ivfreader_test.go
Normal file
167
utilities/ivfreader/ivfreader_test.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
package ivfreader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// buildIVFContainer takes frames and prepends valid IVF file header
|
||||
func buildIVFContainer(frames ...*[]byte) *bytes.Buffer {
|
||||
// Valid IVF file header taken from: https://github.com/webmproject/...
|
||||
// vp8-test-vectors/blob/master/vp80-00-comprehensive-001.ivf
|
||||
// Video Image Width - 176
|
||||
// Video Image Height - 144
|
||||
// Frame Rate Rate - 30000
|
||||
// Frame Rate Scale - 1000
|
||||
// Video Length in Frames - 29
|
||||
// BitRate: 64.01 kb/s
|
||||
ivf := []byte{
|
||||
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00,
|
||||
0x56, 0x50, 0x38, 0x30, 0xb0, 0x00, 0x90, 0x00,
|
||||
0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00,
|
||||
0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
for f := range frames {
|
||||
ivf = append(ivf, *frames[f]...)
|
||||
}
|
||||
|
||||
return bytes.NewBuffer(ivf)
|
||||
}
|
||||
|
||||
func TestIVFReader_ParseValidFileHeader(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ivf := buildIVFContainer(&[]byte{})
|
||||
|
||||
reader, header, err := NewWith(ivf)
|
||||
assert.Nil(err, "IVFReader should be created")
|
||||
assert.NotNil(reader, "Reader shouldn't be nil")
|
||||
assert.NotNil(header, "Header shouldn't be nil")
|
||||
|
||||
assert.Equal("DKIF", header.signature, "signature is 'DKIF'")
|
||||
assert.Equal(uint16(0), header.version, "version should be 0")
|
||||
assert.Equal("VP80", header.FourCC, "FourCC should be 'VP80'")
|
||||
assert.Equal(uint16(176), header.Width, "width should be 176")
|
||||
assert.Equal(uint16(144), header.Height, "height should be 144")
|
||||
assert.Equal(uint32(30000), header.TimebaseDenominator, "timebase denominator should be 30000")
|
||||
assert.Equal(uint32(1000), header.TimebaseNumerator, "timebase numerator should be 1000")
|
||||
assert.Equal(uint32(29), header.NumFrames, "number of frames should be 29")
|
||||
assert.Equal(uint32(0), header.unused, "bytes should be unused")
|
||||
}
|
||||
|
||||
func TestIVFReader_ParseValidFrames(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Frame Length - 4
|
||||
// Timestamp - None
|
||||
// Frame Payload - 0xDEADBEEF
|
||||
validFrame1 := []byte{
|
||||
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF,
|
||||
}
|
||||
|
||||
// Frame Length - 12
|
||||
// Timestamp - None
|
||||
// Frame Payload - 0xDEADBEEFDEADBEEF
|
||||
validFrame2 := []byte{
|
||||
0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF,
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
|
||||
}
|
||||
|
||||
ivf := buildIVFContainer(&validFrame1, &validFrame2)
|
||||
reader, _, err := NewWith(ivf)
|
||||
assert.Nil(err, "IVFReader should be created")
|
||||
assert.NotNil(reader, "Reader shouldn't be nil")
|
||||
|
||||
// Parse Frame #1
|
||||
payload, header, err := reader.ParseNextFrame()
|
||||
|
||||
assert.Nil(err, "Should have parsed frame #1 without error")
|
||||
assert.Equal(uint32(4), header.FrameSize, "Frame header frameSize should be 4")
|
||||
assert.Equal(4, len(payload), "Payload should be length 4")
|
||||
assert.Equal(
|
||||
payload,
|
||||
[]byte{
|
||||
0xDE, 0xAD, 0xBE, 0xEF,
|
||||
},
|
||||
"Payload value should be 0xDEADBEEF")
|
||||
assert.Equal(int64(ivfFrameHeaderSize+ivfFileHeaderSize+header.FrameSize), reader.bytesReadSuccesfully)
|
||||
previousBytesRead := reader.bytesReadSuccesfully
|
||||
|
||||
// Parse Frame #2
|
||||
payload, header, err = reader.ParseNextFrame()
|
||||
|
||||
assert.Nil(err, "Should have parsed frame #2 without error")
|
||||
assert.Equal(uint32(12), header.FrameSize, "Frame header frameSize should be 4")
|
||||
assert.Equal(12, len(payload), "Payload should be length 12")
|
||||
assert.Equal(
|
||||
payload,
|
||||
[]byte{
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD,
|
||||
0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
|
||||
},
|
||||
"Payload value should be 0xDEADBEEFDEADBEEF")
|
||||
assert.Equal(int64(ivfFrameHeaderSize+header.FrameSize)+previousBytesRead, reader.bytesReadSuccesfully)
|
||||
}
|
||||
|
||||
func TestIVFReader_ParseIncompleteFrameHeader(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// frame with 11-byte header (missing 1 byte)
|
||||
incompleteFrame := []byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
ivf := buildIVFContainer(&incompleteFrame)
|
||||
reader, _, err := NewWith(ivf)
|
||||
assert.Nil(err, "IVFReader should be created")
|
||||
assert.NotNil(reader, "Reader shouldn't be nil")
|
||||
|
||||
// Parse Frame #1
|
||||
payload, header, err := reader.ParseNextFrame()
|
||||
|
||||
assert.Nil(payload, "Payload should be nil")
|
||||
assert.Nil(header, "Incomplete header should be nil")
|
||||
assert.Equal(errIncompleteFrameHeader, err)
|
||||
}
|
||||
|
||||
func TestIVFReader_ParseIncompleteFramePayload(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// frame with header defining frameSize of 4
|
||||
// but only 2 bytes available (missing 2 bytes)
|
||||
incompleteFrame := []byte{
|
||||
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD,
|
||||
}
|
||||
|
||||
ivf := buildIVFContainer(&incompleteFrame)
|
||||
reader, _, err := NewWith(ivf)
|
||||
assert.Nil(err, "IVFReader should be created")
|
||||
assert.NotNil(reader, "Reader shouldn't be nil")
|
||||
|
||||
// Parse Frame #1
|
||||
payload, header, err := reader.ParseNextFrame()
|
||||
|
||||
assert.Nil(payload, "Incomplete payload should be nil")
|
||||
assert.Nil(header, "Header should be nil")
|
||||
assert.Equal(errIncompleteFrameData, err)
|
||||
}
|
||||
|
||||
func TestIVFReader_EOFWhenNoFramesLeft(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ivf := buildIVFContainer(&[]byte{})
|
||||
reader, _, err := NewWith(ivf)
|
||||
assert.Nil(err, "IVFReader should be created")
|
||||
assert.NotNil(reader, "Reader shouldn't be nil")
|
||||
|
||||
_, _, err = reader.ParseNextFrame()
|
||||
|
||||
assert.Equal(io.EOF, err)
|
||||
}
|
Loading…
Reference in a new issue