001/******************************************************************************* 002 * Copyright (c) 2024, 2026, Olivier Ayache. All rights reserved. 003 * 004 * This file is part of AVPKit. 005 * 006 * AVPKit is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Lesser General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * AVPKit is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with AVPKit. If not, see <http://www.gnu.org/licenses/>. 018 *******************************************************************************/ 019 020package com.avpkit.core.demos; 021 022import java.awt.image.BufferedImage; 023 024import com.avpkit.core.Global; 025import com.avpkit.core.IContainer; 026import com.avpkit.core.IPacket; 027import com.avpkit.core.IPixelFormat; 028import com.avpkit.core.IStream; 029import com.avpkit.core.IStreamCoder; 030import com.avpkit.core.ICodec; 031import com.avpkit.core.IVideoPicture; 032import com.avpkit.core.IVideoResampler; 033import com.avpkit.core.Utils; 034 035/** 036 * Takes a media container, finds the first video stream, 037 * decodes that stream, and then displays the video frames, 038 * at the frame-rate specified by the container, on a 039 * window. 040 * @author aclarke 041 * 042 */ 043public class DecodeAndPlayVideo 044{ 045 046 /** 047 * Takes a media container (file) as the first argument, opens it, 048 * opens up a Swing window and displays 049 * video frames with <i>roughly</i> the right timing. 050 * 051 * @param args Must contain one string which represents a filename 052 */ 053 @SuppressWarnings("deprecation") 054 public static void main(String[] args) 055 { 056 if (args.length <= 0) 057 throw new IllegalArgumentException("must pass in a filename" + 058 " as the first argument"); 059 060 String filename = args[0]; 061 062 // Let's make sure that we can actually convert video pixel formats. 063 if (!IVideoResampler.isSupported( 064 IVideoResampler.Feature.FEATURE_COLORSPACECONVERSION)) 065 throw new RuntimeException("you must install the GPL version" + 066 " of AVPKit (with IVideoResampler support) for " + 067 "this demo to work"); 068 069 // Create a AVPKit container object 070 IContainer container = IContainer.make(); 071 072 // Open up the container 073 if (container.open(filename, IContainer.Type.READ, null) < 0) 074 throw new IllegalArgumentException("could not open file: " + filename); 075 076 // query how many streams the call to open found 077 int numStreams = container.getNumStreams(); 078 079 // and iterate through the streams to find the first video stream 080 int videoStreamId = -1; 081 IStreamCoder videoCoder = null; 082 for(int i = 0; i < numStreams; i++) 083 { 084 // Find the stream object 085 IStream stream = container.getStream(i); 086 // Get the pre-configured decoder that can decode this stream; 087 IStreamCoder coder = stream.getStreamCoder(); 088 089 if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) 090 { 091 videoStreamId = i; 092 videoCoder = coder; 093 break; 094 } 095 } 096 if (videoStreamId == -1) 097 throw new RuntimeException("could not find video stream in container: " 098 +filename); 099 100 /* 101 * Now we have found the video stream in this file. Let's open up our decoder so it can 102 * do work. 103 */ 104 if (videoCoder.open() < 0) 105 throw new RuntimeException("could not open video decoder for container: " 106 +filename); 107 108 IVideoResampler resampler = null; 109 if (videoCoder.getPixelType() != IPixelFormat.Type.BGR24) 110 { 111 // if this stream is not in BGR24, we're going to need to 112 // convert it. The VideoResampler does that for us. 113 resampler = IVideoResampler.make(videoCoder.getWidth(), 114 videoCoder.getHeight(), IPixelFormat.Type.BGR24, 115 videoCoder.getWidth(), videoCoder.getHeight(), videoCoder.getPixelType()); 116 if (resampler == null) 117 throw new RuntimeException("could not create color space " + 118 "resampler for: " + filename); 119 } 120 /* 121 * And once we have that, we draw a window on screen 122 */ 123 openJavaWindow(); 124 125 /* 126 * Now, we start walking through the container looking at each packet. 127 */ 128 IPacket packet = IPacket.make(); 129 long firstTimestampInStream = Global.NO_PTS; 130 long systemClockStartTime = 0; 131 while(container.readNextPacket(packet) >= 0) 132 { 133 /* 134 * Now we have a packet, let's see if it belongs to our video stream 135 */ 136 if (packet.getStreamIndex() == videoStreamId) 137 { 138 /* 139 * We allocate a new picture to get the data out of AVPKit 140 */ 141 IVideoPicture picture = IVideoPicture.make(videoCoder.getPixelType(), 142 videoCoder.getWidth(), videoCoder.getHeight()); 143 144 int offset = 0; 145 while(offset < packet.getSize()) 146 { 147 /* 148 * Now, we decode the video, checking for any errors. 149 * 150 */ 151 int bytesDecoded = videoCoder.decodeVideo(picture, packet, offset); 152 if (bytesDecoded < 0) 153 throw new RuntimeException("got error decoding video in: " 154 + filename); 155 offset += bytesDecoded; 156 157 /* 158 * Some decoders will consume data in a packet, but will not be able to construct 159 * a full video picture yet. Therefore you should always check if you 160 * got a complete picture from the decoder 161 */ 162 if (picture.isComplete()) 163 { 164 IVideoPicture newPic = picture; 165 /* 166 * If the resampler is not null, that means we didn't get the 167 * video in BGR24 format and 168 * need to convert it into BGR24 format. 169 */ 170 if (resampler != null) 171 { 172 // we must resample 173 newPic = IVideoPicture.make(resampler.getOutputPixelFormat(), 174 picture.getWidth(), picture.getHeight()); 175 if (resampler.resample(newPic, picture) < 0) 176 throw new RuntimeException("could not resample video from: " 177 + filename); 178 } 179 if (newPic.getPixelType() != IPixelFormat.Type.BGR24) 180 throw new RuntimeException("could not decode video" + 181 " as BGR 24 bit data in: " + filename); 182 183 /** 184 * We could just display the images as quickly as we decode them, 185 * but it turns out we can decode a lot faster than you think. 186 * 187 * So instead, the following code does a poor-man's version of 188 * trying to match up the frame-rate requested for each 189 * IVideoPicture with the system clock time on your computer. 190 * 191 * Remember that all AVPKit IAudioSamples and IVideoPicture objects 192 * always give timestamps in Microseconds, relative to the first 193 * decoded item. If instead you used the packet timestamps, they can 194 * be in different units depending on your IContainer, and IStream 195 * and things can get hairy quickly. 196 */ 197 if (firstTimestampInStream == Global.NO_PTS) 198 { 199 // This is our first time through 200 firstTimestampInStream = picture.getTimeStamp(); 201 // get the starting clock time so we can hold up frames 202 // until the right time. 203 systemClockStartTime = System.currentTimeMillis(); 204 } else { 205 long systemClockCurrentTime = System.currentTimeMillis(); 206 long millisecondsClockTimeSinceStartofVideo = 207 systemClockCurrentTime - systemClockStartTime; 208 // compute how long for this frame since the first frame in the 209 // stream. 210 // remember that IVideoPicture and IAudioSamples timestamps are 211 // always in MICROSECONDS, 212 // so we divide by 1000 to get milliseconds. 213 long millisecondsStreamTimeSinceStartOfVideo = 214 (picture.getTimeStamp() - firstTimestampInStream)/1000; 215 final long millisecondsTolerance = 50; // and we give ourselfs 50 ms of tolerance 216 final long millisecondsToSleep = 217 (millisecondsStreamTimeSinceStartOfVideo - 218 (millisecondsClockTimeSinceStartofVideo + 219 millisecondsTolerance)); 220 if (millisecondsToSleep > 0) 221 { 222 try 223 { 224 Thread.sleep(millisecondsToSleep); 225 } 226 catch (InterruptedException e) 227 { 228 // we might get this when the user closes the dialog box, so 229 // just return from the method. 230 return; 231 } 232 } 233 } 234 235 // And finally, convert the BGR24 to an Java buffered image 236 BufferedImage javaImage = Utils.videoPictureToImage(newPic); 237 238 // and display it on the Java Swing window 239 updateJavaWindow(javaImage); 240 } 241 } 242 } 243 else 244 { 245 /* 246 * This packet isn't part of our video stream, so we just 247 * silently drop it. 248 */ 249 do {} while(false); 250 } 251 252 } 253 /* 254 * Technically since we're exiting anyway, these will be cleaned up by 255 * the garbage collector... but because we're nice people and want 256 * to be invited places for Christmas, we're going to show how to clean up. 257 */ 258 if (videoCoder != null) 259 { 260 videoCoder.close(); 261 videoCoder = null; 262 } 263 if (container !=null) 264 { 265 container.close(); 266 container = null; 267 } 268 closeJavaWindow(); 269 270 } 271 272 /** 273 * The window we'll draw the video on. 274 * 275 */ 276 private static VideoImage mScreen = null; 277 278 private static void updateJavaWindow(BufferedImage javaImage) 279 { 280 mScreen.setImage(javaImage); 281 } 282 283 /** 284 * Opens a Swing window on screen. 285 */ 286 private static void openJavaWindow() 287 { 288 mScreen = new VideoImage(); 289 } 290 291 /** 292 * Forces the swing thread to terminate; I'm sure there is a right 293 * way to do this in swing, but this works too. 294 */ 295 private static void closeJavaWindow() 296 { 297 System.exit(0); 298 } 299}