2121import static com .amazonaws .kinesisvideo .producer .FrameFlags .FRAME_FLAG_KEY_FRAME ;
2222import static com .amazonaws .kinesisvideo .producer .FrameFlags .FRAME_FLAG_NONE ;
2323import static com .amazonaws .kinesisvideo .producer .Time .HUNDREDS_OF_NANOS_IN_A_MILLISECOND ;
24+ import static com .amazonaws .kinesisvideo .producer .Time .NANOS_IN_A_MILLISECOND ;
2425
2526/**
2627 * Frame source backed by local image files.
@@ -40,11 +41,13 @@ public class ImageFrameSource {
4041 private final Log log = LogFactory .getLog (ImageFrameSource .class );
4142 private final String metadataName = "ImageLoop" ;
4243 private int metadataCount = 0 ;
44+ private long currentFrameTimestampMs ;
4345
4446 public ImageFrameSource (final ImageFileMediaSourceConfiguration configuration ) {
4547 this .configuration = configuration ;
4648 this .totalFiles = getTotalFiles (configuration .getStartFileIndex (), configuration .getEndFileIndex ());
4749 this .fps = configuration .getFps ();
50+ this .currentFrameTimestampMs = configuration .getStartTimeMs ();
4851 }
4952
5053 private int getTotalFiles (final int startIndex , final int endIndex ) {
@@ -84,20 +87,29 @@ public void run() {
8487 }
8588
8689 private void generateFrameAndNotifyListener () throws KinesisVideoException {
90+ final double frameDurationMs = (double ) Duration .ofSeconds (1L ).toMillis () / fps ;
91+ long nextFrameTimeNs = System .nanoTime (); // to prevent time drift
92+
8793 while (isRunning ) {
8894 if (mkvDataAvailableCallback != null ) {
89- mkvDataAvailableCallback .onFrameDataAvailable (createKinesisVideoFrameFromImage (frameCounter ));
95+ mkvDataAvailableCallback .onFrameDataAvailable (createKinesisVideoFrameFromImage (frameCounter , currentFrameTimestampMs ));
9096 if (isMetadataReady ()) {
9197 mkvDataAvailableCallback .onFragmentMetadataAvailable (metadataName + metadataCount ,
9298 Integer .toString (metadataCount ++), false );
9399 }
94100 }
95101
96102 frameCounter ++;
97- try {
98- Thread .sleep (Duration .ofSeconds (1L ).toMillis () / fps );
99- } catch (final InterruptedException e ) {
100- log .error ("Frame interval wait interrupted by Exception " , e );
103+ currentFrameTimestampMs = configuration .getStartTimeMs () + Math .round (frameCounter * frameDurationMs );
104+ nextFrameTimeNs += (long )(frameDurationMs * NANOS_IN_A_MILLISECOND );
105+
106+ long sleepTimeMs = (nextFrameTimeNs - System .nanoTime ()) / NANOS_IN_A_MILLISECOND ; // Convert to Ms
107+ if (sleepTimeMs > 0 ) {
108+ try {
109+ Thread .sleep (sleepTimeMs );
110+ } catch (final InterruptedException e ) {
111+ log .error ("Frame interval wait interrupted by Exception " , e );
112+ }
101113 }
102114 }
103115 }
@@ -106,12 +118,11 @@ private boolean isMetadataReady() {
106118 return frameCounter % METADATA_INTERVAL == 0 ;
107119 }
108120
109- private KinesisVideoFrame createKinesisVideoFrameFromImage (final long index ) {
121+ private KinesisVideoFrame createKinesisVideoFrameFromImage (final long index , final long timestampMs ) {
110122 final String filename = String .format (
111123 configuration .getFilenameFormat (),
112124 index % totalFiles + configuration .getStartFileIndex ());
113125 final Path path = Paths .get (configuration .getDir () + filename );
114- final long currentTimeMs = System .currentTimeMillis ();
115126
116127 final int flags = isKeyFrame () ? FRAME_FLAG_KEY_FRAME : FRAME_FLAG_NONE ;
117128
@@ -120,8 +131,8 @@ private KinesisVideoFrame createKinesisVideoFrameFromImage(final long index) {
120131 return new KinesisVideoFrame (
121132 frameCounter ,
122133 flags ,
123- currentTimeMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND ,
124- currentTimeMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND ,
134+ timestampMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND ,
135+ timestampMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND ,
125136 FRAME_DURATION_20_MS * HUNDREDS_OF_NANOS_IN_A_MILLISECOND ,
126137 ByteBuffer .wrap (data ));
127138 } catch (final IOException e ) {
0 commit comments