github.com/pion/webrtc/v3@v3.2.24/pkg/media/ivfwriter/ivfwriter.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 // Package ivfwriter implements IVF media container writer 5 package ivfwriter 6 7 import ( 8 "encoding/binary" 9 "errors" 10 "io" 11 "os" 12 13 "github.com/pion/rtp" 14 "github.com/pion/rtp/codecs" 15 "github.com/pion/rtp/pkg/frame" 16 ) 17 18 var ( 19 errFileNotOpened = errors.New("file not opened") 20 errInvalidNilPacket = errors.New("invalid nil packet") 21 errCodecAlreadySet = errors.New("codec is already set") 22 errNoSuchCodec = errors.New("no codec for this MimeType") 23 ) 24 25 const ( 26 mimeTypeVP8 = "video/VP8" 27 mimeTypeAV1 = "video/AV1" 28 29 ivfFileHeaderSignature = "DKIF" 30 ) 31 32 // IVFWriter is used to take RTP packets and write them to an IVF on disk 33 type IVFWriter struct { 34 ioWriter io.Writer 35 count uint64 36 seenKeyFrame bool 37 38 isVP8, isAV1 bool 39 40 // VP8 41 currentFrame []byte 42 43 // AV1 44 av1Frame frame.AV1 45 } 46 47 // New builds a new IVF writer 48 func New(fileName string, opts ...Option) (*IVFWriter, error) { 49 f, err := os.Create(fileName) //nolint:gosec 50 if err != nil { 51 return nil, err 52 } 53 writer, err := NewWith(f, opts...) 54 if err != nil { 55 return nil, err 56 } 57 writer.ioWriter = f 58 return writer, nil 59 } 60 61 // NewWith initialize a new IVF writer with an io.Writer output 62 func NewWith(out io.Writer, opts ...Option) (*IVFWriter, error) { 63 if out == nil { 64 return nil, errFileNotOpened 65 } 66 67 writer := &IVFWriter{ 68 ioWriter: out, 69 seenKeyFrame: false, 70 } 71 72 for _, o := range opts { 73 if err := o(writer); err != nil { 74 return nil, err 75 } 76 } 77 78 if !writer.isAV1 && !writer.isVP8 { 79 writer.isVP8 = true 80 } 81 82 if err := writer.writeHeader(); err != nil { 83 return nil, err 84 } 85 return writer, nil 86 } 87 88 func (i *IVFWriter) writeHeader() error { 89 header := make([]byte, 32) 90 copy(header[0:], ivfFileHeaderSignature) // DKIF 91 binary.LittleEndian.PutUint16(header[4:], 0) // Version 92 binary.LittleEndian.PutUint16(header[6:], 32) // Header size 93 94 // FOURCC 95 if i.isVP8 { 96 copy(header[8:], "VP80") 97 } else if i.isAV1 { 98 copy(header[8:], "AV01") 99 } 100 101 binary.LittleEndian.PutUint16(header[12:], 640) // Width in pixels 102 binary.LittleEndian.PutUint16(header[14:], 480) // Height in pixels 103 binary.LittleEndian.PutUint32(header[16:], 30) // Framerate denominator 104 binary.LittleEndian.PutUint32(header[20:], 1) // Framerate numerator 105 binary.LittleEndian.PutUint32(header[24:], 900) // Frame count, will be updated on first Close() call 106 binary.LittleEndian.PutUint32(header[28:], 0) // Unused 107 108 _, err := i.ioWriter.Write(header) 109 return err 110 } 111 112 func (i *IVFWriter) writeFrame(frame []byte) error { 113 frameHeader := make([]byte, 12) 114 binary.LittleEndian.PutUint32(frameHeader[0:], uint32(len(frame))) // Frame length 115 binary.LittleEndian.PutUint64(frameHeader[4:], i.count) // PTS 116 i.count++ 117 118 if _, err := i.ioWriter.Write(frameHeader); err != nil { 119 return err 120 } 121 _, err := i.ioWriter.Write(frame) 122 return err 123 } 124 125 // WriteRTP adds a new packet and writes the appropriate headers for it 126 func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { 127 if i.ioWriter == nil { 128 return errFileNotOpened 129 } else if len(packet.Payload) == 0 { 130 return nil 131 } 132 133 if i.isVP8 { 134 vp8Packet := codecs.VP8Packet{} 135 if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil { 136 return err 137 } 138 139 isKeyFrame := vp8Packet.Payload[0] & 0x01 140 switch { 141 case !i.seenKeyFrame && isKeyFrame == 1: 142 return nil 143 case i.currentFrame == nil && vp8Packet.S != 1: 144 return nil 145 } 146 147 i.seenKeyFrame = true 148 i.currentFrame = append(i.currentFrame, vp8Packet.Payload[0:]...) 149 150 if !packet.Marker { 151 return nil 152 } else if len(i.currentFrame) == 0 { 153 return nil 154 } 155 156 if err := i.writeFrame(i.currentFrame); err != nil { 157 return err 158 } 159 i.currentFrame = nil 160 } else if i.isAV1 { 161 av1Packet := &codecs.AV1Packet{} 162 if _, err := av1Packet.Unmarshal(packet.Payload); err != nil { 163 return err 164 } 165 166 obus, err := i.av1Frame.ReadFrames(av1Packet) 167 if err != nil { 168 return err 169 } 170 171 for j := range obus { 172 if err := i.writeFrame(obus[j]); err != nil { 173 return err 174 } 175 } 176 } 177 178 return nil 179 } 180 181 // Close stops the recording 182 func (i *IVFWriter) Close() error { 183 if i.ioWriter == nil { 184 // Returns no error as it may be convenient to call 185 // Close() multiple times 186 return nil 187 } 188 189 defer func() { 190 i.ioWriter = nil 191 }() 192 193 if ws, ok := i.ioWriter.(io.WriteSeeker); ok { 194 // Update the framecount 195 if _, err := ws.Seek(24, 0); err != nil { 196 return err 197 } 198 buff := make([]byte, 4) 199 binary.LittleEndian.PutUint32(buff, uint32(i.count)) 200 if _, err := ws.Write(buff); err != nil { 201 return err 202 } 203 } 204 205 if closer, ok := i.ioWriter.(io.Closer); ok { 206 return closer.Close() 207 } 208 209 return nil 210 } 211 212 // An Option configures a SampleBuilder. 213 type Option func(i *IVFWriter) error 214 215 // WithCodec configures if IVFWriter is writing AV1 or VP8 packets to disk 216 func WithCodec(mimeType string) Option { 217 return func(i *IVFWriter) error { 218 if i.isVP8 || i.isAV1 { 219 return errCodecAlreadySet 220 } 221 222 switch mimeType { 223 case mimeTypeVP8: 224 i.isVP8 = true 225 case mimeTypeAV1: 226 i.isAV1 = true 227 default: 228 return errNoSuchCodec 229 } 230 231 return nil 232 } 233 }