Files
upstream-mirror 8acb1a3a29
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:06:07 +05:00

407 lines
9.2 KiB
Go

// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package ivfwriter implements IVF media container writer
package ivfwriter
import (
"encoding/binary"
"errors"
"io"
"os"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/rtp/codecs/av1/obu"
)
var (
errFileNotOpened = errors.New("file not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
errCodecUnset = errors.New("codec is unset")
errCodecAlreadySet = errors.New("codec is already set")
errNoSuchCodec = errors.New("no codec for this MimeType")
errInvalidMediaTimebase = errors.New("invalid media timebase")
)
type (
codec int
// IVFWriter is used to take RTP packets and write them to an IVF on disk.
IVFWriter struct {
ioWriter io.Writer
count uint64
seenKeyFrame bool
codec codec
timebaseDenominator uint32
timebaseNumerator uint32
firstFrameTimestamp uint32
clockRate uint64
videoWidth uint16
videoHeight uint16
directPTS bool
// VP8, VP9
currentFrame []byte
// AV1
av1Depacketizer *codecs.AV1Depacketizer
}
)
const (
codecUnset codec = iota
codecVP8
codecVP9
codecAV1
mimeTypeVP8 = "video/VP8"
mimeTypeVP9 = "video/VP9"
mimeTypeAV1 = "video/AV1"
)
// New builds a new IVF writer.
func New(fileName string, opts ...Option) (*IVFWriter, error) {
file, err := os.Create(fileName) //nolint:gosec
if err != nil {
return nil, err
}
writer, err := NewWith(file, opts...)
if err != nil {
return nil, err
}
writer.ioWriter = file
return writer, nil
}
// NewWith initialize a new IVF writer with an io.Writer output.
func NewWith(out io.Writer, opts ...Option) (*IVFWriter, error) {
if out == nil {
return nil, errFileNotOpened
}
writer := &IVFWriter{
ioWriter: out,
seenKeyFrame: false,
timebaseDenominator: 30,
timebaseNumerator: 1,
clockRate: 90000,
videoWidth: 640,
videoHeight: 480,
}
for _, o := range opts {
if err := o(writer); err != nil {
return nil, err
}
}
if writer.codec == codecUnset {
writer.codec = codecVP8
}
if err := writer.writeHeader(); err != nil {
return nil, err
}
if writer.timebaseDenominator == 0 {
return nil, errInvalidMediaTimebase
}
return writer, nil
}
func (i *IVFWriter) writeHeader() error {
header := make([]byte, 32)
copy(header[0:], "DKIF") // DKIF
binary.LittleEndian.PutUint16(header[4:], 0) // Version
binary.LittleEndian.PutUint16(header[6:], 32) // Header size
// FOURCC
switch i.codec {
case codecVP8:
copy(header[8:], "VP80")
case codecVP9:
copy(header[8:], "VP90")
case codecAV1:
copy(header[8:], "AV01")
default:
return errCodecUnset
}
binary.LittleEndian.PutUint16(header[12:], i.videoWidth) // Width in pixels
binary.LittleEndian.PutUint16(header[14:], i.videoHeight) // Height in pixels
binary.LittleEndian.PutUint32(header[16:], i.timebaseDenominator) // Framerate denominator
binary.LittleEndian.PutUint32(header[20:], i.timebaseNumerator) // Framerate numerator
binary.LittleEndian.PutUint32(header[24:], 900) // Frame count, will be updated on first Close() call
binary.LittleEndian.PutUint32(header[28:], 0) // Unused
_, err := i.ioWriter.Write(header)
return err
}
func (i *IVFWriter) timestampToPts(timestamp uint64) uint64 {
return timestamp * uint64(i.timebaseNumerator) / uint64(i.timebaseDenominator)
}
func (i *IVFWriter) writeFrame(frame []byte, timestamp uint64) error {
frameHeader := make([]byte, 12)
//nolint:gosec // G115
binary.LittleEndian.PutUint32(frameHeader[0:], uint32(len(frame))) // Frame length
var pts uint64
if i.directPTS {
// Direct PTS mode: use timestamp directly as PTS.
pts = timestamp
} else {
// Existing behavior: convert using timebase.
pts = i.timestampToPts(timestamp)
}
binary.LittleEndian.PutUint64(frameHeader[4:], pts) // PTS
i.count++
if _, err := i.ioWriter.Write(frameHeader); err != nil {
return err
}
_, err := i.ioWriter.Write(frame)
return err
}
// WriteRTP adds a new packet and writes the appropriate headers for it.
func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error {
if i.ioWriter == nil {
return errFileNotOpened
} else if len(packet.Payload) == 0 {
return nil
}
if i.count == 0 {
i.firstFrameTimestamp = packet.Timestamp
}
var timestamp uint64
if i.directPTS {
// Direct PTS mode: use RTP timestamp directly (no millisecond conversion).
timestamp = uint64(packet.Timestamp - i.firstFrameTimestamp)
} else {
// Existing behavior: convert to milliseconds first.
timestamp = 1000 * uint64(packet.Timestamp-i.firstFrameTimestamp) / i.clockRate
}
switch i.codec {
case codecVP8:
return i.writeVP8(packet, timestamp)
case codecVP9:
return i.writeVP9(packet, timestamp)
case codecAV1:
return i.writeAV1(packet, timestamp)
default:
return errCodecUnset
}
}
func (i *IVFWriter) writeVP8(packet *rtp.Packet, timestamp uint64) error {
vp8Packet := codecs.VP8Packet{}
if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil {
return err
}
// a payload that carries only the VP8 descriptor has nothing to write.
if len(vp8Packet.Payload) == 0 {
return nil
}
isKeyFrame := (vp8Packet.Payload[0] & 0x01) == 0
switch {
case !i.seenKeyFrame && !isKeyFrame:
return nil
case i.currentFrame == nil && vp8Packet.S != 1:
return nil
}
i.seenKeyFrame = true
i.currentFrame = append(i.currentFrame, vp8Packet.Payload[0:]...)
if !packet.Marker {
return nil
} else if len(i.currentFrame) == 0 {
return nil
}
if err := i.writeFrame(i.currentFrame, timestamp); err != nil {
return err
}
i.currentFrame = nil
return nil
}
func (i *IVFWriter) writeVP9(packet *rtp.Packet, timestamp uint64) error {
vp9Packet := codecs.VP9Packet{}
if _, err := vp9Packet.Unmarshal(packet.Payload); err != nil {
return err
}
switch {
case !i.seenKeyFrame && vp9Packet.P:
return nil
case i.currentFrame == nil && !vp9Packet.B:
return nil
}
i.seenKeyFrame = true
i.currentFrame = append(i.currentFrame, vp9Packet.Payload[0:]...)
if !packet.Marker {
return nil
} else if len(i.currentFrame) == 0 {
return nil
}
// the timestamp must be sequential. webrtc mandates a clock rate of 90000
// and we've assumed 30fps in the header.
if err := i.writeFrame(i.currentFrame, timestamp); err != nil {
return err
}
i.currentFrame = nil
return nil
}
func (i *IVFWriter) writeAV1(packet *rtp.Packet, timestamp uint64) error {
if i.av1Depacketizer == nil {
i.av1Depacketizer = &codecs.AV1Depacketizer{}
}
payload, err := i.av1Depacketizer.Unmarshal(packet.Payload)
if err != nil {
return err
}
if !i.seenKeyFrame {
isKeyFrame := i.av1Depacketizer.N || (len(payload) > 0 && obu.Type((payload[0]&0x78)>>3) == obu.OBUSequenceHeader)
if !isKeyFrame {
return nil
}
i.seenKeyFrame = true
}
i.currentFrame = append(i.currentFrame, payload...)
if !packet.Marker {
return nil
}
delimiter := obu.Header{
Type: obu.OBUTemporalDelimiter,
HasSizeField: true,
}
frame := append(delimiter.Marshal(), 0)
frame = append(frame, i.currentFrame...)
if err := i.writeFrame(frame, timestamp); err != nil {
return err
}
i.currentFrame = nil
return nil
}
// Close stops the recording.
func (i *IVFWriter) Close() error {
if i.ioWriter == nil {
// Returns no error as it may be convenient to call
// Close() multiple times
return nil
}
defer func() {
i.ioWriter = nil
}()
if ws, ok := i.ioWriter.(io.WriteSeeker); ok {
// Update the framecount
if _, err := ws.Seek(24, 0); err != nil {
return err
}
buff := make([]byte, 4)
binary.LittleEndian.PutUint32(buff, uint32(i.count)) //nolint:gosec // G115
if _, err := ws.Write(buff); err != nil {
return err
}
}
if closer, ok := i.ioWriter.(io.Closer); ok {
return closer.Close()
}
return nil
}
// An Option configures a SampleBuilder.
type Option func(i *IVFWriter) error
// WithCodec configures if IVFWriter is writing AV1 or VP8 packets to disk.
func WithCodec(mimeType string) Option {
return func(i *IVFWriter) error {
if i.codec != codecUnset {
return errCodecAlreadySet
}
switch mimeType {
case mimeTypeVP8:
i.codec = codecVP8
case mimeTypeVP9:
i.codec = codecVP9
case mimeTypeAV1:
i.codec = codecAV1
default:
return errNoSuchCodec
}
return nil
}
}
func WithWidthAndHeight(width, height uint16) Option {
return func(i *IVFWriter) error {
i.videoWidth = width
i.videoHeight = height
return nil
}
}
func WithFrameRate(numerator, denominator uint32) Option {
return func(i *IVFWriter) error {
i.timebaseNumerator = numerator
i.timebaseDenominator = denominator
return nil
}
}
// WithDirectPTS enables direct use of RTP timestamps as PTS values
// without millisecond conversion.
//
// When this option is used, RTP timestamps are written directly as PTS values,
// preserving full timestamp precision. Use WithFrameRate to set the appropriate
// timebase (e.g., WithFrameRate(1, 90000) for standard 90kHz RTP clock).
//
// Example usage for standard RTP video (90kHz clock rate):
//
// NewWith(file, WithFrameRate(1, 90000), WithDirectPTS())
func WithDirectPTS() Option {
return func(i *IVFWriter) error {
i.directPTS = true
return nil
}
}