@@ -181,6 +181,54 @@ public function testCodecChangeStartsANewSegment(): void
181181 $ this ->assertSame (1 , self ::describe ($ this ->segment (1 , 'video ' ))['frames ' ]);
182182 }
183183
184+ /** A 32x32 H.264 keyframe (SPS, PPS and IDR slice, with start codes) and the inter frame after it. */
185+ private const H264_KEYFRAME = '000000016742c00ad9096c0440000003004000000f03c489920000000168cb83cb200000016588840af118a00024031c00043ae3800097c49d75e0 ' ;
186+ private const H264_INTER = '00000001419a3813e580 ' ;
187+
188+ /**
189+ * @return list<array{codec: string, keyframe: bool}>
190+ */
191+ private static function frames (string $ path ): array
192+ {
193+ $ frames = [];
194+ foreach ((new Matroska (new LocalFile ($ path ), null ))->frames as $ frame ) {
195+ $ frames [] = ['codec ' => $ frame ['codec ' ], 'keyframe ' => $ frame ['keyframe ' ]];
196+ }
197+ return $ frames ;
198+ }
199+
200+ /**
201+ * A sender can switch codec on the same SSRC (as MadelineProto itself does in group calls, when the next file
202+ * has another video codec): the codec of the payload type of the frames is what tells, as an inter frame of one
203+ * codec can look like another's. The frames of the new codec go into a new segment, parsed as what they are.
204+ */
205+ public function testCodecChangeOnTheSameSourceStartsANewSegment (): void
206+ {
207+ $ recorder = CallRecorder ::series (new LocalFile ($ this ->base ), RecordingFormat::Mkv);
208+ $ recorder ->setExpected (audio: false , video: true , presentation: false );
209+ $ recorder ->pushVideoFrame (self ::vp9 (true ), 0 , 1 , codecId: 'V_VP9 ' );
210+ $ recorder ->pushVideoFrame (self ::vp9 (false ), 3000 , 1 , codecId: 'V_VP9 ' );
211+ $ recorder ->pushVideoFrame (hex2bin (self ::H264_KEYFRAME ), 6000 , 1 , codecId: 'V_MPEG4/ISO/AVC ' );
212+ $ this ->assertFileExists ($ this ->segment (1 , 'video ' ));
213+ $ recorder ->pushVideoFrame (hex2bin (self ::H264_INTER ), 9000 , 1 , codecId: 'V_MPEG4/ISO/AVC ' );
214+ $ recorder ->close ();
215+ $ this ->assertSame ([['codec ' => 'V_VP9 ' , 'keyframe ' => true ], ['codec ' => 'V_VP9 ' , 'keyframe ' => false ]], self ::frames ($ this ->segment (0 , 'video ' )));
216+ $ this ->assertSame ([['codec ' => 'V_MPEG4/ISO/AVC ' , 'keyframe ' => true ], ['codec ' => 'V_MPEG4/ISO/AVC ' , 'keyframe ' => false ]], self ::frames ($ this ->segment (1 , 'video ' )));
217+ }
218+
219+ /** A single-file recording can't change codec either: it's finished, rather than given frames of another codec. */
220+ public function testCodecChangeOnTheSameSourceFinishesAFixedRecording (): void
221+ {
222+ $ recorder = CallRecorder ::fixed (new LocalFile ($ this ->base ), RecordingFormat::Mkv, CallStream::VIDEO );
223+ $ recorder ->setExpected (audio: false , video: true , presentation: false );
224+ $ recorder ->pushVideoFrame (self ::vp9 (true ), 0 , 1 , codecId: 'V_VP9 ' );
225+ $ recorder ->pushVideoFrame (self ::vp9 (false ), 3000 , 1 , codecId: 'V_VP9 ' );
226+ $ recorder ->pushVideoFrame (hex2bin (self ::H264_KEYFRAME ), 6000 , 1 , codecId: 'V_MPEG4/ISO/AVC ' );
227+ $ recorder ->pushVideoFrame (hex2bin (self ::H264_INTER ), 9000 , 1 , codecId: 'V_MPEG4/ISO/AVC ' );
228+ $ recorder ->close ();
229+ $ this ->assertSame ([['codec ' => 'V_VP9 ' , 'keyframe ' => true ], ['codec ' => 'V_VP9 ' , 'keyframe ' => false ]], self ::frames ($ this ->base ));
230+ }
231+
184232 public function testDirectoryStemAndNothingRecorded (): void
185233 {
186234 $ dir = sys_get_temp_dir ().'/rec_ ' .bin2hex (random_bytes (4 )).'/ ' ;
0 commit comments