19 package org.sleuthkit.autopsy.corecomponents;
21 import com.google.common.io.Files;
22 import java.awt.Dimension;
23 import java.awt.Image;
24 import java.awt.image.BufferedImage;
26 import java.io.IOException;
27 import java.nio.IntBuffer;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.HashSet;
32 import java.util.List;
34 import java.util.concurrent.CancellationException;
35 import java.util.concurrent.ExecutionException;
36 import java.util.concurrent.TimeUnit;
37 import java.util.logging.Level;
38 import javax.swing.BoxLayout;
39 import javax.swing.JButton;
40 import javax.swing.JLabel;
41 import javax.swing.JPanel;
42 import javax.swing.JSlider;
43 import javax.swing.SwingUtilities;
44 import javax.swing.SwingWorker;
45 import javax.swing.event.ChangeEvent;
46 import org.gstreamer.ClockTime;
47 import org.gstreamer.Gst;
48 import org.gstreamer.GstException;
49 import org.gstreamer.State;
50 import org.gstreamer.StateChangeReturn;
51 import org.gstreamer.elements.PlayBin2;
52 import org.gstreamer.elements.RGBDataSink;
53 import org.gstreamer.swing.VideoComponent;
54 import org.netbeans.api.progress.ProgressHandle;
55 import org.openide.util.NbBundle;
56 import org.openide.util.lookup.ServiceProvider;
57 import org.openide.util.lookup.ServiceProviders;
66 @ServiceProviders(value = {
67 @ServiceProvider(service = FrameCapture.class)
71 private static final String[] EXTENSIONS =
new String[]{
".mov",
".m4v",
".flv",
".mp4",
".3gp",
".avi",
".mpg",
".mpeg",
".wmv"};
72 private static final List<String> MIMETYPES = Arrays.asList(
"video/quicktime",
"audio/mpeg",
"audio/x-mpeg",
"video/mpeg",
"video/x-mpeg",
"audio/mpeg3",
"audio/x-mpeg-3",
"video/x-flv",
"video/mp4",
"audio/x-m4a",
"video/x-m4v",
"audio/x-wav");
76 private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
77 private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000;
78 private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.cannotProcFile.err");
80 private long durationMillis = 0;
85 private boolean autoTracking =
false;
86 private final Object playbinLock =
new Object();
88 private final Set<String> badVideoFiles = Collections.synchronizedSet(
new HashSet<String>());
95 customizeComponents();
103 return progressLabel;
107 return progressSlider;
115 return gstVideoComponent;
128 progressSlider.setEnabled(
false);
129 progressSlider.setValue(0);
131 progressSlider.addChangeListener((ChangeEvent e) -> {
137 int time = progressSlider.getValue();
138 synchronized (playbinLock) {
139 if (gstPlaybin2 != null && !autoTracking) {
140 State orig = gstPlaybin2.getState();
141 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
142 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
143 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
146 if (gstPlaybin2.seek(ClockTime.fromMillis(time)) ==
false) {
147 logger.log(Level.WARNING,
"Attempt to call PlayBin2.seek() failed.");
148 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
151 gstPlaybin2.setState(orig);
159 logger.log(Level.INFO,
"Initializing gstreamer for video/audio viewing");
162 }
catch (GstException e) {
164 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e);
166 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.gstException.msg"),
169 }
catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) {
171 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and extraction capabilities", e);
173 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.otherException.msg"),
182 void setupVideo(
final AbstractFile file,
final Dimension dims) {
184 infoLabel.setText(
"");
186 final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
188 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.setupVideo.infoLabel.text"));
189 videoPanel.removeAll();
190 pauseButton.setEnabled(
false);
191 progressSlider.setEnabled(
false);
197 path = file.getUniquePath();
198 }
catch (TskCoreException ex) {
199 logger.log(Level.SEVERE,
"Cannot get unique path of video file");
201 infoLabel.setText(path);
202 infoLabel.setToolTipText(path);
203 pauseButton.setEnabled(
true);
204 progressSlider.setEnabled(
true);
206 java.io.File ioFile = VideoUtils.getTempVideoFile(file);
208 gstVideoComponent =
new VideoComponent();
209 synchronized (playbinLock) {
210 if (gstPlaybin2 != null) {
211 gstPlaybin2.dispose();
213 gstPlaybin2 =
new PlayBin2(
"VideoPlayer");
214 gstPlaybin2.setVideoSink(gstVideoComponent.getElement());
216 videoPanel.removeAll();
218 videoPanel.setLayout(
new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
219 videoPanel.add(gstVideoComponent);
221 videoPanel.setVisible(
true);
223 gstPlaybin2.setInputFile(ioFile);
225 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
226 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
227 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
237 SwingUtilities.invokeLater(() -> {
238 progressLabel.setText(
"");
245 synchronized (playbinLock) {
246 if (gstPlaybin2 != null) {
247 if (gstPlaybin2.isPlaying()) {
248 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
249 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
250 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
254 if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) {
255 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.NULL) failed.");
256 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
259 if (gstPlaybin2.getState().equals(State.NULL)) {
260 gstPlaybin2.dispose();
264 gstVideoComponent = null;
268 if (videoProgressWorker != null) {
269 videoProgressWorker.cancel(
true);
270 videoProgressWorker = null;
287 public List<VideoFrame>
captureFrames(java.io.File file,
int numFrames)
throws Exception {
289 List<VideoFrame> frames =
new ArrayList<>();
291 Object lock =
new Object();
299 if (badVideoFiles.contains(file.getName())) {
301 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemFile.msg", file.getName()));
305 RGBDataSink videoSink =
new RGBDataSink(
"rgb", rgbListener);
306 PlayBin2 playbin =
new PlayBin2(
"VideoFrameCapture");
307 playbin.setInputFile(file);
308 playbin.setVideoSink(videoSink);
311 StateChangeReturn ret = playbin.play();
312 if (ret == StateChangeReturn.FAILURE) {
314 badVideoFiles.add(file.getName());
315 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlay.msg"));
317 ret = playbin.pause();
318 if (ret == StateChangeReturn.FAILURE) {
320 badVideoFiles.add(file.getName());
321 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPause.msg"));
326 TimeUnit unit = TimeUnit.MILLISECONDS;
327 long myDurationMillis = playbin.queryDuration(unit);
328 if (myDurationMillis <= 0) {
333 int numFramesToGet = numFrames;
334 long frameInterval = myDurationMillis / numFrames;
335 if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
340 for (
int i = 0; i < numFramesToGet; ++i) {
341 long timeStamp = i * frameInterval;
343 ret = playbin.pause();
344 if (ret == StateChangeReturn.FAILURE) {
346 badVideoFiles.add(file.getName());
348 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPauseCaptFrame.msg"));
352 if (!playbin.seek(timeStamp, unit)) {
353 logger.log(Level.INFO,
"There was a problem seeking to " + timeStamp +
" " + unit.name().toLowerCase());
356 ret = playbin.play();
357 if (ret == StateChangeReturn.FAILURE) {
359 badVideoFiles.add(file.getName());
361 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlayCaptFrame.msg"));
365 synchronized (lock) {
367 lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS);
368 }
catch (InterruptedException e) {
369 logger.log(Level.INFO,
"InterruptedException occurred while waiting for frame capture.", e);
372 Image image = rgbListener.
getImage();
374 ret = playbin.stop();
375 if (ret == StateChangeReturn.FAILURE) {
377 badVideoFiles.add(file.getName());
379 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemStopCaptFrame.msg"));
383 logger.log(Level.WARNING,
"There was a problem while trying to capture a frame from file " + file.getName());
384 badVideoFiles.add(file.getName());
397 this.waiter = waiter;
400 private BufferedImage
bi;
404 public void rgbFrame(
boolean bln,
int w,
int h, IntBuffer rgbPixels) {
405 synchronized (waiter) {
406 bi =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
407 bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w);
413 synchronized (waiter) {
427 @SuppressWarnings(
"unchecked")
429 private
void initComponents() {
431 videoPanel =
new javax.swing.JPanel();
432 controlPanel =
new javax.swing.JPanel();
433 pauseButton =
new javax.swing.JButton();
434 progressSlider =
new javax.swing.JSlider();
435 progressLabel =
new javax.swing.JLabel();
436 infoLabel =
new javax.swing.JLabel();
438 javax.swing.GroupLayout videoPanelLayout =
new javax.swing.GroupLayout(videoPanel);
439 videoPanel.setLayout(videoPanelLayout);
440 videoPanelLayout.setHorizontalGroup(
441 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
442 .addGap(0, 0, Short.MAX_VALUE)
444 videoPanelLayout.setVerticalGroup(
445 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
446 .addGap(0, 231, Short.MAX_VALUE)
449 org.openide.awt.Mnemonics.setLocalizedText(pauseButton,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.pauseButton.text"));
450 pauseButton.addActionListener(
new java.awt.event.ActionListener() {
451 public void actionPerformed(java.awt.event.ActionEvent evt) {
452 pauseButtonActionPerformed(evt);
456 org.openide.awt.Mnemonics.setLocalizedText(progressLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.progressLabel.text"));
458 org.openide.awt.Mnemonics.setLocalizedText(infoLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.infoLabel.text"));
460 javax.swing.GroupLayout controlPanelLayout =
new javax.swing.GroupLayout(controlPanel);
461 controlPanel.setLayout(controlPanelLayout);
462 controlPanelLayout.setHorizontalGroup(
463 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
464 .addGroup(controlPanelLayout.createSequentialGroup()
466 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
467 .addGroup(controlPanelLayout.createSequentialGroup()
469 .addComponent(infoLabel)
470 .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
471 .addGroup(controlPanelLayout.createSequentialGroup()
472 .addComponent(pauseButton)
473 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
474 .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE)
475 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
476 .addComponent(progressLabel)
477 .addContainerGap())))
479 controlPanelLayout.setVerticalGroup(
480 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
481 .addGroup(controlPanelLayout.createSequentialGroup()
483 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
484 .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
485 .addComponent(pauseButton)
486 .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE))
487 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
488 .addComponent(infoLabel)
492 javax.swing.GroupLayout layout =
new javax.swing.GroupLayout(
this);
493 this.setLayout(layout);
494 layout.setHorizontalGroup(
495 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
496 .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
497 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
499 layout.setVerticalGroup(
500 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
501 .addGroup(layout.createSequentialGroup()
502 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
503 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
504 .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
509 synchronized (playbinLock) {
510 State state = gstPlaybin2.getState();
511 if (state.equals(State.PLAYING)) {
512 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
513 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
514 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
517 pauseButton.setText(
"►");
519 if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) {
520 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PAUSED) failed.");
521 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
524 }
else if (state.equals(State.PAUSED)) {
525 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
526 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
527 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
530 pauseButton.setText(
"||");
532 if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) {
533 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PLAYING) failed.");
534 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
537 }
else if (state.equals(State.READY)) {
557 private final String durationFormat =
"%02d:%02d:%02d/%02d:%02d:%02d ";
558 private long millisElapsed = 0;
559 private final long INTER_FRAME_PERIOD_MS = 20;
560 private final long END_TIME_MARGIN_MS = 50;
563 synchronized (playbinLock) {
564 return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL);
569 synchronized (playbinLock) {
570 if (gstPlaybin2 != null) {
571 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
572 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
573 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
576 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
577 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
578 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
580 gstPlaybin2.getState();
583 pauseButton.setText(
"►");
584 progressSlider.setValue(0);
586 String durationStr = String.format(durationFormat, 0, 0, 0,
587 totalHours, totalMinutes, totalSeconds);
588 progressLabel.setText(durationStr);
598 return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS;
605 progressSlider.setEnabled(
true);
608 while (hasNotEnded() && isPlayBinReady() && !isCancelled()) {
610 synchronized (playbinLock) {
611 pos = gstPlaybin2.queryPosition();
613 millisElapsed = pos.toMillis();
616 long secondsElapsed = millisElapsed / 1000;
617 int elapsedHours = (int) secondsElapsed / 3600;
618 secondsElapsed -= elapsedHours * 3600;
619 int elapsedMinutes = (int) secondsElapsed / 60;
620 secondsElapsed -= elapsedMinutes * 60;
621 int elapsedSeconds = (int) secondsElapsed;
623 String durationStr = String.format(durationFormat,
624 elapsedHours, elapsedMinutes, elapsedSeconds,
625 totalHours, totalMinutes, totalSeconds);
627 progressLabel.setText(durationStr);
629 progressSlider.setValue((
int) millisElapsed);
630 autoTracking =
false;
633 Thread.sleep(INTER_FRAME_PERIOD_MS);
634 }
catch (InterruptedException ex) {
640 progressSlider.setEnabled(
false);
652 }
catch (InterruptedException | ExecutionException ex) {
653 logger.log(Level.WARNING,
"Error updating video progress: " + ex.getMessage());
654 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.infoLabel.updateErr",
657 catch (java.util.concurrent.CancellationException ex) {
672 this.sourceFile = sFile;
673 this.tempFile = jFile;
678 if (tempFile.exists() ==
false || tempFile.length() < sourceFile.getSize()) {
679 progress = ProgressHandle.createHandle(NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () ->
ExtractMedia.this.cancel(
true));
680 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.buffering"));
683 Files.createParentDirs(tempFile);
685 }
catch (IOException ex) {
686 logger.log(Level.WARNING,
"Error buffering file", ex);
700 }
catch (CancellationException ex) {
701 logger.log(Level.INFO,
"Media buffering was canceled.");
702 }
catch (InterruptedException ex) {
703 logger.log(Level.INFO,
"Media buffering was interrupted.");
704 }
catch (Exception ex) {
705 logger.log(Level.SEVERE,
"Fatal error during media buffering.", ex);
707 if (progress != null) {
710 if (!this.isCancelled()) {
717 if (tempFile == null || !tempFile.exists()) {
718 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progressLabel.bufferingErr"));
722 synchronized (playbinLock) {
724 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
725 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
726 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
729 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
730 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
731 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
734 gstPlaybin2.getState();
735 dur = gstPlaybin2.queryDuration();
737 durationMillis = dur.toMillis();
740 long durationSeconds = (int) durationMillis / 1000;
741 totalHours = (int) durationSeconds / 3600;
742 durationSeconds -= totalHours * 3600;
743 totalMinutes = (int) durationSeconds / 60;
744 durationSeconds -= totalMinutes * 60;
745 totalSeconds = (int) durationSeconds;
747 SwingUtilities.invokeLater(() -> {
748 progressSlider.setMaximum((
int) durationMillis);
749 progressSlider.setMinimum(0);
751 synchronized (playbinLock) {
752 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
753 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
754 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
757 pauseButton.setText(
"||");
758 videoProgressWorker =
new VideoProgressWorker();
759 videoProgressWorker.execute();
766 return EXTENSIONS.clone();
List< VideoFrame > captureFrames(java.io.File file, int numFrames)
void customizeComponents()
javax.swing.JLabel progressLabel
JSlider getProgressSlider()
List< String > getMimeTypes()
void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels)
javax.swing.JButton pauseButton
JLabel getProgressLabel()
javax.swing.JPanel videoPanel
javax.swing.JSlider progressSlider
static< T > long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future< T > worker, boolean source)
void pauseButtonActionPerformed(java.awt.event.ActionEvent evt)
VideoComponent getVideoComponent()
javax.swing.JLabel infoLabel
javax.swing.JPanel controlPanel
VideoProgressWorker videoProgressWorker
volatile PlayBin2 gstPlaybin2
static void error(String title, String message)
synchronized static Logger getLogger(String name)
VideoComponent gstVideoComponent
static File getTempVideoFile(AbstractFile file)
FrameCaptureRGBListener(Object waiter)