From c514dbe04ffa430b95ad3dadf62cfcb543fdefa9 Mon Sep 17 00:00:00 2001 From: jawallace Date: Fri, 23 Aug 2013 12:39:37 -0400 Subject: [PATCH 01/12] Added system property to allow javafx to be embedded in mac application. --- Core/src/org/sleuthkit/autopsy/core/Installer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index fcac88d7c3..d2b0409d1f 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -60,6 +60,7 @@ public class Installer extends ModuleInstall { private void initJavaFx() { //initialize java fx if exists + System.setProperty("javafx.macosx.embedded", "true"); try { Platform.setImplicitExit(false); PlatformImpl.startup(new Runnable() { From 62efe1f8d1ad24dc0f3dc8fa4a3f4495f9f4bed0 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Mon, 26 Aug 2013 08:27:51 -0400 Subject: [PATCH 02/12] Sigar now loads correctly on mac --- .../src/org/sleuthkit/autopsy/corelibs/SigarLoader.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CoreLibs/src/org/sleuthkit/autopsy/corelibs/SigarLoader.java b/CoreLibs/src/org/sleuthkit/autopsy/corelibs/SigarLoader.java index 6e06539cda..f0f0859478 100644 --- a/CoreLibs/src/org/sleuthkit/autopsy/corelibs/SigarLoader.java +++ b/CoreLibs/src/org/sleuthkit/autopsy/corelibs/SigarLoader.java @@ -18,7 +18,7 @@ */ package org.sleuthkit.autopsy.corelibs; -import java.io.File; +import com.sun.javafx.PlatformUtil; import org.hyperic.sigar.Sigar; /** @@ -42,7 +42,11 @@ public class SigarLoader { if (sigar == null) { try { //rely on netbeans / jna to locate the lib variation for architecture/OS - System.loadLibrary("libsigar"); + if (PlatformUtil.isWindows()) { + System.loadLibrary("libsigar"); + } else { + System.loadLibrary("sigar"); + } sigar = new Sigar(); sigar.enableLogging(false); //forces a test From 7bd18a630579f7cb631fad7c9b38e6adc42584e6 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Mon, 26 Aug 2013 13:33:12 -0400 Subject: [PATCH 03/12] Fixed timeline on unix platforms --- Timeline/release/mactime/mactime.pl | 938 ++++++++++++++++++ .../sleuthkit/autopsy/timeline/Timeline.java | 13 +- 2 files changed, 947 insertions(+), 4 deletions(-) create mode 100755 Timeline/release/mactime/mactime.pl diff --git a/Timeline/release/mactime/mactime.pl b/Timeline/release/mactime/mactime.pl new file mode 100755 index 0000000000..868d930906 --- /dev/null +++ b/Timeline/release/mactime/mactime.pl @@ -0,0 +1,938 @@ +#!/usr/bin/perl -w +my $VER="4.1.0"; +# +# This program is based on the 'mactime' program by Dan Farmer and +# and the 'mac_daddy' program by Rob Lee. +# +# It takes as input data from either 'ils -m' or 'fls -m' (from The Sleuth +# Kit) or 'mac-robber'. +# Based on the dates as arguments given, the data is sorted by and +# printed. +# +# The Sleuth Kit +# Brian Carrier [carrier sleuthkit [dot] org] +# Copyright (c) 2003-2012 Brian Carrier. All rights reserved +# +# TASK +# Copyright (c) 2002 Brian Carrier, @stake Inc. All rights reserved +# +# +# The modifications to the original mactime are distributed under +# the Common Public License 1.0 +# +# +# Copyright 1999 by Dan Farmer. All rights reserved. Some individual +# files may be covered by other copyrights (this will be noted in the +# file itself.) +# +# Redistribution and use in source and binary forms are permitted +# provided that this entire copyright notice is duplicated in all such +# copies. +# +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE. +# +# IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +use POSIX; +use strict; + +my $debug = 0; + +# %month_to_digit = ("Jan", 1, "Feb", 2, "Mar", 3, "Apr", 4, "May", 5, "Jun", 6, +# "Jul", 7, "Aug", 8, "Sep", 9, "Oct", 10, "Nov", 11, "Dec", 12); +my %digit_to_month = ( + "01", "Jan", "02", "Feb", "03", "Mar", "04", "Apr", + "05", "May", "06", "Jun", "07", "Jul", "08", "Aug", + "09", "Sep", "10", "Oct", "11", "Nov", "12", "Dec" +); +my %digit_to_day = ( + "0", "Sun", "1", "Mon", "2", "Tue", "3", "Wed", + "4", "Thu", "5", "Fri", "6", "Sat" +); + +sub usage { + print <all_names() ) { + push( @t_list, $_ ); + } + foreach( keys( %{DateTime::TimeZone->links()}) ) { + push( @t_list, $_ ); + } + + return sort { $a cmp $b } @t_list; +} + +usage() if (scalar(@ARGV) == 0); + +while ((scalar(@ARGV) > 0) && (($_ = $ARGV[0]) =~ /^-(.)(.*)/)) { + + # Body File + if (/^-b$/) { + shift(@ARGV); + if (defined $ARGV[0]) { + $BODY = $ARGV[0]; + } + else { + print "-b requires body file argument\n"; + } + } + elsif (/^-d$/) { + $COMMA = 1; + } + + # Group File + elsif (/^-g$/) { + shift(@ARGV); + if (defined $ARGV[0]) { + &'load_group_info($ARGV[0]); + $GROUP = $ARGV[0]; + } + else { + print "-g requires group file argument\n"; + usage(); + } + } + + # Password File + elsif (/^-p$/) { + shift(@ARGV); + if (defined $ARGV[0]) { + &'load_passwd_info($ARGV[0]); + $PASSWD = $ARGV[0]; + } + else { + print "-p requires password file argument\n"; + usage(); + } + } + elsif (/^-h$/) { + $header = 1; + } + + # Index File + elsif (/^-i$/) { + shift(@ARGV); + + if (defined $ARGV[0]) { + + # Find out what type + if ($ARGV[0] eq "day") { + $INDEX_TYPE = $INDEX_DAY; + } + elsif ($ARGV[0] eq "hour") { + $INDEX_TYPE = $INDEX_HOUR; + } + shift(@ARGV); + unless (defined $ARGV[0]) { + print "-i requires index file argument\n"; + usage(); + } + $INDEX = $ARGV[0]; + } + else { + print "-i requires index file argument and type\n"; + usage(); + } + open(INDEX, ">$INDEX") or die "Can not open $INDEX"; + } + elsif (/^-V$/) { + version(); + exit(0); + } + elsif (/^-m$/) { + $month_num = 1; + } + elsif (/^-y$/) { + $iso8601 = 1; + } + elsif (/^-z$/) { + shift(@ARGV); + if (defined $ARGV[0]) { + my $tz = "$ARGV[0]"; + + if ($tz =~ m/^list$/i) { + if ($_HAS_DATETIME_TIMEZONE) { + my $txt = " +----------------------------------- + TIMEZONE LIST +-----------------------------------\n"; + foreach ( get_timezone_list() ) { + $txt .= $_ . "\n"; + } + print( $txt ); + } + else { + print "DateTime module not loaded -- cannot list timezones\n"; + } + exit(0); + } + # validate the string if we have DateTime module + elsif ($_HAS_DATETIME_TIMEZONE) { + my $realtz = 0; + foreach ( get_timezone_list() ) { + if ($tz =~ m/^$_$/i) { + $realtz = $_; + last; + } + } + if ($realtz) { + $ENV{TZ} = $realtz; + } + else { + print "invalid timezone provided. Use -z to list valid timezones.\n"; + usage(); + } + } + # blindly take it otherwise + else { + $ENV{TZ} = $tz; + } + } + else { + print "-z requires the time zone argument\n"; + usage(); + } + } + else { + print "Unknown option: $_\n"; + usage(); + } + shift(@ARGV); +} + +# Was the time given +if (defined $ARGV[0]) { + my $t_in; + my $t_out; + + $TIME = $ARGV[0]; + if ($ARGV[0] =~ /\.\./) { + ($t_in, $t_out) = split(/\.\./, $ARGV[0]); + } + else { + $t_in = $ARGV[0]; + $t_out = 0; + } + $in_seconds = parse_isodate($t_in); + die "Invalid Date: $t_in\n" if ($in_seconds < 0); + + if ($t_out) { + $out_seconds = parse_isodate($t_out); + die "Invalid Date: $t_out\n" if ($out_seconds < 0); + } + else { + $out_seconds = 0; + } +} +else { + $in_seconds = 0; + $out_seconds = 0; +} + +# Print header info +print_header() if ($header == 1); + +# Print the index header +if ($INDEX ne "") { + my $time_str = ""; + if ($INDEX_TYPE == $INDEX_DAY) { + $time_str = "Daily"; + } + else { + $time_str = "Hourly"; + } + if ($BODY ne "") { + print INDEX "$time_str Summary for Timeline of $BODY\n\n"; + } + else { + print INDEX "$time_str Summary for Timeline of STDIN\n\n"; + } +} + +read_body(); + +print_tl(); + +################ SUBROUTINES ################## + +#convert yyyy-mm-dd string to Unix date +sub parse_isodate { + my $iso_date = shift; + + my $sec = 0; + my $min = 0; + my $hour = 0; + my $wday = 0; + my $yday = 0; + if ($iso_date =~ /^(\d\d\d\d)\-(\d\d)\-(\d\d)$/) { + return mktime($sec, $min, $hour, $3, $2 - 1, $1 - 1900, $wday, $yday); + } + else { + return -1; + } +} + +# Read the body file from the BODY variable +sub read_body { + + # Read the body file from STDIN or the -b specified body file + if ($BODY ne "") { + open(BODY, "<$BODY") or die "Can't open $BODY"; + } + else { + open(BODY, "<&STDIN") or die "Can't dup STDIN"; + } + + while () { + next if ((/^\#/) || (/^\s+$/)); + + chomp; + + my ( + $tmp1, $file, $st_ino, $st_ls, + $st_uid, $st_gid, $st_size, $st_atime, + $st_mtime, $st_ctime, $st_crtime, $tmp2 + ) + = &tm_split($_); + + # Sanity check so that we ignore the header entries + next unless ((defined $st_ino) && ($st_ino =~ /[\d-]+/)); + next unless ((defined $st_uid) && ($st_uid =~ /\d+/)); + next unless ((defined $st_gid) && ($st_gid =~ /\d+/)); + next unless ((defined $st_size) && ($st_size =~ /\d+/)); + next unless ((defined $st_mtime) && ($st_mtime =~ /\d+/)); + next unless ((defined $st_atime) && ($st_atime =~ /\d+/)); + next unless ((defined $st_ctime) && ($st_ctime =~ /\d+/)); + next unless ((defined $st_crtime) && ($st_crtime =~ /\d+/)); + + # we need *some* value in mactimes! + next if (!$st_atime && !$st_mtime && !$st_ctime && !$st_crtime); + + # Skip if these are all too early + next + if ( ($st_mtime < $in_seconds) + && ($st_atime < $in_seconds) + && ($st_ctime < $in_seconds) + && ($st_crtime < $in_seconds)); + + # add leading zeros to timestamps because we will later sort + # these using a string-based comparison + $st_mtime = sprintf("%.10d", $st_mtime); + $st_atime = sprintf("%.10d", $st_atime); + $st_ctime = sprintf("%.10d", $st_ctime); + $st_crtime = sprintf("%.10d", $st_crtime); + + # Put all the times in one big array along with the inode and + # name (they are used in the final sorting) + + # If the date on the file is too old, don't put it in the array + my $post = ",$st_ino,$file"; + + if ($out_seconds) { + $timestr2macstr{"$st_mtime$post"} .= "m" + if ( + ($st_mtime >= $in_seconds) + && ($st_mtime < $out_seconds) + && ( (!(exists $timestr2macstr{"$st_mtime$post"})) + || ($timestr2macstr{"$st_mtime$post"} !~ /m/)) + ); + + $timestr2macstr{"$st_atime$post"} .= "a" + if ( + ($st_atime >= $in_seconds) + && ($st_atime < $out_seconds) + && ( (!(exists $timestr2macstr{"$st_atime$post"})) + || ($timestr2macstr{"$st_atime$post"} !~ /a/)) + ); + + $timestr2macstr{"$st_ctime$post"} .= "c" + if ( + ($st_ctime >= $in_seconds) + && ($st_ctime < $out_seconds) + && ( (!(exists $timestr2macstr{"$st_ctime$post"})) + || ($timestr2macstr{"$st_ctime$post"} !~ /c/)) + ); + + $timestr2macstr{"$st_crtime$post"} .= "b" + if ( + ($st_crtime >= $in_seconds) + && ($st_crtime < $out_seconds) + && ( (!(exists $timestr2macstr{"$st_crtime$post"})) + || ($timestr2macstr{"$st_crtime$post"} !~ /b/)) + ); + } + else { + $timestr2macstr{"$st_mtime$post"} .= "m" + if ( + ($st_mtime >= $in_seconds) + && ( (!(exists $timestr2macstr{"$st_mtime$post"})) + || ($timestr2macstr{"$st_mtime$post"} !~ /m/)) + ); + + $timestr2macstr{"$st_atime$post"} .= "a" + if ( + ($st_atime >= $in_seconds) + && ( (!(exists $timestr2macstr{"$st_atime$post"})) + || ($timestr2macstr{"$st_atime$post"} !~ /a/)) + ); + + $timestr2macstr{"$st_ctime$post"} .= "c" + if ( + ($st_ctime >= $in_seconds) + && ( (!(exists $timestr2macstr{"$st_ctime$post"})) + || ($timestr2macstr{"$st_ctime$post"} !~ /c/)) + ); + + $timestr2macstr{"$st_crtime$post"} .= "b" + if ( + ($st_crtime >= $in_seconds) + && ( (!(exists $timestr2macstr{"$st_crtime$post"})) + || ($timestr2macstr{"$st_crtime$post"} !~ /b/)) + ); + } + + # if the UID or GID is not in the array then add it. + # these are filled if the -p or -g options are given + $uid2names{$st_uid} = $st_uid + unless (defined $uid2names{$st_uid}); + $gid2names{$st_gid} = $st_gid + unless (defined $gid2names{$st_gid}); + + # + # put /'s between multiple UID/GIDs + # + $uid2names{$st_uid} =~ s@\s@/@g; + $gid2names{$st_gid} =~ s@\s@/@g; + + $file2other{$file} = + "$st_ls:$uid2names{$st_uid}:$gid2names{$st_gid}:$st_size"; + } + + close BODY; +} # end of read_body + +sub print_header { + return if ($header == 0); + + print "The Sleuth Kit mactime Timeline\n"; + + print "Input Source: "; + if ($BODY eq "") { + print "STDIN\n"; + } + else { + print "$BODY\n"; + } + + print "Time: $TIME\t\t" if ($TIME ne ""); + + if ($ENV{TZ} eq "") { + print "\n"; + } + else { + print "Timezone: $ENV{TZ}\n"; + } + + print "passwd File: $PASSWD" if ($PASSWD ne ""); + if ($GROUP ne "") { + print "\t" if ($PASSWD ne ""); + print "group File: $GROUP"; + } + print "\n" if (($PASSWD ne "") || ($GROUP ne "")); + + print "\n"; +} + +# +# Print the time line +# +sub print_tl { + + my $prev_day = ""; # has the format of 'day day_week mon year' + my $prev_hour = ""; # has just the hour and is used for hourly index + my $prev_cnt = 0; + my $old_date_string = ""; + + my $delim = ":"; + if ($COMMA != 0) { + print "Date,Size,Type,Mode,UID,GID,Meta,File Name\n"; + $delim = ","; + } + + # Cycle through the files and print them in sorted order. + # Note that we sort using a string comparison because the keys + # also contain the inode and file name + for my $key (sort { $a cmp $b } keys %timestr2macstr) { + my $time; + my $inode; + my $file; + + if ($key =~ /^(\d+),([\d-]+),(.*)$/) { + $time = $1; + $inode = $2; + $file = $3; + } + else { + next; + } + + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); + if ($iso8601) { + ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = + gmtime($time); + } + else { + ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = + localtime($time); + } + + # the month here is 0-11, not 1-12, like what we want + $mon++; + + print +"\t($sec,$min,$hour,MDay: $mday,M: $mon,$year,$wday,$yday,$isdst) = ($time)\n" + if $debug; + + # + # cosmetic change to make it look like unix dates + # + $mon = "0$mon" if $mon < 10; + $mday = "0$mday" if $mday < 10; + $hour = "0$hour" if $hour < 10; + $min = "0$min" if $min < 10; + $sec = "0$sec" if $sec < 10; + + my $yeart = $year + 1900; + + # How do we print the date? + # + my $date_string; + if ($iso8601) { + if ($time == 0) { + $date_string = "0000-00-00T00:00:00Z"; + } + else { + $date_string = +"$yeart-$mon-${mday}T$hour:$min:${sec}Z"; + } + } + else { + if ($time == 0) { + $date_string = "Xxx Xxx 00 0000 00:00:00"; + } + elsif ($month_num) { + $date_string = + "$digit_to_day{$wday} $mon $mday $yeart $hour:$min:$sec"; + } + else { + $date_string = +"$digit_to_day{$wday} $digit_to_month{$mon} $mday $yeart $hour:$min:$sec"; + } + } + + # + # However, we only print the date if it's different from the one + # above. We need to fill the empty space with blanks, though. + # + if ($old_date_string eq $date_string) { + if ($iso8601) { + $date_string = " "; + } + else { + $date_string = " "; + } + $prev_cnt++ + if ($INDEX ne ""); + } + else { + $old_date_string = $date_string; + + # Indexing code + if ($INDEX ne "") { + + # First time it is run + if ($prev_day eq "") { + $prev_day = "$mday $wday $mon $yeart"; + $prev_hour = $hour; + $prev_cnt = 0; + } + + # A new day, so print the results + elsif ($prev_day ne "$mday $wday $mon $yeart") { + my @prev_vals = split(/ /, $prev_day); + + my $date_str; + if ($month_num) { + $date_str = + "$digit_to_day{$prev_vals[1]} " + . "$prev_vals[2] " + . "$prev_vals[0] ${prev_vals[3]}"; + } + else { + $date_str = + "$digit_to_day{$prev_vals[1]} " + . "$digit_to_month{$prev_vals[2]} " + . "$prev_vals[0] ${prev_vals[3]}"; + } + + $date_str .= " $prev_hour:00:00" + if ($INDEX_TYPE == $INDEX_HOUR); + + print INDEX "${date_str}${delim} $prev_cnt\n"; + + # Reset + $prev_cnt = 0; + $prev_day = "$mday $wday $mon $yeart"; + $prev_hour = $hour; + + } + + # Same day, but new hour + elsif (($INDEX_TYPE == $INDEX_HOUR) && ($prev_hour != $hour)) { + my @prev_vals = split(/ /, $prev_day); + + if ($month_num) { + print INDEX "$digit_to_day{$prev_vals[1]} " + . "$prev_vals[2] " + . "$prev_vals[0] ${prev_vals[3]} " + . "$prev_hour:00:00${delim} $prev_cnt\n"; + } + else { + print INDEX "$digit_to_day{$prev_vals[1]} " + . "$digit_to_month{$prev_vals[2]} " + . "$prev_vals[0] ${prev_vals[3]} " + . "$prev_hour:00:00${delim} $prev_cnt\n"; + } + + # Reset + $prev_cnt = 0; + $prev_hour = $hour; + } + $prev_cnt++; + } + } + + # + # Muck around with the [mac]times string to make it pretty. + # + my $mactime_tmp = $timestr2macstr{$key}; + my $mactime = ""; + if ($mactime_tmp =~ /m/) { + $mactime = "m"; + } + else { + $mactime = "."; + } + + if ($mactime_tmp =~ /a/) { + $mactime .= "a"; + } + else { + $mactime .= "."; + } + + if ($mactime_tmp =~ /c/) { + $mactime .= "c"; + } + else { + $mactime .= "."; + } + + if ($mactime_tmp =~ /b/) { + $mactime .= "b"; + } + else { + $mactime .= "."; + } + + my ($ls, $uids, $groups, $size) = split(/:/, $file2other{$file}); + + print "FILE: $file MODES: $ls U: $uids G: $groups S: $size\n" + if $debug; + + if ($COMMA == 0) { + printf("%s %8s %3s %s %-8s %-8s %-8s %s\n", + $date_string, $size, $mactime, $ls, $uids, $groups, $inode, + $file); + } + else { + # escape any quotes in filename + my $file_tmp = $file; + $file_tmp =~ s/\"/\"\"/g; + printf("%s,%s,%s,%s,%s,%s,%s,\"%s\"\n", + $old_date_string, $size, $mactime, $ls, $uids, $groups, $inode, + $file_tmp); + } + } + + # Finish the index page for the last entry + if (($INDEX ne "") && ($prev_cnt > 0)) { + my @prev_vals = split(/ /, $prev_day); + + my $date_str; + if ($month_num) { + $date_str = + "$digit_to_day{$prev_vals[1]} " + . "$prev_vals[2] " + . "$prev_vals[0] ${prev_vals[3]}"; + } + else { + $date_str = + "$digit_to_day{$prev_vals[1]} " + . "$digit_to_month{$prev_vals[2]} " + . "$prev_vals[0] ${prev_vals[3]}"; + } + + $date_str .= " $prev_hour:00:00" + if ($INDEX_TYPE == $INDEX_HOUR); + + print INDEX "${date_str}${delim} $prev_cnt\n"; + close INDEX; + } +} + +# +# Routines for reading and caching user and group information. These +# are used in multiple programs... it caches the info once, then hopefully +# won't be used again. +# +# Steve Romig, May 1991. +# +# Provides a bunch of routines and a bunch of arrays. Routines +# (and their usage): +# +# load_passwd_info($use_getent, $file_name) +# +# loads user information into the %uname* and %uid* arrays +# (see below). +# +# If $use_getent is non-zero: +# get the info via repeated 'getpwent' calls. This can be +# *slow* on some hosts, especially if they are running as a +# YP (NIS) client. +# If $use_getent is 0: +# if $file_name is "", then get the info from reading the +# results of "ypcat passwd" and from /etc/passwd. Otherwise, +# read the named file. The file should be in passwd(5) +# format. +# +# load_group_info($use_gentent, $file_name) +# +# is similar to load_passwd_info. +# +# Information is stored in several convenient associative arrays: +# +# %uid2names Assoc array, indexed by uid, value is list of +# user names with that uid, in form "name name +# name...". +# +# %gid2members Assoc array, indexed by gid, value is list of +# group members in form "name name name..." +# +# %gname2gid Assoc array, indexed by group name, value is +# matching gid. +# +# %gid2names Assoc array, indexed by gid, value is the +# list of group names with that gid in form +# "name name name...". +# +# You can also use routines named the same as the arrays - pass the index +# as the arg, get back the value. If you use this, get{gr|pw}{uid|gid|nam} +# will be used to lookup entries that aren't found in the cache. +# +# To be done: +# probably ought to add routines to deal with full names. +# maybe there ought to be some anal-retentive checking of password +# and group entries. +# probably ought to cache get{pw|gr}{nam|uid|gid} lookups also. +# probably ought to avoid overwriting existing entries (eg, duplicate +# names in password file would collide in the tables that are +# indexed by name). +# +# Disclaimer: +# If you use YP and you use netgroup entries such as +# +@servers:::::: +# +:*:::::/usr/local/utils/messages +# then loading the password file in with &load_passwd_info(0) will get +# you mostly correct YP stuff *except* that it won't do the password and +# shell substitutions as you'd expect. You might want to use +# &load_passwd_info(1) instead to use getpwent calls to do the lookups, +# which would be more correct. +# +# +# minor changes to make it fit with the TCT program, 9/25/99, - dan +# A whole lot removed to clean it up for TSK - July 2008 - Brian +# + +package main; + +my $passwd_loaded = 0; # flags to use to avoid reloading everything +my $group_loaded = 0; # unnecessarily... + +# +# Update user information for the user named $name. We cache the password, +# uid, login group, home directory and shell. +# + +sub add_pw_info { + my ($name, $tmp, $uid) = @_; + + if ((defined $name) && ($name ne "")) { + + if ((defined $uid) && ($uid ne "")) { + if (defined($uid2names{$uid})) { + $uid2names{$uid} .= " $name"; + } + else { + $uid2names{$uid} = $name; + } + } + } +} + +# +# Update group information for the group named $name. We cache the gid +# and the list of group members. +# + +sub add_gr_info { + my ($name, $tmp, $gid) = @_; + + if ((defined $name) && ($name ne "")) { + + if ((defined $gid) && ($gid ne "")) { + if (defined($gid2names{$gid})) { + $gid2names{$gid} .= " $name"; + } + else { + $gid2names{$gid} = $name; + } + } + } +} + +sub load_passwd_info { + my ($file_name) = @_; + my (@pw_info); + + if ($passwd_loaded) { + return; + } + + $passwd_loaded = 1; + + open(FILE, $file_name) + || die "can't open $file_name"; + + while () { + chop; + + if ($_ !~ /^\+/) { + &add_pw_info(split(/:/)); + } + } + close(FILE); +} + +sub load_group_info { + my ($file_name) = @_; + my (@gr_info); + + if ($group_loaded) { + return; + } + + $group_loaded = 1; + + open(FILE, $file_name) + || die "can't open $file_name"; + + while () { + chop; + if ($_ !~ /^\+/) { + &add_gr_info(split(/:/)); + } + } + close(FILE); +} + +# +# Split a time machine record. +# +sub tm_split { + my ($line) = @_; + my (@fields); + + for (@fields = split(/\|/, $line)) { + s/%([A-F0-9][A-F0-9])/pack("C", hex($1))/egis; + } + return @fields; +} +1; + diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java b/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java index 74aafa2b59..01977bfabd 100644 --- a/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java @@ -1044,18 +1044,23 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar, } private String makeMacTime(String pathToBodyFile) { + String cmdpath = ""; String macpath = ""; + String[] mactimeArgs; final String machome = macRoot.getAbsolutePath(); pathToBodyFile = PlatformUtil.getOSFilePath(pathToBodyFile); if (PlatformUtil.isWindowsOS()) { macpath = machome + java.io.File.separator + "mactime.exe"; - macpath = PlatformUtil.getOSFilePath(macpath); + cmdpath = PlatformUtil.getOSFilePath(cmdpath); + mactimeArgs = new String[]{"-b", pathToBodyFile, "-d", "-y"}; } else { - macpath = "perl " + machome + java.io.File.separator + "mactime.pl"; + cmdpath = "perl"; + macpath = machome + java.io.File.separator + "mactime.pl"; + mactimeArgs = new String[]{macpath, "-b", pathToBodyFile, "-d", "-y"}; } String macfile = moduleDir.getAbsolutePath() + java.io.File.separator + mactimeFileName; - String[] mactimeArgs = new String[]{"-b", pathToBodyFile, "-d", "-y"}; + String output = ""; ExecUtil execUtil = new ExecUtil(); @@ -1063,7 +1068,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar, try { //JavaSystemCaller.Exec.execute("\"" + command + "\""); writer = new FileWriter(macfile); - execUtil.execute(writer, macpath, mactimeArgs); + execUtil.execute(writer, cmdpath, mactimeArgs); } catch (InterruptedException ie) { logger.log(Level.WARNING, "Mactime process was interrupted by user", ie); return null; From 63de1d39175631c925b63cc4a8fca97a8f23a29b Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Mon, 26 Aug 2013 13:58:58 -0400 Subject: [PATCH 04/12] Fixed UI issues on mac os x --- Core/src/org/sleuthkit/autopsy/core/layer.xml | 1 + .../autopsy/corecomponents/Installer.java | 54 ++++++++++++++----- build.xml | 2 +- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index f72ce99565..3b5b8b481e 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -361,6 +361,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java index 66eb1d2d23..62cfacb5b3 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java @@ -18,10 +18,10 @@ */ package org.sleuthkit.autopsy.corecomponents; -import com.sun.javafx.application.PlatformImpl; import java.awt.Insets; +import java.util.Map; +import java.util.TreeMap; import java.util.logging.Level; -import javafx.application.Platform; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.BorderFactory; import javax.swing.UIManager; @@ -29,9 +29,9 @@ import javax.swing.UIManager.LookAndFeelInfo; import javax.swing.UnsupportedLookAndFeelException; import org.netbeans.swing.tabcontrol.plaf.DefaultTabbedContainerUI; import org.openide.modules.ModuleInstall; +import org.openide.util.Exceptions; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** * Manages this module's lifecycle. Opens the startup dialog during startup. @@ -57,7 +57,7 @@ public class Installer extends ModuleInstall { public void restored() { super.restored(); - //setupLAF(); + setupLAF(); UIManager.put("ViewTabDisplayerUI", "org.sleuthkit.autopsy.corecomponents.NoTabsTabDisplayerUI"); UIManager.put(DefaultTabbedContainerUI.KEY_VIEW_CONTENT_BORDER, BorderFactory.createEmptyBorder()); UIManager.put("TabbedPane.contentBorderInsets", new Insets(0, 0, 0, 0)); @@ -84,25 +84,53 @@ public class Installer extends ModuleInstall { //UIManager.put("nimbusBase", new Color()); //UIManager.put("nimbusBlueGrey", new Color()); //UIManager.put("control", new Color()); + + if (System.getProperty("os.name").toLowerCase().contains("mac")) { + setupMacOsXLAF(); + } + + } - + private void setupMacOsXLAF() { Logger logger = Logger.getLogger(Installer.class.getName()); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException ex) { + logger.log(Level.WARNING, "Unable to set theme. ", ex); + } + + final String[] UI_KEYS = new String[]{"MenuBarUI", + "MenuUI", + "MenuItemUI", + "CheckBoxMenuItemUI", + "RadioButtonMenuItemUI", + "PopupMenuUI"}; + + Map uiEntries = new TreeMap(); + + for(String key : UI_KEYS) { + uiEntries.put(key, UIManager.get(key)); + } + + //use Nimbus if available for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { + if ("Metal".equals(info.getName())) { try { UIManager.setLookAndFeel(info.getClassName()); - } catch (ClassNotFoundException ex) { - logger.log(Level.WARNING, "Unable to set theme. ", ex); - } catch (InstantiationException ex) { - logger.log(Level.WARNING, "Unable to set theme. ", ex); - } catch (IllegalAccessException ex) { - logger.log(Level.WARNING, "Unable to set theme. ", ex); - } catch (UnsupportedLookAndFeelException ex) { + } catch (ClassNotFoundException | InstantiationException | + IllegalAccessException | UnsupportedLookAndFeelException ex) { logger.log(Level.WARNING, "Unable to set theme. ", ex); } break; } } + + for(Map.Entry entry : uiEntries.entrySet()) { + UIManager.put(entry.getKey(), entry.getValue()); + } + } } diff --git a/build.xml b/build.xml index f1f6586b11..f45a6828ef 100644 --- a/build.xml +++ b/build.xml @@ -86,7 +86,7 @@ - + From 0e4e424435f537bd722b386bd7ef8b95c54e5d23 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Mon, 26 Aug 2013 14:01:50 -0400 Subject: [PATCH 05/12] Added Mac OS X icon file --- branding_autopsy/icon.icns | Bin 0 -> 42162 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 branding_autopsy/icon.icns diff --git a/branding_autopsy/icon.icns b/branding_autopsy/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..2d55fafc0dae5b84fecb35ef05534726c231a7e6 GIT binary patch literal 42162 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 z z@$$cP{_GiWkX0j8l`u{CJtXstkJYa}0g}Z+bppnP<&c_X_DSvQL>>!;ibI*66zVTK zgG`OM^Pq=n5c=VAya0&|-Sea#K9&;x3q>*Pa3Bx-cY~c+1q}GW){0aqyd^tUEB-+F z-V|3mTx=5Q?G>U@pnjIyN}XDGd!vYo`W-|(GMqP$%XvxYJ49wT${d<~ftV=uX9ZBg z&WSAV<;s7F*f!a?L`;MpxUVa~@O^kfCe`({&z-Zs*&jYO3LzVE0HB@F*zzXal*MxU zj|71Tb=QNQO*syUju&PR@RBUI9MWQrQz+ByOCEq Date: Mon, 26 Aug 2013 14:06:31 -0400 Subject: [PATCH 06/12] Added better documentation to Look and Feel setup --- .../autopsy/corecomponents/Installer.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java index 62cfacb5b3..f6ab3e5b57 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java @@ -91,9 +91,12 @@ public class Installer extends ModuleInstall { } + /** + * Set the look and feel to be the Cross Platform 'Metal', but keep Aqua + * dependent elements that set the Menu Bar to be in the correct place on + * Mac OS X. + */ private void setupMacOsXLAF() { - Logger logger = Logger.getLogger(Installer.class.getName()); - try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException @@ -101,21 +104,22 @@ public class Installer extends ModuleInstall { logger.log(Level.WARNING, "Unable to set theme. ", ex); } - final String[] UI_KEYS = new String[]{"MenuBarUI", - "MenuUI", - "MenuItemUI", - "CheckBoxMenuItemUI", - "RadioButtonMenuItemUI", - "PopupMenuUI"}; + final String[] UI_MENU_ITEM_KEYS = new String[]{"MenuBarUI", + "MenuUI", + "MenuItemUI", + "CheckBoxMenuItemUI", + "RadioButtonMenuItemUI", + "PopupMenuUI"}; Map uiEntries = new TreeMap(); - for(String key : UI_KEYS) { + // Store the keys that deal with menu items + for(String key : UI_MENU_ITEM_KEYS) { uiEntries.put(key, UIManager.get(key)); } - //use Nimbus if available + //use Metal if available for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { if ("Metal".equals(info.getName())) { try { @@ -128,6 +132,7 @@ public class Installer extends ModuleInstall { } } + // Overwrite the Metal menu item keys to use the Aqua versions for(Map.Entry entry : uiEntries.entrySet()) { UIManager.put(entry.getKey(), entry.getValue()); } From bf60d352585fd3651cc9970623a7cdbbc9526be3 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Fri, 30 Aug 2013 13:12:26 -0400 Subject: [PATCH 07/12] Updated JavaFx References to use newest JavaFX API. --- .../org/sleuthkit/autopsy/core/Installer.java | 23 +- .../autopsy/corecomponents/FXVideoPanel.java | 323 ++++++++++++------ .../sleuthkit/autopsy/timeline/Timeline.java | 11 +- 3 files changed, 229 insertions(+), 128 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index d2b0409d1f..be5e6a0066 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -18,11 +18,11 @@ */ package org.sleuthkit.autopsy.core; -import com.sun.javafx.application.PlatformImpl; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.modules.ModuleInstall; import org.openide.windows.WindowManager; @@ -37,10 +37,11 @@ public class Installer extends ModuleInstall { private List packageInstallers; private static final Logger logger = Logger.getLogger(Installer.class.getName()); - private volatile boolean javaFxInit = true; + private static volatile boolean javaFxInit = false; public Installer() { - javaFxInit = true; + logger.log(Level.INFO, "core installer created"); + javaFxInit = false; packageInstallers = new ArrayList(); packageInstallers.add(org.sleuthkit.autopsy.coreutils.Installer.getDefault()); @@ -54,24 +55,20 @@ public class Installer extends ModuleInstall { * Check if JavaFx initialized * @return false if java fx not initialized (classes coult not load), true if initialized */ - public boolean isJavaFxInited() { - return this.javaFxInit; + public static boolean isJavaFxInited() { + return javaFxInit; } - private void initJavaFx() { + private static void initJavaFx() { //initialize java fx if exists System.setProperty("javafx.macosx.embedded", "true"); try { + // Creating a JFXPanel initializes JavaFX + new JFXPanel(); Platform.setImplicitExit(false); - PlatformImpl.startup(new Runnable() { - @Override - public void run() { - logger.log(Level.INFO, "Initializing JavaFX for image viewing"); - } - }); + javaFxInit = true; } catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) { //in case javafx not present - javaFxInit = false; final String msg = "Error initializing JavaFX. "; final String details = " Some features will not be available. " + " Check that you have the right JRE installed (Oracle JRE > 1.7.10). "; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java index e3faf121c6..d09f48e13a 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java @@ -18,16 +18,14 @@ */ package org.sleuthkit.autopsy.corecomponents; -import com.sun.javafx.application.PlatformImpl; import java.awt.Component; import java.awt.Dimension; -import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.logging.Level; +import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.value.ChangeListener; @@ -44,14 +42,15 @@ import javafx.scene.control.Slider; import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.media.Media; +import javafx.scene.media.MediaBuilder; import javafx.scene.media.MediaException; import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaPlayer.Status; import static javafx.scene.media.MediaPlayer.Status.READY; +import javafx.scene.media.MediaPlayerBuilder; import javafx.scene.media.MediaView; import javafx.util.Duration; import javax.swing.BoxLayout; @@ -60,9 +59,7 @@ import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; -import org.openide.modules.ModuleInstall; import org.openide.util.Cancellable; -import org.openide.util.Exceptions; import org.openide.util.lookup.ServiceProvider; import org.openide.util.lookup.ServiceProviders; import org.sleuthkit.autopsy.casemodule.Case; @@ -71,6 +68,7 @@ import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.autopsy.core.Installer; /** * Video viewer part of the Media View layered pane. @@ -83,7 +81,6 @@ public class FXVideoPanel extends MediaViewVideoPanel { private static final Logger logger = Logger.getLogger(MediaViewVideoPanel.class.getName()); private boolean fxInited = false; // FX Components - private MediaPlayer fxMediaPlayer; private MediaPane mediaPane; // Current media content representations private AbstractFile currentFile; @@ -94,11 +91,7 @@ public class FXVideoPanel extends MediaViewVideoPanel { * Creates new form MediaViewVideoPanel */ public FXVideoPanel() { - org.sleuthkit.autopsy.core.Installer coreInstaller = - ModuleInstall.findObject(org.sleuthkit.autopsy.core.Installer.class, false); - if (coreInstaller != null) { - fxInited = coreInstaller.isJavaFxInited(); - } + fxInited = Installer.isJavaFxInited(); initComponents(); customizeComponents(); } @@ -112,7 +105,25 @@ public class FXVideoPanel extends MediaViewVideoPanel { } private void customizeComponents() { - setupFx(); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + videoComponent = new JFXPanel(); + + videoPanel.removeAll(); + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.add(videoComponent); + videoPanel.setVisible(true); + + Platform.runLater(new Runnable() { + @Override + public void run() { + setupFx(); + } + }); + } + }); + } @@ -143,47 +154,20 @@ public class FXVideoPanel extends MediaViewVideoPanel { if(!fxInited) { return; } - logger.log(Level.INFO, "In Setup FX"); - PlatformImpl.runLater(new Runnable() { - @Override - public void run() { - mediaPane = new MediaPane(); - logger.log(Level.INFO, "Created MediaPane"); - Scene fxScene = new Scene(mediaPane); - videoComponent = new JFXPanel(); - videoComponent.setScene(fxScene); - - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - // Configure VideoPanel - videoPanel.removeAll(); - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - videoPanel.add(videoComponent); - videoPanel.setVisible(true); - } - }); - } - }); + mediaPane = new MediaPane(); + Scene fxScene = new Scene(mediaPane); + videoComponent.setScene(fxScene); } @Override void reset() { - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { - if (fxMediaPlayer != null) { - if (fxMediaPlayer.getStatus() == MediaPlayer.Status.PLAYING ) { - fxMediaPlayer.stop(); - } - fxMediaPlayer = null; - } - - - if (videoComponent != null) { - videoComponent = null; + if (mediaPane != null) { + mediaPane.reset(); } } }); @@ -271,8 +255,13 @@ public class FXVideoPanel extends MediaViewVideoPanel { return extractedBytes; } - public Media getMedia() { - return new Media(Paths.get(jFile.getAbsolutePath()).toUri().toString()); + /** + * Get the URI of the media file. + * + * @return the URI of the media file. + */ + public String getMediaUri() { + return Paths.get(jFile.getAbsolutePath()).toUri().toString(); } @Override @@ -313,13 +302,10 @@ public class FXVideoPanel extends MediaViewVideoPanel { if (!this.isCancelled()) { logger.log(Level.INFO, "ExtractMedia in done: " + jFile.getName()); try { - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { - fxMediaPlayer = new MediaPlayer(getMedia()); - logger.log(Level.INFO, "Fx Media Player null? " + (fxMediaPlayer == null)); - logger.log(Level.INFO, "Media Tools null? " + (mediaPane == null)); - mediaPane.setMediaPlayer(fxMediaPlayer); + mediaPane.prepareMedia(getMediaUri()); } }); } catch(MediaException e) { @@ -333,12 +319,22 @@ public class FXVideoPanel extends MediaViewVideoPanel { } } + /** + * The JavaFX Component that contains the Media and it's Controls. + * + */ private class MediaPane extends BorderPane { private MediaPlayer mediaPlayer; private MediaView mediaView; + /** The Duration of the media. **/ private Duration duration; + + /** The container for the media controls. **/ private HBox mediaTools; + + /** The container for the media video output. **/ private HBox mediaViewPane; + private Slider progressSlider; private Button pauseButton; private Label progressLabel; @@ -348,12 +344,22 @@ public class FXVideoPanel extends MediaViewVideoPanel { private int totalSeconds; private String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; + /** The Listener for MediaPlayer.onReady(). **/ + private final ReadyListener READY_LISTENER = new ReadyListener(); + + /** The Listener for MediaPlayer.onEndOfMedia(). **/ + private final EndOfMediaListener END_LISTENER = new EndOfMediaListener(); + + /** The Listener for the CurrentTime property of the MediaPlayer. **/ + private final TimeListener TIME_LISTENER = new TimeListener(); + public MediaPane() { // Video Display mediaViewPane = new HBox(); mediaViewPane.setStyle("-fx-background-color: black"); mediaViewPane.setAlignment(Pos.CENTER); mediaView = new MediaView(); + mediaView.setPreserveRatio(true); mediaViewPane.getChildren().add(mediaView); setAlignment(mediaViewPane, Pos.CENTER); setCenter(mediaViewPane); @@ -383,10 +389,43 @@ public class FXVideoPanel extends MediaViewVideoPanel { controlPanel.getChildren().add(infoLabel); setBottom(controlPanel); setProgressActionListeners(); + disableControls(true); } + /** + * Setup the MediaPane for media playback. + * + * @param mediaUri the URI of the media + */ + public void prepareMedia(String mediaUri) { + disableControls(true); + mediaPlayer = createMediaPlayer(mediaUri); + mediaView.setMediaPlayer(mediaPlayer); + disableControls(false); + } + + /** + * Reset this MediaPane. + * + */ + public void reset() { + if (mediaPlayer != null) { + if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING ) { + mediaPlayer.stop(); + } + mediaPlayer = null; + } + setInfoLabelText(""); + disableControls(true); + } + + /** + * Set the Information Label of this MediaPane. + * + * @param text + */ public void setInfoLabelText(final String text) { - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { infoLabel.setText(text); @@ -394,54 +433,9 @@ public class FXVideoPanel extends MediaViewVideoPanel { }); } - public void setMediaPlayer(MediaPlayer mp) { - pauseButton.setDisable(true); - mediaPlayer = mp; - mediaView.setMediaPlayer(mp); - pauseButton.setDisable(false); - - setMediaActionListeners(); - } - - private void setMediaActionListeners() { - mediaPlayer.setOnReady(new Runnable() { - @Override - public void run() { - duration = mediaPlayer.getMedia().getDuration(); - long durationInMillis = (long) fxMediaPlayer.getMedia().getDuration().toMillis(); - - // pick out the total hours, minutes, seconds - long durationSeconds = (int) durationInMillis / 1000; - totalHours = (int) durationSeconds / 3600; - durationSeconds -= totalHours * 3600; - totalMinutes = (int) durationSeconds / 60; - durationSeconds -= totalMinutes * 60; - totalSeconds = (int) durationSeconds; - updateProgress(); - } - }); - - mediaPlayer.setOnEndOfMedia(new Runnable() { - @Override - public void run() { - Duration beginning = mediaPlayer.getStartTime(); - mediaPlayer.stop(); - mediaPlayer.pause(); - pauseButton.setText("►"); - updateSlider(beginning); - updateTime(beginning); - } - }); - - mediaPlayer.currentTimeProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { - updateSlider(newValue); - updateTime(newValue); - } - }); - } - + /** + * Set the action listeners for the pause button and progress slider. + */ private void setProgressActionListeners() { pauseButton.setOnAction(new EventHandler() { @Override @@ -462,6 +456,11 @@ public class FXVideoPanel extends MediaViewVideoPanel { mediaPlayer.play(); break; default: + logger.log(Level.INFO, "MediaPlayer in unexpected state: " + status.toString()); + // If the MediaPlayer is in an unexpected state, stop playback. + pauseButton.setText("►"); + mediaPlayer.pause(); + setInfoLabelText("Playback error."); break; } } @@ -477,12 +476,60 @@ public class FXVideoPanel extends MediaViewVideoPanel { }); } + /** + * Toggle the availability of the progress slider and pause button. + * + * @param disable true to disable controls + * false to enable controls + */ + private void disableControls(final boolean disable) { + Platform.runLater(new Runnable() { + @Override + public void run() { + pauseButton.setDisable(disable); + progressSlider.setDisable(disable); + } + }); + } + + /** + * Construct a MediaPlayer from the given Media URI. + * + * Also adds the necessary listeners to MediaPlayer events. + * + * @param mediaUri the location of the media. + * @return a MediaPlayer + */ + private MediaPlayer createMediaPlayer(String mediaUri) { + MediaBuilder mediaBuilder = MediaBuilder.create(); + mediaBuilder.source(mediaUri); + Media media = mediaBuilder.build(); + + MediaPlayerBuilder mediaPlayerBuilder = MediaPlayerBuilder.create(); + mediaPlayerBuilder.media(media); + mediaPlayerBuilder.onReady(READY_LISTENER); + mediaPlayerBuilder.onEndOfMedia(END_LISTENER); + + MediaPlayer player = mediaPlayerBuilder.build(); + player.currentTimeProperty().addListener(TIME_LISTENER); + + return player; + } + + /** + * Update the progress slider and label with the current time of the media. + */ private void updateProgress() { Duration currentTime = mediaPlayer.getCurrentTime(); updateSlider(currentTime); updateTime(currentTime); } + /** + * Update the slider with the current time. + * + * @param currentTime + */ private void updateSlider(Duration currentTime) { if (progressSlider != null) { progressSlider.setDisable(duration.isUnknown()); @@ -493,6 +540,11 @@ public class FXVideoPanel extends MediaViewVideoPanel { } } + /** + * Update the progress label with the current time. + * + * @param currentTime + */ private void updateTime(Duration currentTime) { long millisElapsed = (long) currentTime.toMillis(); @@ -508,11 +560,16 @@ public class FXVideoPanel extends MediaViewVideoPanel { String durationStr = String.format(durationFormat, elapsedHours, elapsedMinutes, elapsedSeconds, totalHours, totalMinutes, totalSeconds); - progressLabel.setText(durationStr); + setProgressLabelText(durationStr); } + /** + * Update the progress label to show the text. + * + * @param text + */ private void setProgressLabelText(final String text) { - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { progressLabel.setText(text); @@ -521,13 +578,65 @@ public class FXVideoPanel extends MediaViewVideoPanel { } private void setInfoLabelToolTipText(final String text) { - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { infoLabel.setTooltip(new Tooltip(text)); } }); } + + /** + * Responds to MediaPlayer onReady events. + * + * Updates the progress label with the duration of the media. + */ + private class ReadyListener implements Runnable { + @Override + public void run() { + duration = mediaPlayer.getMedia().getDuration(); + long durationInMillis = (long) mediaPlayer.getMedia().getDuration().toMillis(); + + // pick out the total hours, minutes, seconds + long durationSeconds = (int) durationInMillis / 1000; + totalHours = (int) durationSeconds / 3600; + durationSeconds -= totalHours * 3600; + totalMinutes = (int) durationSeconds / 60; + durationSeconds -= totalMinutes * 60; + totalSeconds = (int) durationSeconds; + updateProgress(); + } + } + + /** + * Responds to MediaPlayer onEndOfMediaEvents. + * + * Prepares the media to be replayed. + */ + private class EndOfMediaListener implements Runnable { + @Override + public void run() { + Duration beginning = mediaPlayer.getStartTime(); + mediaPlayer.stop(); + mediaPlayer.pause(); + pauseButton.setText("►"); + updateSlider(beginning); + updateTime(beginning); + } + } + + /** + * Responds changes in the MediaPlayer currentTime property. + * + * Updates the progress slider and label with the current Time. + */ + private class TimeListener implements ChangeListener { + @Override + public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { + updateSlider(newValue); + updateTime(newValue); + } + } } /** @@ -572,7 +681,7 @@ public class FXVideoPanel extends MediaViewVideoPanel { // } // // private void initFx(final java.io.File file) { -// PlatformImpl.runAndWait(new Runnable() { +// Platform.runAndWait(new Runnable() { // @Override // public void run() { // logger.log(Level.INFO, "In initFX."); @@ -626,7 +735,7 @@ public class FXVideoPanel extends MediaViewVideoPanel { // logger.log(Level.INFO, "Grabbing a frame..."); // final long timeStamp = i * frameInterval + INTER_FRAME_PERIOD_MS; // -// // PlatformImpl.runLater(new Runnable() { +// // Platform.runLater(new Runnable() { // // @Override // // public void run() { // // synchronized (frameLock) { diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java b/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java index 01977bfabd..781da5158a 100644 --- a/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/Timeline.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.timeline; -import com.sun.javafx.application.PlatformImpl; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; @@ -86,6 +85,7 @@ import org.openide.util.actions.Presenter; import org.openide.util.lookup.Lookups; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.corecomponents.DataContentPanel; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.coreutils.Logger; @@ -98,7 +98,6 @@ import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.coreutils.ExecUtil; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -147,11 +146,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar, public Timeline() { super(); - org.sleuthkit.autopsy.core.Installer coreInstaller = - ModuleInstall.findObject(org.sleuthkit.autopsy.core.Installer.class, false); - if (coreInstaller != null) { - fxInited = coreInstaller.isJavaFxInited(); - } + fxInited = Installer.isJavaFxInited(); } @@ -211,7 +206,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar, //JavaFX thread //JavaFX components MUST be run in the JavaFX thread, otherwise massive amounts of exceptions will be thrown and caught. Liable to freeze up and crash. //Components can be declared whenever, but initialization and manipulation must take place here. - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { try { From 02b82c1a86373ba4431a63eea23b2d59ed7f209b Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Fri, 30 Aug 2013 13:13:19 -0400 Subject: [PATCH 08/12] Changed LNF to more modern Nimbus. --- Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java index f6ab3e5b57..ecf5b847b7 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java @@ -121,7 +121,7 @@ public class Installer extends ModuleInstall { //use Metal if available for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if ("Metal".equals(info.getName())) { + if ("Nimbus".equals(info.getName())) { try { UIManager.setLookAndFeel(info.getClassName()); } catch (ClassNotFoundException | InstantiationException | From 14a77f5d0cfac4c426f78d4ae1fac1f5e00c37c4 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Fri, 30 Aug 2013 13:14:00 -0400 Subject: [PATCH 09/12] Added updated JavaFx reference that was missed in earlier commit --- .../corecomponents/MediaViewImagePanel.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index 8589b0ea7d..04d8f458ee 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -18,14 +18,13 @@ */ package org.sleuthkit.autopsy.corecomponents; -import com.sun.javafx.application.PlatformImpl; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; -import java.util.Collection; import java.util.logging.Level; +import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.embed.swing.SwingFXUtils; import javafx.scene.Scene; @@ -34,7 +33,6 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javax.imageio.ImageIO; import org.openide.modules.ModuleInstall; -import org.openide.util.Lookup; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.coreutils.Logger; @@ -61,11 +59,7 @@ public class MediaViewImagePanel extends javax.swing.JPanel { - org.sleuthkit.autopsy.core.Installer coreInstaller = - ModuleInstall.findObject(org.sleuthkit.autopsy.core.Installer.class, false); - if (coreInstaller != null) { - fxInited = coreInstaller.isJavaFxInited(); - } + fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); if (fxInited) { @@ -82,7 +76,7 @@ public class MediaViewImagePanel extends javax.swing.JPanel { */ private void setupFx() { // load the image - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { fxPanel = new JFXPanel(); @@ -129,7 +123,7 @@ public class MediaViewImagePanel extends javax.swing.JPanel { fxPanel.setVisible(false); // load the image - PlatformImpl.runLater(new Runnable() { + Platform.runLater(new Runnable() { @Override public void run() { if (!Case.isCaseOpen()) { From 27b401031d220e1851f7693d8a730bc5fa344984 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Tue, 3 Sep 2013 09:49:59 -0400 Subject: [PATCH 10/12] Fixed null pointer exception on closing case. --- .../autopsy/corecomponents/FXVideoPanel.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java index d09f48e13a..6214e674ec 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java @@ -108,11 +108,8 @@ public class FXVideoPanel extends MediaViewVideoPanel { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - videoComponent = new JFXPanel(); - videoPanel.removeAll(); videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - videoPanel.add(videoComponent); videoPanel.setVisible(true); Platform.runLater(new Runnable() { @@ -154,9 +151,18 @@ public class FXVideoPanel extends MediaViewVideoPanel { if(!fxInited) { return; } + + videoComponent = new JFXPanel(); mediaPane = new MediaPane(); Scene fxScene = new Scene(mediaPane); videoComponent.setScene(fxScene); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + videoPanel.add(videoComponent); + } + }); } @@ -454,6 +460,10 @@ public class FXVideoPanel extends MediaViewVideoPanel { case STOPPED: pauseButton.setText("||"); mediaPlayer.play(); + if(mediaPlayer.getStatus() == Status.PAUSED) { + mediaPlayer.stop(); + setInfoLabelText("Playback error. File may be corrupted."); + } break; default: logger.log(Level.INFO, "MediaPlayer in unexpected state: " + status.toString()); From 437179b4fd0d00c3e8915481a750e705d201fc6e Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Thu, 5 Sep 2013 11:14:06 -0400 Subject: [PATCH 11/12] Added .form files for Video Panels --- .../autopsy/corecomponents/FXVideoPanel.form | 45 + .../autopsy/corecomponents/FXVideoPanel.java | 1538 ++++++++-------- .../autopsy/corecomponents/GstVideoPanel.form | 123 ++ .../autopsy/corecomponents/GstVideoPanel.java | 1540 +++++++++-------- 4 files changed, 1713 insertions(+), 1533 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.form create mode 100644 Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.form new file mode 100644 index 0000000000..b3d3d62704 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.form @@ -0,0 +1,45 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java index 6214e674ec..694096642a 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/FXVideoPanel.java @@ -1,767 +1,771 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.corecomponents; - -import java.awt.Component; -import java.awt.Dimension; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.List; -import java.util.concurrent.CancellationException; -import java.util.logging.Level; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.embed.swing.JFXPanel; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Slider; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; -import javafx.scene.media.Media; -import javafx.scene.media.MediaBuilder; -import javafx.scene.media.MediaException; -import javafx.scene.media.MediaPlayer; -import javafx.scene.media.MediaPlayer.Status; -import static javafx.scene.media.MediaPlayer.Status.READY; -import javafx.scene.media.MediaPlayerBuilder; -import javafx.scene.media.MediaView; -import javafx.util.Duration; -import javax.swing.BoxLayout; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; -import org.netbeans.api.progress.ProgressHandle; -import org.netbeans.api.progress.ProgressHandleFactory; -import org.openide.util.Cancellable; -import org.openide.util.lookup.ServiceProvider; -import org.openide.util.lookup.ServiceProviders; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; -import org.sleuthkit.autopsy.core.Installer; - -/** - * Video viewer part of the Media View layered pane. - */ -@ServiceProviders(value = { - @ServiceProvider(service = FrameCapture.class) -}) -public class FXVideoPanel extends MediaViewVideoPanel { - - private static final Logger logger = Logger.getLogger(MediaViewVideoPanel.class.getName()); - private boolean fxInited = false; - // FX Components - private MediaPane mediaPane; - // Current media content representations - private AbstractFile currentFile; - // FX UI Components - private JFXPanel videoComponent; - - /** - * Creates new form MediaViewVideoPanel - */ - public FXVideoPanel() { - fxInited = Installer.isJavaFxInited(); - initComponents(); - customizeComponents(); - } - - public JPanel getVideoPanel() { - return videoPanel; - } - - public Component getVideoComponent() { - return videoComponent; - } - - private void customizeComponents() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - videoPanel.removeAll(); - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - videoPanel.setVisible(true); - - Platform.runLater(new Runnable() { - @Override - public void run() { - setupFx(); - } - }); - } - }); - - } - - - @Override - synchronized void setupVideo(final AbstractFile file, final Dimension dims) { - currentFile = file; - final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); - if (deleted) { - mediaPane.setInfoLabelText("Playback of deleted videos is not supported, use an external player."); - videoPanel.removeAll(); - return; - } - - String path = ""; - try { - path = file.getUniquePath(); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Cannot get unique path of video file"); - } - mediaPane.setInfoLabelText(path); - mediaPane.setInfoLabelToolTipText(path); - - ExtractMedia em = new ExtractMedia(currentFile, getJFile(currentFile)); - em.execute(); - } - - synchronized void setupFx() { - if(!fxInited) { - return; - } - - videoComponent = new JFXPanel(); - mediaPane = new MediaPane(); - Scene fxScene = new Scene(mediaPane); - videoComponent.setScene(fxScene); - - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - videoPanel.add(videoComponent); - } - }); - } - - - @Override - void reset() { - - Platform.runLater(new Runnable() { - @Override - public void run() { - if (mediaPane != null) { - mediaPane.reset(); - } - } - }); - - - currentFile = null; - } - - private java.io.File getJFile(AbstractFile file) { - // Get the temp folder path of the case - String tempPath = Case.getCurrentCase().getTempDirectory(); - String name = file.getName(); - int extStart = name.lastIndexOf("."); - String ext = ""; - if (extStart != -1) { - ext = name.substring(extStart, name.length()).toLowerCase(); - } - tempPath = tempPath + java.io.File.separator + file.getId() + ext; - - java.io.File tempFile = new java.io.File(tempPath); - return tempFile; - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // - private void initComponents() { - - videoPanel = new javax.swing.JPanel(); - - javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); - videoPanel.setLayout(videoPanelLayout); - videoPanelLayout.setHorizontalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 448, Short.MAX_VALUE) - ); - videoPanelLayout.setVerticalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 248, Short.MAX_VALUE) - ); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(videoPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - }// - - // Variables declaration - do not modify - private javax.swing.JPanel videoPanel; - // End of variables declaration - - @Override - public boolean isInited() { - return fxInited; - } - - /** - * Thread that extracts Media from a Sleuthkit file representation to a - * Java file representation that the Media Player can take as input. - */ - private class ExtractMedia extends SwingWorker { - - private ProgressHandle progress; - boolean success = false; - private AbstractFile sFile; - private java.io.File jFile; - private long extractedBytes; - - ExtractMedia(org.sleuthkit.datamodel.AbstractFile sFile, java.io.File jFile) { - this.sFile = sFile; - this.jFile = jFile; - } - - public long getExtractedBytes() { - return extractedBytes; - } - - /** - * Get the URI of the media file. - * - * @return the URI of the media file. - */ - public String getMediaUri() { - return Paths.get(jFile.getAbsolutePath()).toUri().toString(); - } - - @Override - protected Object doInBackground() throws Exception { - success = false; - progress = ProgressHandleFactory.createHandle("Buffering " + sFile.getName(), new Cancellable() { - @Override - public boolean cancel() { - return ExtractMedia.this.cancel(true); - } - }); - mediaPane.setProgressLabelText("Buffering... "); - progress.start(); - progress.switchToDeterminate(100); - try { - extractedBytes = ContentUtils.writeToFile(sFile, jFile, progress, this, true); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error buffering file", ex); - } - logger.log(Level.INFO, "Done buffering: " + jFile.getName()); - success = true; - return null; - } - - /* clean up or start the worker threads */ - @Override - protected void done() { - try { - super.get(); //block and get all exceptions thrown while doInBackground() - } catch (CancellationException ex) { - logger.log(Level.INFO, "Media buffering was canceled."); - } catch (InterruptedException ex) { - logger.log(Level.INFO, "Media buffering was interrupted."); - } catch (Exception ex) { - logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); - } finally { - progress.finish(); - if (!this.isCancelled()) { - logger.log(Level.INFO, "ExtractMedia in done: " + jFile.getName()); - try { - Platform.runLater(new Runnable() { - @Override - public void run() { - mediaPane.prepareMedia(getMediaUri()); - } - }); - } catch(MediaException e) { - logger.log(Level.WARNING, "something went wrong with javafx", e); - reset(); - mediaPane.setInfoLabelText(e.getMessage()); - return; - } - } - } - } - } - - /** - * The JavaFX Component that contains the Media and it's Controls. - * - */ - private class MediaPane extends BorderPane { - private MediaPlayer mediaPlayer; - private MediaView mediaView; - /** The Duration of the media. **/ - private Duration duration; - - /** The container for the media controls. **/ - private HBox mediaTools; - - /** The container for the media video output. **/ - private HBox mediaViewPane; - - private Slider progressSlider; - private Button pauseButton; - private Label progressLabel; - private Label infoLabel; - private int totalHours; - private int totalMinutes; - private int totalSeconds; - private String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; - - /** The Listener for MediaPlayer.onReady(). **/ - private final ReadyListener READY_LISTENER = new ReadyListener(); - - /** The Listener for MediaPlayer.onEndOfMedia(). **/ - private final EndOfMediaListener END_LISTENER = new EndOfMediaListener(); - - /** The Listener for the CurrentTime property of the MediaPlayer. **/ - private final TimeListener TIME_LISTENER = new TimeListener(); - - public MediaPane() { - // Video Display - mediaViewPane = new HBox(); - mediaViewPane.setStyle("-fx-background-color: black"); - mediaViewPane.setAlignment(Pos.CENTER); - mediaView = new MediaView(); - mediaView.setPreserveRatio(true); - mediaViewPane.getChildren().add(mediaView); - setAlignment(mediaViewPane, Pos.CENTER); - setCenter(mediaViewPane); - - // Media Controls - VBox controlPanel = new VBox(); - mediaTools = new HBox(); - mediaTools.setAlignment(Pos.CENTER); - mediaTools.setPadding(new Insets(5, 10, 5, 10)); - - pauseButton = new Button("►"); - mediaTools.getChildren().add(pauseButton); - mediaTools.getChildren().add(new Label(" ")); - progressSlider = new Slider(); - HBox.setHgrow(progressSlider,Priority.ALWAYS); - progressSlider.setMinWidth(50); - progressSlider.setMaxWidth(Double.MAX_VALUE); - mediaTools.getChildren().add(progressSlider); - progressLabel = new Label(); - progressLabel.setPrefWidth(130); - progressLabel.setMinWidth(50); - mediaTools.getChildren().add(progressLabel); - - controlPanel.getChildren().add(mediaTools); - controlPanel.setStyle("-fx-background-color: white"); - infoLabel = new Label(""); - controlPanel.getChildren().add(infoLabel); - setBottom(controlPanel); - setProgressActionListeners(); - disableControls(true); - } - - /** - * Setup the MediaPane for media playback. - * - * @param mediaUri the URI of the media - */ - public void prepareMedia(String mediaUri) { - disableControls(true); - mediaPlayer = createMediaPlayer(mediaUri); - mediaView.setMediaPlayer(mediaPlayer); - disableControls(false); - } - - /** - * Reset this MediaPane. - * - */ - public void reset() { - if (mediaPlayer != null) { - if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING ) { - mediaPlayer.stop(); - } - mediaPlayer = null; - } - setInfoLabelText(""); - disableControls(true); - } - - /** - * Set the Information Label of this MediaPane. - * - * @param text - */ - public void setInfoLabelText(final String text) { - Platform.runLater(new Runnable() { - @Override - public void run() { - infoLabel.setText(text); - } - }); - } - - /** - * Set the action listeners for the pause button and progress slider. - */ - private void setProgressActionListeners() { - pauseButton.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent e) { - Status status = mediaPlayer.getStatus(); - - switch (status) { - // If playing, pause - case PLAYING: - pauseButton.setText("►"); - mediaPlayer.pause(); - break; - // If ready, paused or stopped, continue playing - case READY: - case PAUSED: - case STOPPED: - pauseButton.setText("||"); - mediaPlayer.play(); - if(mediaPlayer.getStatus() == Status.PAUSED) { - mediaPlayer.stop(); - setInfoLabelText("Playback error. File may be corrupted."); - } - break; - default: - logger.log(Level.INFO, "MediaPlayer in unexpected state: " + status.toString()); - // If the MediaPlayer is in an unexpected state, stop playback. - pauseButton.setText("►"); - mediaPlayer.pause(); - setInfoLabelText("Playback error."); - break; - } - } - }); - - progressSlider.valueProperty().addListener(new InvalidationListener() { - @Override - public void invalidated(Observable o) { - if (progressSlider.isValueChanging()) { - mediaPlayer.seek(duration.multiply(progressSlider.getValue() / 100.0)); - } - } - }); - } - - /** - * Toggle the availability of the progress slider and pause button. - * - * @param disable true to disable controls - * false to enable controls - */ - private void disableControls(final boolean disable) { - Platform.runLater(new Runnable() { - @Override - public void run() { - pauseButton.setDisable(disable); - progressSlider.setDisable(disable); - } - }); - } - - /** - * Construct a MediaPlayer from the given Media URI. - * - * Also adds the necessary listeners to MediaPlayer events. - * - * @param mediaUri the location of the media. - * @return a MediaPlayer - */ - private MediaPlayer createMediaPlayer(String mediaUri) { - MediaBuilder mediaBuilder = MediaBuilder.create(); - mediaBuilder.source(mediaUri); - Media media = mediaBuilder.build(); - - MediaPlayerBuilder mediaPlayerBuilder = MediaPlayerBuilder.create(); - mediaPlayerBuilder.media(media); - mediaPlayerBuilder.onReady(READY_LISTENER); - mediaPlayerBuilder.onEndOfMedia(END_LISTENER); - - MediaPlayer player = mediaPlayerBuilder.build(); - player.currentTimeProperty().addListener(TIME_LISTENER); - - return player; - } - - /** - * Update the progress slider and label with the current time of the media. - */ - private void updateProgress() { - Duration currentTime = mediaPlayer.getCurrentTime(); - updateSlider(currentTime); - updateTime(currentTime); - } - - /** - * Update the slider with the current time. - * - * @param currentTime - */ - private void updateSlider(Duration currentTime) { - if (progressSlider != null) { - progressSlider.setDisable(duration.isUnknown()); - if (!progressSlider.isDisabled() && duration.greaterThan(Duration.ZERO) - && !progressSlider.isValueChanging()) { - progressSlider.setValue(currentTime.divide(duration.toMillis()).toMillis() * 100.0); - } - } - } - - /** - * Update the progress label with the current time. - * - * @param currentTime - */ - private void updateTime(Duration currentTime) { - long millisElapsed = (long) currentTime.toMillis(); - - long elapsedHours, elapsedMinutes, elapsedSeconds; - // pick out the elapsed hours, minutes, seconds - long secondsElapsed = millisElapsed / 1000; - elapsedHours = (int) secondsElapsed / 3600; - secondsElapsed -= elapsedHours * 3600; - elapsedMinutes = (int) secondsElapsed / 60; - secondsElapsed -= elapsedMinutes * 60; - elapsedSeconds = (int) secondsElapsed; - - String durationStr = String.format(durationFormat, - elapsedHours, elapsedMinutes, elapsedSeconds, - totalHours, totalMinutes, totalSeconds); - setProgressLabelText(durationStr); - } - - /** - * Update the progress label to show the text. - * - * @param text - */ - private void setProgressLabelText(final String text) { - Platform.runLater(new Runnable() { - @Override - public void run() { - progressLabel.setText(text); - } - }); - } - - private void setInfoLabelToolTipText(final String text) { - Platform.runLater(new Runnable() { - @Override - public void run() { - infoLabel.setTooltip(new Tooltip(text)); - } - }); - } - - /** - * Responds to MediaPlayer onReady events. - * - * Updates the progress label with the duration of the media. - */ - private class ReadyListener implements Runnable { - @Override - public void run() { - duration = mediaPlayer.getMedia().getDuration(); - long durationInMillis = (long) mediaPlayer.getMedia().getDuration().toMillis(); - - // pick out the total hours, minutes, seconds - long durationSeconds = (int) durationInMillis / 1000; - totalHours = (int) durationSeconds / 3600; - durationSeconds -= totalHours * 3600; - totalMinutes = (int) durationSeconds / 60; - durationSeconds -= totalMinutes * 60; - totalSeconds = (int) durationSeconds; - updateProgress(); - } - } - - /** - * Responds to MediaPlayer onEndOfMediaEvents. - * - * Prepares the media to be replayed. - */ - private class EndOfMediaListener implements Runnable { - @Override - public void run() { - Duration beginning = mediaPlayer.getStartTime(); - mediaPlayer.stop(); - mediaPlayer.pause(); - pauseButton.setText("►"); - updateSlider(beginning); - updateTime(beginning); - } - } - - /** - * Responds changes in the MediaPlayer currentTime property. - * - * Updates the progress slider and label with the current Time. - */ - private class TimeListener implements ChangeListener { - @Override - public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { - updateSlider(newValue); - updateTime(newValue); - } - } - } - - /** - * @param file a video file from which to capture frames - * @param numFrames the number of frames to capture. These frames will be - * captured at successive intervals given by durationOfVideo/numFrames. If - * this frame interval is less than MIN_FRAME_INTERVAL_MILLIS, then only one - * frame will be captured and returned. - * @return a List of VideoFrames representing the captured frames. - */ - @Override - public List captureFrames(java.io.File file, int numFrames) throws Exception { -// -// try { -// List frames = new ArrayList<>(); -// -// FrameCapturer fc = new FrameCapturer(file); -// logger.log(Level.INFO, "Fc is null? " + (fc == null)); -// frames = fc.getFrames(numFrames); -// -// return frames; -// } -// catch (NullPointerException e) { -// e.printStackTrace(); -// return null; -// } - return null; - } - -// private class FrameCapturer { -// -// private MediaPlayer mediaPlayer; -// private JFXPanel panel; -// private boolean isReady = false; -// -// FrameCapturer(java.io.File file) { -// initFx(file); -// } -// -// boolean isReady() { -// return isReady; -// } -// -// private void initFx(final java.io.File file) { -// Platform.runAndWait(new Runnable() { -// @Override -// public void run() { -// logger.log(Level.INFO, "In initFX."); -// // Create Media Player with no video output -// Media media = new Media(Paths.get(file.getAbsolutePath()).toUri().toString()); -// mediaPlayer = new MediaPlayer(media); -// MediaView mediaView = new MediaView(mediaPlayer); -// mediaView.setStyle("-fx-background-color: black"); -// Pane mediaViewPane = new Pane(); -// mediaViewPane.getChildren().add(mediaView); -// Scene scene = new Scene(mediaViewPane); -// panel = new JFXPanel(); -// panel.setScene(scene); -// isReady = true; -// } -// }); -// } -// -// List getFrames(int numFrames) { -// logger.log(Level.INFO, "in get frames"); -// List frames = new ArrayList(0); -// -// if (mediaPlayer.getStatus() != Status.READY) { -// try { -// Thread.sleep(500); -// } catch (InterruptedException e) { -// return frames; -// } -// } -// -// // get the duration of the video -// long myDurationMillis = (long) mediaPlayer.getMedia().getDuration().toMillis(); -// if (myDurationMillis <= 0) { -// return frames; -// } -// -// // calculate the frame interval -// int numFramesToGet = numFrames; -// long frameInterval = (myDurationMillis - INTER_FRAME_PERIOD_MS) / numFrames; -// if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) { -// numFramesToGet = 1; -// } -// -// final Object frameLock = new Object(); -// BufferedImage frame; -// final int width = (int) panel.getSize().getWidth(); -// final int height = (int) panel.getSize().getHeight(); -// // for each timeStamp, grap a frame -// for (int i = 0; i < numFramesToGet; ++i) { -// frame = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); -// logger.log(Level.INFO, "Grabbing a frame..."); -// final long timeStamp = i * frameInterval + INTER_FRAME_PERIOD_MS; -// -// // Platform.runLater(new Runnable() { -// // @Override -// // public void run() { -// // synchronized (frameLock) { -// logger.log(Level.INFO, "seeking."); -// mediaPlayer.seek(new Duration(timeStamp)); -// // } -// // } -// // }); -// -// synchronized (frameLock) { -// panel.paint(frame.createGraphics()); -// logger.log(Level.INFO, "Adding image to frames"); -// } -// frames.add(new VideoFrame(frame, timeStamp)); -// } -// return frames; -// } -// } -} +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.corecomponents; + +import java.awt.Component; +import java.awt.Dimension; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.embed.swing.JFXPanel; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Slider; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import static javafx.scene.layout.BorderPane.setAlignment; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.media.Media; +import javafx.scene.media.MediaBuilder; +import javafx.scene.media.MediaException; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaPlayer.Status; +import static javafx.scene.media.MediaPlayer.Status.PAUSED; +import static javafx.scene.media.MediaPlayer.Status.PLAYING; +import static javafx.scene.media.MediaPlayer.Status.READY; +import static javafx.scene.media.MediaPlayer.Status.STOPPED; +import javafx.scene.media.MediaPlayerBuilder; +import javafx.scene.media.MediaView; +import javafx.util.Duration; +import javax.swing.BoxLayout; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.progress.ProgressHandleFactory; +import org.openide.util.Cancellable; +import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.autopsy.core.Installer; + +/** + * Video viewer part of the Media View layered pane. + */ +@ServiceProviders(value = { + @ServiceProvider(service = FrameCapture.class) +}) +public class FXVideoPanel extends MediaViewVideoPanel { + + private static final Logger logger = Logger.getLogger(MediaViewVideoPanel.class.getName()); + private boolean fxInited = false; + // FX Components + private FXVideoPanel.MediaPane mediaPane; + // Current media content representations + private AbstractFile currentFile; + // FX UI Components + private JFXPanel videoComponent; + + /** + * Creates new form MediaViewVideoPanel + */ + public FXVideoPanel() { + fxInited = Installer.isJavaFxInited(); + initComponents(); + customizeComponents(); + } + + public JPanel getVideoPanel() { + return videoPanel; + } + + public Component getVideoComponent() { + return videoComponent; + } + + private void customizeComponents() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + videoPanel.removeAll(); + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.setVisible(true); + + Platform.runLater(new Runnable() { + @Override + public void run() { + setupFx(); + } + }); + } + }); + + } + + + @Override + synchronized void setupVideo(final AbstractFile file, final Dimension dims) { + currentFile = file; + final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); + if (deleted) { + mediaPane.setInfoLabelText("Playback of deleted videos is not supported, use an external player."); + videoPanel.removeAll(); + return; + } + + String path = ""; + try { + path = file.getUniquePath(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Cannot get unique path of video file"); + } + mediaPane.setInfoLabelText(path); + mediaPane.setInfoLabelToolTipText(path); + + FXVideoPanel.ExtractMedia em = new FXVideoPanel.ExtractMedia(currentFile, getJFile(currentFile)); + em.execute(); + } + + synchronized void setupFx() { + if(!fxInited) { + return; + } + + videoComponent = new JFXPanel(); + mediaPane = new FXVideoPanel.MediaPane(); + Scene fxScene = new Scene(mediaPane); + videoComponent.setScene(fxScene); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + videoPanel.add(videoComponent); + } + }); + } + + + @Override + void reset() { + + Platform.runLater(new Runnable() { + @Override + public void run() { + if (mediaPane != null) { + mediaPane.reset(); + } + } + }); + + + currentFile = null; + } + + private java.io.File getJFile(AbstractFile file) { + // Get the temp folder path of the case + String tempPath = Case.getCurrentCase().getTempDirectory(); + String name = file.getName(); + int extStart = name.lastIndexOf("."); + String ext = ""; + if (extStart != -1) { + ext = name.substring(extStart, name.length()).toLowerCase(); + } + tempPath = tempPath + java.io.File.separator + file.getId() + ext; + + java.io.File tempFile = new java.io.File(tempPath); + return tempFile; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + videoPanel = new javax.swing.JPanel(); + + org.jdesktop.layout.GroupLayout videoPanelLayout = new org.jdesktop.layout.GroupLayout(videoPanel); + videoPanel.setLayout(videoPanelLayout); + videoPanelLayout.setHorizontalGroup( + videoPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(0, 400, Short.MAX_VALUE) + ); + videoPanelLayout.setVerticalGroup( + videoPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(0, 300, Short.MAX_VALUE) + ); + + org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(org.jdesktop.layout.GroupLayout.TRAILING, videoPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(org.jdesktop.layout.GroupLayout.TRAILING, videoPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel videoPanel; + // End of variables declaration//GEN-END:variables + + @Override + public boolean isInited() { + return fxInited; + } + + /** + * Thread that extracts Media from a Sleuthkit file representation to a + * Java file representation that the Media Player can take as input. + */ + private class ExtractMedia extends SwingWorker { + + private ProgressHandle progress; + boolean success = false; + private AbstractFile sFile; + private java.io.File jFile; + private long extractedBytes; + + ExtractMedia(org.sleuthkit.datamodel.AbstractFile sFile, java.io.File jFile) { + this.sFile = sFile; + this.jFile = jFile; + } + + public long getExtractedBytes() { + return extractedBytes; + } + + /** + * Get the URI of the media file. + * + * @return the URI of the media file. + */ + public String getMediaUri() { + return Paths.get(jFile.getAbsolutePath()).toUri().toString(); + } + + @Override + protected Object doInBackground() throws Exception { + success = false; + progress = ProgressHandleFactory.createHandle("Buffering " + sFile.getName(), new Cancellable() { + @Override + public boolean cancel() { + return FXVideoPanel.ExtractMedia.this.cancel(true); + } + }); + mediaPane.setProgressLabelText("Buffering... "); + progress.start(); + progress.switchToDeterminate(100); + try { + extractedBytes = ContentUtils.writeToFile(sFile, jFile, progress, this, true); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error buffering file", ex); + } + logger.log(Level.INFO, "Done buffering: " + jFile.getName()); + success = true; + return null; + } + + /* clean up or start the worker threads */ + @Override + protected void done() { + try { + super.get(); //block and get all exceptions thrown while doInBackground() + } catch (CancellationException ex) { + logger.log(Level.INFO, "Media buffering was canceled."); + } catch (InterruptedException ex) { + logger.log(Level.INFO, "Media buffering was interrupted."); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); + } finally { + progress.finish(); + if (!this.isCancelled()) { + logger.log(Level.INFO, "ExtractMedia in done: " + jFile.getName()); + try { + Platform.runLater(new Runnable() { + @Override + public void run() { + mediaPane.prepareMedia(getMediaUri()); + } + }); + } catch(MediaException e) { + logger.log(Level.WARNING, "something went wrong with javafx", e); + reset(); + mediaPane.setInfoLabelText(e.getMessage()); + return; + } + } + } + } + } + + /** + * The JavaFX Component that contains the Media and it's Controls. + * + */ + private class MediaPane extends BorderPane { + private MediaPlayer mediaPlayer; + private MediaView mediaView; + /** The Duration of the media. **/ + private Duration duration; + + /** The container for the media controls. **/ + private HBox mediaTools; + + /** The container for the media video output. **/ + private HBox mediaViewPane; + + private Slider progressSlider; + private Button pauseButton; + private Label progressLabel; + private Label infoLabel; + private int totalHours; + private int totalMinutes; + private int totalSeconds; + private String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; + + /** The Listener for MediaPlayer.onReady(). **/ + private final MediaPane.ReadyListener READY_LISTENER = new MediaPane.ReadyListener(); + + /** The Listener for MediaPlayer.onEndOfMedia(). **/ + private final MediaPane.EndOfMediaListener END_LISTENER = new MediaPane.EndOfMediaListener(); + + /** The Listener for the CurrentTime property of the MediaPlayer. **/ + private final MediaPane.TimeListener TIME_LISTENER = new MediaPane.TimeListener(); + + public MediaPane() { + // Video Display + mediaViewPane = new HBox(); + mediaViewPane.setStyle("-fx-background-color: black"); + mediaViewPane.setAlignment(Pos.CENTER); + mediaView = new MediaView(); + mediaView.setPreserveRatio(true); + mediaViewPane.getChildren().add(mediaView); + setAlignment(mediaViewPane, Pos.CENTER); + setCenter(mediaViewPane); + + // Media Controls + VBox controlPanel = new VBox(); + mediaTools = new HBox(); + mediaTools.setAlignment(Pos.CENTER); + mediaTools.setPadding(new Insets(5, 10, 5, 10)); + + pauseButton = new Button("►"); + mediaTools.getChildren().add(pauseButton); + mediaTools.getChildren().add(new Label(" ")); + progressSlider = new Slider(); + HBox.setHgrow(progressSlider,Priority.ALWAYS); + progressSlider.setMinWidth(50); + progressSlider.setMaxWidth(Double.MAX_VALUE); + mediaTools.getChildren().add(progressSlider); + progressLabel = new Label(); + progressLabel.setPrefWidth(130); + progressLabel.setMinWidth(50); + mediaTools.getChildren().add(progressLabel); + + controlPanel.getChildren().add(mediaTools); + controlPanel.setStyle("-fx-background-color: white"); + infoLabel = new Label(""); + controlPanel.getChildren().add(infoLabel); + setBottom(controlPanel); + setProgressActionListeners(); + disableControls(true); + } + + /** + * Setup the MediaPane for media playback. + * + * @param mediaUri the URI of the media + */ + public void prepareMedia(String mediaUri) { + disableControls(true); + mediaPlayer = createMediaPlayer(mediaUri); + mediaView.setMediaPlayer(mediaPlayer); + disableControls(false); + } + + /** + * Reset this MediaPane. + * + */ + public void reset() { + if (mediaPlayer != null) { + if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING ) { + mediaPlayer.stop(); + } + mediaPlayer = null; + } + setInfoLabelText(""); + disableControls(true); + } + + /** + * Set the Information Label of this MediaPane. + * + * @param text + */ + public void setInfoLabelText(final String text) { + Platform.runLater(new Runnable() { + @Override + public void run() { + infoLabel.setText(text); + } + }); + } + + /** + * Set the action listeners for the pause button and progress slider. + */ + private void setProgressActionListeners() { + pauseButton.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent e) { + Status status = mediaPlayer.getStatus(); + + switch (status) { + // If playing, pause + case PLAYING: + pauseButton.setText("►"); + mediaPlayer.pause(); + break; + // If ready, paused or stopped, continue playing + case READY: + case PAUSED: + case STOPPED: + pauseButton.setText("||"); + mediaPlayer.play(); + if(mediaPlayer.getStatus() == Status.PAUSED) { + mediaPlayer.stop(); + setInfoLabelText("Playback error. File may be corrupted."); + } + break; + default: + logger.log(Level.INFO, "MediaPlayer in unexpected state: " + status.toString()); + // If the MediaPlayer is in an unexpected state, stop playback. + pauseButton.setText("►"); + mediaPlayer.pause(); + setInfoLabelText("Playback error."); + break; + } + } + }); + + progressSlider.valueProperty().addListener(new InvalidationListener() { + @Override + public void invalidated(Observable o) { + if (progressSlider.isValueChanging()) { + mediaPlayer.seek(duration.multiply(progressSlider.getValue() / 100.0)); + } + } + }); + } + + /** + * Toggle the availability of the progress slider and pause button. + * + * @param disable true to disable controls + * false to enable controls + */ + private void disableControls(final boolean disable) { + Platform.runLater(new Runnable() { + @Override + public void run() { + pauseButton.setDisable(disable); + progressSlider.setDisable(disable); + } + }); + } + + /** + * Construct a MediaPlayer from the given Media URI. + * + * Also adds the necessary listeners to MediaPlayer events. + * + * @param mediaUri the location of the media. + * @return a MediaPlayer + */ + private MediaPlayer createMediaPlayer(String mediaUri) { + MediaBuilder mediaBuilder = MediaBuilder.create(); + mediaBuilder.source(mediaUri); + Media media = mediaBuilder.build(); + + MediaPlayerBuilder mediaPlayerBuilder = MediaPlayerBuilder.create(); + mediaPlayerBuilder.media(media); + mediaPlayerBuilder.onReady(READY_LISTENER); + mediaPlayerBuilder.onEndOfMedia(END_LISTENER); + + MediaPlayer player = mediaPlayerBuilder.build(); + player.currentTimeProperty().addListener(TIME_LISTENER); + + return player; + } + + /** + * Update the progress slider and label with the current time of the media. + */ + private void updateProgress() { + Duration currentTime = mediaPlayer.getCurrentTime(); + updateSlider(currentTime); + updateTime(currentTime); + } + + /** + * Update the slider with the current time. + * + * @param currentTime + */ + private void updateSlider(Duration currentTime) { + if (progressSlider != null) { + progressSlider.setDisable(duration.isUnknown()); + if (!progressSlider.isDisabled() && duration.greaterThan(Duration.ZERO) + && !progressSlider.isValueChanging()) { + progressSlider.setValue(currentTime.divide(duration.toMillis()).toMillis() * 100.0); + } + } + } + + /** + * Update the progress label with the current time. + * + * @param currentTime + */ + private void updateTime(Duration currentTime) { + long millisElapsed = (long) currentTime.toMillis(); + + long elapsedHours, elapsedMinutes, elapsedSeconds; + // pick out the elapsed hours, minutes, seconds + long secondsElapsed = millisElapsed / 1000; + elapsedHours = (int) secondsElapsed / 3600; + secondsElapsed -= elapsedHours * 3600; + elapsedMinutes = (int) secondsElapsed / 60; + secondsElapsed -= elapsedMinutes * 60; + elapsedSeconds = (int) secondsElapsed; + + String durationStr = String.format(durationFormat, + elapsedHours, elapsedMinutes, elapsedSeconds, + totalHours, totalMinutes, totalSeconds); + setProgressLabelText(durationStr); + } + + /** + * Update the progress label to show the text. + * + * @param text + */ + private void setProgressLabelText(final String text) { + Platform.runLater(new Runnable() { + @Override + public void run() { + progressLabel.setText(text); + } + }); + } + + private void setInfoLabelToolTipText(final String text) { + Platform.runLater(new Runnable() { + @Override + public void run() { + infoLabel.setTooltip(new Tooltip(text)); + } + }); + } + + /** + * Responds to MediaPlayer onReady events. + * + * Updates the progress label with the duration of the media. + */ + private class ReadyListener implements Runnable { + @Override + public void run() { + duration = mediaPlayer.getMedia().getDuration(); + long durationInMillis = (long) mediaPlayer.getMedia().getDuration().toMillis(); + + // pick out the total hours, minutes, seconds + long durationSeconds = (int) durationInMillis / 1000; + totalHours = (int) durationSeconds / 3600; + durationSeconds -= totalHours * 3600; + totalMinutes = (int) durationSeconds / 60; + durationSeconds -= totalMinutes * 60; + totalSeconds = (int) durationSeconds; + updateProgress(); + } + } + + /** + * Responds to MediaPlayer onEndOfMediaEvents. + * + * Prepares the media to be replayed. + */ + private class EndOfMediaListener implements Runnable { + @Override + public void run() { + Duration beginning = mediaPlayer.getStartTime(); + mediaPlayer.stop(); + mediaPlayer.pause(); + pauseButton.setText("►"); + updateSlider(beginning); + updateTime(beginning); + } + } + + /** + * Responds changes in the MediaPlayer currentTime property. + * + * Updates the progress slider and label with the current Time. + */ + private class TimeListener implements ChangeListener { + @Override + public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { + updateSlider(newValue); + updateTime(newValue); + } + } + } + + /** + * @param file a video file from which to capture frames + * @param numFrames the number of frames to capture. These frames will be + * captured at successive intervals given by durationOfVideo/numFrames. If + * this frame interval is less than MIN_FRAME_INTERVAL_MILLIS, then only one + * frame will be captured and returned. + * @return a List of VideoFrames representing the captured frames. + */ + @Override + public List captureFrames(java.io.File file, int numFrames) throws Exception { +// +// try { +// List frames = new ArrayList<>(); +// +// FrameCapturer fc = new FrameCapturer(file); +// logger.log(Level.INFO, "Fc is null? " + (fc == null)); +// frames = fc.getFrames(numFrames); +// +// return frames; +// } +// catch (NullPointerException e) { +// e.printStackTrace(); +// return null; +// } + return null; + } + +// private class FrameCapturer { +// +// private MediaPlayer mediaPlayer; +// private JFXPanel panel; +// private boolean isReady = false; +// +// FrameCapturer(java.io.File file) { +// initFx(file); +// } +// +// boolean isReady() { +// return isReady; +// } +// +// private void initFx(final java.io.File file) { +// Platform.runAndWait(new Runnable() { +// @Override +// public void run() { +// logger.log(Level.INFO, "In initFX."); +// // Create Media Player with no video output +// Media media = new Media(Paths.get(file.getAbsolutePath()).toUri().toString()); +// mediaPlayer = new MediaPlayer(media); +// MediaView mediaView = new MediaView(mediaPlayer); +// mediaView.setStyle("-fx-background-color: black"); +// Pane mediaViewPane = new Pane(); +// mediaViewPane.getChildren().add(mediaView); +// Scene scene = new Scene(mediaViewPane); +// panel = new JFXPanel(); +// panel.setScene(scene); +// isReady = true; +// } +// }); +// } +// +// List getFrames(int numFrames) { +// logger.log(Level.INFO, "in get frames"); +// List frames = new ArrayList(0); +// +// if (mediaPlayer.getStatus() != Status.READY) { +// try { +// Thread.sleep(500); +// } catch (InterruptedException e) { +// return frames; +// } +// } +// +// // get the duration of the video +// long myDurationMillis = (long) mediaPlayer.getMedia().getDuration().toMillis(); +// if (myDurationMillis <= 0) { +// return frames; +// } +// +// // calculate the frame interval +// int numFramesToGet = numFrames; +// long frameInterval = (myDurationMillis - INTER_FRAME_PERIOD_MS) / numFrames; +// if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) { +// numFramesToGet = 1; +// } +// +// final Object frameLock = new Object(); +// BufferedImage frame; +// final int width = (int) panel.getSize().getWidth(); +// final int height = (int) panel.getSize().getHeight(); +// // for each timeStamp, grap a frame +// for (int i = 0; i < numFramesToGet; ++i) { +// frame = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); +// logger.log(Level.INFO, "Grabbing a frame..."); +// final long timeStamp = i * frameInterval + INTER_FRAME_PERIOD_MS; +// +// // Platform.runLater(new Runnable() { +// // @Override +// // public void run() { +// // synchronized (frameLock) { +// logger.log(Level.INFO, "seeking."); +// mediaPlayer.seek(new Duration(timeStamp)); +// // } +// // } +// // }); +// +// synchronized (frameLock) { +// panel.paint(frame.createGraphics()); +// logger.log(Level.INFO, "Adding image to frames"); +// } +// frames.add(new VideoFrame(frame, timeStamp)); +// } +// return frames; +// } +// } + +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form new file mode 100644 index 0000000000..71babdd9f2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form @@ -0,0 +1,123 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java index 2fdb28c45a..70845431ea 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java @@ -1,766 +1,774 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.corecomponents; - -import java.awt.Dimension; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.nio.IntBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CancellationException; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import org.gstreamer.ClockTime; -import org.gstreamer.Gst; -import org.gstreamer.GstException; -import org.gstreamer.State; -import org.gstreamer.StateChangeReturn; -import org.gstreamer.elements.PlayBin2; -import org.gstreamer.elements.RGBDataSink; -import org.gstreamer.swing.VideoComponent; -import org.netbeans.api.progress.ProgressHandle; -import org.netbeans.api.progress.ProgressHandleFactory; -import org.openide.util.Cancellable; -import org.openide.util.lookup.ServiceProvider; -import org.openide.util.lookup.ServiceProviders; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; - - -@ServiceProviders(value = { - @ServiceProvider(service = FrameCapture.class) -}) -public class GstVideoPanel extends MediaViewVideoPanel { - - private static final Logger logger = Logger.getLogger(GstVideoPanel.class.getName()); - private boolean gstInited; - private static final long MIN_FRAME_INTERVAL_MILLIS = 500; - private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000; - private static final String MEDIA_PLAYER_ERROR_STRING = "The media player cannot process this file."; - //playback - private long durationMillis = 0; - private VideoProgressWorker videoProgressWorker; - private int totalHours, totalMinutes, totalSeconds; - private volatile PlayBin2 gstPlaybin2; - private VideoComponent gstVideoComponent; - private boolean autoTracking = false; // true if the slider is moving automatically - private final Object playbinLock = new Object(); // lock for synchronization of gstPlaybin2 player - private AbstractFile currentFile; - private Set badVideoFiles = Collections.synchronizedSet(new HashSet()); - - /** - * Creates new form MediaViewVideoPanel - */ - public GstVideoPanel() { - initComponents(); - customizeComponents(); - } - - public JButton getPauseButton() { - return pauseButton; - } - - public JLabel getProgressLabel() { - return progressLabel; - } - - public JSlider getProgressSlider() { - return progressSlider; - } - - public JPanel getVideoPanel() { - return videoPanel; - } - - public VideoComponent getVideoComponent() { - return gstVideoComponent; - } - - @Override - public boolean isInited() { - return gstInited; - } - - private void customizeComponents() { - if (!initGst()) { - return; - } - - progressSlider.setEnabled(false); // disable slider; enable after user plays vid - progressSlider.setValue(0); - - progressSlider.addChangeListener(new ChangeListener() { - /** - * Should always try to synchronize any call to - * progressSlider.setValue() to avoid a different thread - * changing playbin while stateChanged() is processing - */ - @Override - public void stateChanged(ChangeEvent e) { - int time = progressSlider.getValue(); - synchronized (playbinLock) { - if (gstPlaybin2 != null && !autoTracking) { - State orig = gstPlaybin2.getState(); - if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - if (gstPlaybin2.seek(ClockTime.fromMillis(time)) == false) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.seek() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - gstPlaybin2.setState(orig); - } - } - } - }); - } - - private boolean initGst() { - try { - logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); - Gst.init(); - gstInited = true; - } catch (GstException e) { - gstInited = false; - logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e); - MessageNotifyUtil.Notify.error("Error initializing gstreamer for audio/video viewing and frame extraction capabilities. " - + " Video and audio viewing will be disabled. ", - e.getMessage()); - return false; - } catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) { - gstInited = false; - logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and extraction capabilities", e); - MessageNotifyUtil.Notify.error("Error initializing gstreamer for audio/video viewing frame extraction capabilities. " - + " Video and audio viewing will be disabled. ", - e.getMessage()); - return false; - } - - return true; - } - - @Override - void setupVideo(final AbstractFile file, final Dimension dims) { - infoLabel.setText(""); - currentFile = file; - final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); - if (deleted) { - infoLabel.setText("Playback of deleted videos is not supported, use an external player."); - videoPanel.removeAll(); - pauseButton.setEnabled(false); - progressSlider.setEnabled(false); - return; - } - - String path = ""; - try { - path = file.getUniquePath(); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Cannot get unique path of video file"); - } - infoLabel.setText(path); - infoLabel.setToolTipText(path); - pauseButton.setEnabled(true); - progressSlider.setEnabled(true); - - java.io.File ioFile = getJFile(file); - - gstVideoComponent = new VideoComponent(); - synchronized (playbinLock) { - if (gstPlaybin2 != null) { - gstPlaybin2.dispose(); - } - gstPlaybin2 = new PlayBin2("VideoPlayer"); - gstPlaybin2.setVideoSink(gstVideoComponent.getElement()); - - videoPanel.removeAll(); - - - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - videoPanel.add(gstVideoComponent); - - videoPanel.setVisible(true); - - gstPlaybin2.setInputFile(ioFile); - - if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.READY) failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - } - } - - } - - @Override - void reset() { - - // reset the progress label text on the event dispatch thread - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressLabel.setText(""); - } - }); - - if (!isInited()) { - return; - } - - synchronized (playbinLock) { - if (gstPlaybin2 != null) { - if (gstPlaybin2.isPlaying()) { - if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.stop() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - } - if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.NULL) failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - if (gstPlaybin2.getState().equals(State.NULL)) { - gstPlaybin2.dispose(); - } - gstPlaybin2 = null; - } - gstVideoComponent = null; - } - - // get rid of any existing videoProgressWorker thread - if (videoProgressWorker != null) { - videoProgressWorker.cancel(true); - videoProgressWorker = null; - } - - currentFile = null; - } - - private java.io.File getJFile(AbstractFile file) { - // Get the temp folder path of the case - String tempPath = Case.getCurrentCase().getTempDirectory(); - String name = file.getName(); - int extStart = name.lastIndexOf("."); - String ext = ""; - if (extStart != -1) { - ext = name.substring(extStart, name.length()).toLowerCase(); - } - tempPath = tempPath + java.io.File.separator + file.getId() + ext; - - java.io.File tempFile = new java.io.File(tempPath); - return tempFile; - } - - /** - * @param file a video file from which to capture frames - * @param numFrames the number of frames to capture. These frames will be - * captured at successive intervals given by durationOfVideo/numFrames. If - * this frame interval is less than MIN_FRAME_INTERVAL_MILLIS, then only one - * frame will be captured and returned. - * @return a List of VideoFrames representing the captured frames. - */ - @Override - public List captureFrames(java.io.File file, int numFrames) throws Exception { - - List frames = new ArrayList<>(); - - Object lock = new Object(); - FrameCaptureRGBListener rgbListener = new FrameCaptureRGBListener(lock); - - if (!isInited()) { - return frames; - } - - // throw exception if this file is known to be problematic - if (badVideoFiles.contains(file.getName())) { - throw new Exception("Cannot capture frames from this file (" + file.getName() + ")."); - } - - // set up a PlayBin2 object - RGBDataSink videoSink = new RGBDataSink("rgb", rgbListener); - PlayBin2 playbin = new PlayBin2("VideoFrameCapture"); - playbin.setInputFile(file); - playbin.setVideoSink(videoSink); - - // this is necessary to get a valid duration value - StateChangeReturn ret = playbin.play(); - if (ret == StateChangeReturn.FAILURE) { - // add this file to the set of known bad ones - badVideoFiles.add(file.getName()); - throw new Exception("Problem with video file; problem when attempting to play while obtaining duration."); - } - ret = playbin.pause(); - if (ret == StateChangeReturn.FAILURE) { - // add this file to the set of known bad ones - badVideoFiles.add(file.getName()); - throw new Exception("Problem with video file; problem when attempting to pause while obtaining duration."); - } - playbin.getState(); - - // get the duration of the video - TimeUnit unit = TimeUnit.MILLISECONDS; - long myDurationMillis = playbin.queryDuration(unit); - if (myDurationMillis <= 0) { - return frames; - } - - // calculate the number of frames to capture - int numFramesToGet = numFrames; - long frameInterval = myDurationMillis / numFrames; - if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) { - numFramesToGet = 1; - } - - // for each timeStamp, grap a frame - for (int i = 0; i < numFramesToGet; ++i) { - long timeStamp = i * frameInterval; - - ret = playbin.pause(); - if (ret == StateChangeReturn.FAILURE) { - // add this file to the set of known bad ones - badVideoFiles.add(file.getName()); - throw new Exception("Problem with video file; problem when attempting to pause while capturing a frame."); - } - playbin.getState(); - - //System.out.println("Seeking to " + timeStamp + "milliseconds."); - if (!playbin.seek(timeStamp, unit)) { - logger.log(Level.INFO, "There was a problem seeking to " + timeStamp + " " + unit.name().toLowerCase()); - } - - ret = playbin.play(); - if (ret == StateChangeReturn.FAILURE) { - // add this file to the set of known bad ones - badVideoFiles.add(file.getName()); - throw new Exception("Problem with video file; problem when attempting to play while capturing a frame."); - } - - // wait for FrameCaptureRGBListener to finish - synchronized(lock) { - try { - lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS); - } catch (InterruptedException e) { - logger.log(Level.INFO, "InterruptedException occurred while waiting for frame capture.", e); - } - } - Image image = rgbListener.getImage(); - - ret = playbin.stop(); - if (ret == StateChangeReturn.FAILURE) { - // add this file to the set of known bad ones - badVideoFiles.add(file.getName()); - throw new Exception("Problem with video file; problem when attempting to stop while capturing a frame."); - } - - if (image == null) { - logger.log(Level.WARNING, "There was a problem while trying to capture a frame from file " + file.getName()); - badVideoFiles.add(file.getName()); - break; - } - - frames.add(new VideoFrame(image, timeStamp)); - } - - return frames; - } - - private class FrameCaptureRGBListener implements RGBDataSink.Listener { - - public FrameCaptureRGBListener(Object waiter) { - this.waiter = waiter; - } - - private BufferedImage bi; - private final Object waiter; - - @Override - public void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels) { - synchronized (waiter) { - bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w); - waiter.notify(); - } - } - - public Image getImage() { - synchronized (waiter) { - Image image = bi; - bi = null; - return image; - } - } - - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // - private void initComponents() { - - videoPanel = new javax.swing.JPanel(); - controlPanel = new javax.swing.JPanel(); - pauseButton = new javax.swing.JButton(); - progressSlider = new javax.swing.JSlider(); - progressLabel = new javax.swing.JLabel(); - infoLabel = new javax.swing.JLabel(); - - javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); - videoPanel.setLayout(videoPanelLayout); - videoPanelLayout.setHorizontalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - videoPanelLayout.setVerticalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 188, Short.MAX_VALUE) - ); - - org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(MediaViewVideoPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N - pauseButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pauseButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaViewVideoPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaViewVideoPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N - - javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); - controlPanel.setLayout(controlPanelLayout); - controlPanelLayout.setHorizontalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(pauseButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressLabel) - .addContainerGap()) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(infoLabel) - .addGap(0, 0, Short.MAX_VALUE)) - ); - controlPanelLayout.setVerticalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(pauseButton) - .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(progressLabel, javax.swing.GroupLayout.Alignment.TRAILING)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(infoLabel)) - ); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(videoPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) - ); - }// - - private void pauseButtonActionPerformed(java.awt.event.ActionEvent evt) { - synchronized (playbinLock) { - State state = gstPlaybin2.getState(); - if (state.equals(State.PLAYING)) { - if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("►"); - // Is this call necessary considering we just called gstPlaybin2.pause()? - if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.PAUSED) failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - } else if (state.equals(State.PAUSED)) { - if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("||"); - // Is this call necessary considering we just called gstPlaybin2.play()? - if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.PLAYING) failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - } else if (state.equals(State.READY)) { - ExtractMedia em = new ExtractMedia(currentFile, getJFile(currentFile)); - em.execute(); - em.getExtractedBytes(); - } - } - } - // Variables declaration - do not modify - private javax.swing.JPanel controlPanel; - private javax.swing.JLabel infoLabel; - private javax.swing.JButton pauseButton; - private javax.swing.JLabel progressLabel; - private javax.swing.JSlider progressSlider; - private javax.swing.JPanel videoPanel; - // End of variables declaration - - private class VideoProgressWorker extends SwingWorker { - - private String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; - private long millisElapsed = 0; - private final long INTER_FRAME_PERIOD_MS = 20; - private final long END_TIME_MARGIN_MS = 50; - private boolean hadError = false; - - private boolean isPlayBinReady() { - synchronized (playbinLock) { - return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL); - } - } - - private void resetVideo() throws Exception { - synchronized (playbinLock) { - if (gstPlaybin2 != null) { - if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.stop() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - } - // ready to be played again - if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.READY) failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - } - gstPlaybin2.getState(); //NEW - } - } - pauseButton.setText("►"); - progressSlider.setValue(0); - - String durationStr = String.format(durationFormat, 0, 0, 0, - totalHours, totalMinutes, totalSeconds); - progressLabel.setText(durationStr); - } - - /** - * @return true while millisElapsed is greater than END_TIME_MARGIN_MS - * from durationMillis. This is used to indicate when the video has - * ended because for some videos the time elapsed never becomes equal to - * the reported duration of the video. - */ - private boolean hasNotEnded() { - return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS; - } - - @Override - protected Object doInBackground() throws Exception { - - // enable the slider - progressSlider.setEnabled(true); - - int elapsedHours = -1, elapsedMinutes = -1, elapsedSeconds = -1; - ClockTime pos = null; - while (hasNotEnded() && isPlayBinReady() && !isCancelled()) { - - synchronized (playbinLock) { - pos = gstPlaybin2.queryPosition(); - } - millisElapsed = pos.toMillis(); - - // pick out the elapsed hours, minutes, seconds - long secondsElapsed = millisElapsed / 1000; - elapsedHours = (int) secondsElapsed / 3600; - secondsElapsed -= elapsedHours * 3600; - elapsedMinutes = (int) secondsElapsed / 60; - secondsElapsed -= elapsedMinutes * 60; - elapsedSeconds = (int) secondsElapsed; - - String durationStr = String.format(durationFormat, - elapsedHours, elapsedMinutes, elapsedSeconds, - totalHours, totalMinutes, totalSeconds); - - progressLabel.setText(durationStr); - autoTracking = true; - progressSlider.setValue((int) millisElapsed); - autoTracking = false; - - try { - Thread.sleep(INTER_FRAME_PERIOD_MS); - } catch (InterruptedException ex) { - break; - } - } - - // disable the slider - progressSlider.setEnabled(false); - - resetVideo(); - - return null; - } - } //end class progress worker - - /* Thread that extracts and plays a file */ - private class ExtractMedia extends SwingWorker { - - private ProgressHandle progress; - boolean success = false; - private AbstractFile sFile; - private java.io.File jFile; - private String duration; - private String position; - private long extractedBytes; - - ExtractMedia(org.sleuthkit.datamodel.AbstractFile sFile, java.io.File jFile) { - this.sFile = sFile; - this.jFile = jFile; - } - - public long getExtractedBytes() { - return extractedBytes; - } - - @Override - protected Object doInBackground() throws Exception { - success = false; - progress = ProgressHandleFactory.createHandle("Buffering " + sFile.getName(), new Cancellable() { - @Override - public boolean cancel() { - return ExtractMedia.this.cancel(true); - } - }); - progressLabel.setText("Buffering... "); - progress.start(); - progress.switchToDeterminate(100); - try { - extractedBytes = ContentUtils.writeToFile(sFile, jFile, progress, this, true); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error buffering file", ex); - } - success = true; - return null; - } - - /* clean up or start the worker threads */ - @Override - protected void done() { - try { - super.get(); //block and get all exceptions thrown while doInBackground() - } catch (CancellationException ex) { - logger.log(Level.INFO, "Media buffering was canceled."); - } catch (InterruptedException ex) { - logger.log(Level.INFO, "Media buffering was interrupted."); - } catch (Exception ex) { - logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); - } finally { - progress.finish(); - if (!this.isCancelled()) { - playMedia(); - } - } - } - - void playMedia() { - if (jFile == null || !jFile.exists()) { - progressLabel.setText("Error buffering file"); - return; - } - ClockTime dur = null; - synchronized (playbinLock) { - // must play, then pause and get state to get duration. - if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - gstPlaybin2.getState(); - dur = gstPlaybin2.queryDuration(); - } - duration = dur.toString(); - durationMillis = dur.toMillis(); - - // pick out the total hours, minutes, seconds - long durationSeconds = (int) durationMillis / 1000; - totalHours = (int) durationSeconds / 3600; - durationSeconds -= totalHours * 3600; - totalMinutes = (int) durationSeconds / 60; - durationSeconds -= totalMinutes * 60; - totalSeconds = (int) durationSeconds; - - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressSlider.setMaximum((int) durationMillis); - progressSlider.setMinimum(0); - - synchronized (playbinLock) { - if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - } - } - pauseButton.setText("||"); - videoProgressWorker = new VideoProgressWorker(); - videoProgressWorker.execute(); - } - }); - } - } -} +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.corecomponents; + +import java.awt.Dimension; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.gstreamer.ClockTime; +import org.gstreamer.Gst; +import org.gstreamer.GstException; +import org.gstreamer.State; +import org.gstreamer.StateChangeReturn; +import org.gstreamer.elements.PlayBin2; +import org.gstreamer.elements.RGBDataSink; +import org.gstreamer.swing.VideoComponent; +import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.progress.ProgressHandleFactory; +import org.openide.util.Cancellable; +import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + + +@ServiceProviders(value = { + @ServiceProvider(service = FrameCapture.class) +}) +public class GstVideoPanel extends MediaViewVideoPanel { + + private static final Logger logger = Logger.getLogger(GstVideoPanel.class.getName()); + private boolean gstInited; + private static final long MIN_FRAME_INTERVAL_MILLIS = 500; + private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000; + private static final String MEDIA_PLAYER_ERROR_STRING = "The media player cannot process this file."; + //playback + private long durationMillis = 0; + private GstVideoPanel.VideoProgressWorker videoProgressWorker; + private int totalHours, totalMinutes, totalSeconds; + private volatile PlayBin2 gstPlaybin2; + private VideoComponent gstVideoComponent; + private boolean autoTracking = false; // true if the slider is moving automatically + private final Object playbinLock = new Object(); // lock for synchronization of gstPlaybin2 player + private AbstractFile currentFile; + private Set badVideoFiles = Collections.synchronizedSet(new HashSet()); + + /** + * Creates new form MediaViewVideoPanel + */ + public GstVideoPanel() { + initComponents(); + customizeComponents(); + } + + public JButton getPauseButton() { + return pauseButton; + } + + public JLabel getProgressLabel() { + return progressLabel; + } + + public JSlider getProgressSlider() { + return progressSlider; + } + + public JPanel getVideoPanel() { + return videoPanel; + } + + public VideoComponent getVideoComponent() { + return gstVideoComponent; + } + + @Override + public boolean isInited() { + return gstInited; + } + + private void customizeComponents() { + if (!initGst()) { + return; + } + + progressSlider.setEnabled(false); // disable slider; enable after user plays vid + progressSlider.setValue(0); + + progressSlider.addChangeListener(new ChangeListener() { + /** + * Should always try to synchronize any call to + * progressSlider.setValue() to avoid a different thread + * changing playbin while stateChanged() is processing + */ + @Override + public void stateChanged(ChangeEvent e) { + int time = progressSlider.getValue(); + synchronized (playbinLock) { + if (gstPlaybin2 != null && !autoTracking) { + State orig = gstPlaybin2.getState(); + if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + if (gstPlaybin2.seek(ClockTime.fromMillis(time)) == false) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.seek() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + gstPlaybin2.setState(orig); + } + } + } + }); + } + + private boolean initGst() { + try { + logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); + Gst.init(); + gstInited = true; + } catch (GstException e) { + gstInited = false; + logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e); + MessageNotifyUtil.Notify.error("Error initializing gstreamer for audio/video viewing and frame extraction capabilities. " + + " Video and audio viewing will be disabled. ", + e.getMessage()); + return false; + } catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) { + gstInited = false; + logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and extraction capabilities", e); + MessageNotifyUtil.Notify.error("Error initializing gstreamer for audio/video viewing frame extraction capabilities. " + + " Video and audio viewing will be disabled. ", + e.getMessage()); + return false; + } + + return true; + } + + @Override + void setupVideo(final AbstractFile file, final Dimension dims) { + infoLabel.setText(""); + currentFile = file; + final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); + if (deleted) { + infoLabel.setText("Playback of deleted videos is not supported, use an external player."); + videoPanel.removeAll(); + pauseButton.setEnabled(false); + progressSlider.setEnabled(false); + return; + } + + String path = ""; + try { + path = file.getUniquePath(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Cannot get unique path of video file"); + } + infoLabel.setText(path); + infoLabel.setToolTipText(path); + pauseButton.setEnabled(true); + progressSlider.setEnabled(true); + + java.io.File ioFile = getJFile(file); + + gstVideoComponent = new VideoComponent(); + synchronized (playbinLock) { + if (gstPlaybin2 != null) { + gstPlaybin2.dispose(); + } + gstPlaybin2 = new PlayBin2("VideoPlayer"); + gstPlaybin2.setVideoSink(gstVideoComponent.getElement()); + + videoPanel.removeAll(); + + + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.add(gstVideoComponent); + + videoPanel.setVisible(true); + + gstPlaybin2.setInputFile(ioFile); + + if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.READY) failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + } + } + + } + + @Override + void reset() { + + // reset the progress label text on the event dispatch thread + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressLabel.setText(""); + } + }); + + if (!isInited()) { + return; + } + + synchronized (playbinLock) { + if (gstPlaybin2 != null) { + if (gstPlaybin2.isPlaying()) { + if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.stop() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + } + if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.NULL) failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + if (gstPlaybin2.getState().equals(State.NULL)) { + gstPlaybin2.dispose(); + } + gstPlaybin2 = null; + } + gstVideoComponent = null; + } + + // get rid of any existing videoProgressWorker thread + if (videoProgressWorker != null) { + videoProgressWorker.cancel(true); + videoProgressWorker = null; + } + + currentFile = null; + } + + private java.io.File getJFile(AbstractFile file) { + // Get the temp folder path of the case + String tempPath = Case.getCurrentCase().getTempDirectory(); + String name = file.getName(); + int extStart = name.lastIndexOf("."); + String ext = ""; + if (extStart != -1) { + ext = name.substring(extStart, name.length()).toLowerCase(); + } + tempPath = tempPath + java.io.File.separator + file.getId() + ext; + + java.io.File tempFile = new java.io.File(tempPath); + return tempFile; + } + + /** + * @param file a video file from which to capture frames + * @param numFrames the number of frames to capture. These frames will be + * captured at successive intervals given by durationOfVideo/numFrames. If + * this frame interval is less than MIN_FRAME_INTERVAL_MILLIS, then only one + * frame will be captured and returned. + * @return a List of VideoFrames representing the captured frames. + */ + @Override + public List captureFrames(java.io.File file, int numFrames) throws Exception { + + List frames = new ArrayList<>(); + + Object lock = new Object(); + GstVideoPanel.FrameCaptureRGBListener rgbListener = new GstVideoPanel.FrameCaptureRGBListener(lock); + + if (!isInited()) { + return frames; + } + + // throw exception if this file is known to be problematic + if (badVideoFiles.contains(file.getName())) { + throw new Exception("Cannot capture frames from this file (" + file.getName() + ")."); + } + + // set up a PlayBin2 object + RGBDataSink videoSink = new RGBDataSink("rgb", rgbListener); + PlayBin2 playbin = new PlayBin2("VideoFrameCapture"); + playbin.setInputFile(file); + playbin.setVideoSink(videoSink); + + // this is necessary to get a valid duration value + StateChangeReturn ret = playbin.play(); + if (ret == StateChangeReturn.FAILURE) { + // add this file to the set of known bad ones + badVideoFiles.add(file.getName()); + throw new Exception("Problem with video file; problem when attempting to play while obtaining duration."); + } + ret = playbin.pause(); + if (ret == StateChangeReturn.FAILURE) { + // add this file to the set of known bad ones + badVideoFiles.add(file.getName()); + throw new Exception("Problem with video file; problem when attempting to pause while obtaining duration."); + } + playbin.getState(); + + // get the duration of the video + TimeUnit unit = TimeUnit.MILLISECONDS; + long myDurationMillis = playbin.queryDuration(unit); + if (myDurationMillis <= 0) { + return frames; + } + + // calculate the number of frames to capture + int numFramesToGet = numFrames; + long frameInterval = myDurationMillis / numFrames; + if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) { + numFramesToGet = 1; + } + + // for each timeStamp, grap a frame + for (int i = 0; i < numFramesToGet; ++i) { + long timeStamp = i * frameInterval; + + ret = playbin.pause(); + if (ret == StateChangeReturn.FAILURE) { + // add this file to the set of known bad ones + badVideoFiles.add(file.getName()); + throw new Exception("Problem with video file; problem when attempting to pause while capturing a frame."); + } + playbin.getState(); + + //System.out.println("Seeking to " + timeStamp + "milliseconds."); + if (!playbin.seek(timeStamp, unit)) { + logger.log(Level.INFO, "There was a problem seeking to " + timeStamp + " " + unit.name().toLowerCase()); + } + + ret = playbin.play(); + if (ret == StateChangeReturn.FAILURE) { + // add this file to the set of known bad ones + badVideoFiles.add(file.getName()); + throw new Exception("Problem with video file; problem when attempting to play while capturing a frame."); + } + + // wait for FrameCaptureRGBListener to finish + synchronized(lock) { + try { + lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + logger.log(Level.INFO, "InterruptedException occurred while waiting for frame capture.", e); + } + } + Image image = rgbListener.getImage(); + + ret = playbin.stop(); + if (ret == StateChangeReturn.FAILURE) { + // add this file to the set of known bad ones + badVideoFiles.add(file.getName()); + throw new Exception("Problem with video file; problem when attempting to stop while capturing a frame."); + } + + if (image == null) { + logger.log(Level.WARNING, "There was a problem while trying to capture a frame from file " + file.getName()); + badVideoFiles.add(file.getName()); + break; + } + + frames.add(new VideoFrame(image, timeStamp)); + } + + return frames; + } + + private class FrameCaptureRGBListener implements RGBDataSink.Listener { + + public FrameCaptureRGBListener(Object waiter) { + this.waiter = waiter; + } + + private BufferedImage bi; + private final Object waiter; + + @Override + public void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels) { + synchronized (waiter) { + bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w); + waiter.notify(); + } + } + + public Image getImage() { + synchronized (waiter) { + Image image = bi; + bi = null; + return image; + } + } + + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + controlPanel = new javax.swing.JPanel(); + infoLabel = new javax.swing.JLabel(); + pauseButton = new javax.swing.JButton(); + progressSlider = new javax.swing.JSlider(); + progressLabel = new javax.swing.JLabel(); + videoPanel = new javax.swing.JPanel(); + + org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N + pauseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pauseButtonActionPerformed(evt); + } + }); + + progressLabel.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N + + org.jdesktop.layout.GroupLayout controlPanelLayout = new org.jdesktop.layout.GroupLayout(controlPanel); + controlPanel.setLayout(controlPanelLayout); + controlPanelLayout.setHorizontalGroup( + controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .add(infoLabel) + .add(0, 0, Short.MAX_VALUE)) + .add(controlPanelLayout.createSequentialGroup() + .add(pauseButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 45, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) + .add(progressSlider, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 327, Short.MAX_VALUE) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) + .add(progressLabel) + .add(17, 17, 17)) + ); + controlPanelLayout.setVerticalGroup( + controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .addContainerGap() + .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING) + .add(pauseButton) + .add(progressSlider, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .add(org.jdesktop.layout.GroupLayout.TRAILING, controlPanelLayout.createSequentialGroup() + .addContainerGap(org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .add(progressLabel) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED))) + .add(infoLabel) + .addContainerGap()) + ); + + org.jdesktop.layout.GroupLayout videoPanelLayout = new org.jdesktop.layout.GroupLayout(videoPanel); + videoPanel.setLayout(videoPanelLayout); + videoPanelLayout.setHorizontalGroup( + videoPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(0, 0, Short.MAX_VALUE) + ); + videoPanelLayout.setVerticalGroup( + videoPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(0, 240, Short.MAX_VALUE) + ); + + org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .add(videoPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(org.jdesktop.layout.GroupLayout.TRAILING, layout.createSequentialGroup() + .add(videoPanel, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) + .add(controlPanel, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) + ); + }// //GEN-END:initComponents + + private void pauseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pauseButtonActionPerformed + synchronized (playbinLock) { + State state = gstPlaybin2.getState(); + if (state.equals(State.PLAYING)) { + if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + pauseButton.setText("►"); + // Is this call necessary considering we just called gstPlaybin2.pause()? + if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.PAUSED) failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + } else if (state.equals(State.PAUSED)) { + if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + pauseButton.setText("||"); + // Is this call necessary considering we just called gstPlaybin2.play()? + if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.PLAYING) failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + } else if (state.equals(State.READY)) { + ExtractMedia em = new ExtractMedia(currentFile, getJFile(currentFile)); + em.execute(); + em.getExtractedBytes(); + } + } + }//GEN-LAST:event_pauseButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel controlPanel; + private javax.swing.JLabel infoLabel; + private javax.swing.JButton pauseButton; + private javax.swing.JLabel progressLabel; + private javax.swing.JSlider progressSlider; + private javax.swing.JPanel videoPanel; + // End of variables declaration//GEN-END:variables + + private class VideoProgressWorker extends SwingWorker { + + private String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; + private long millisElapsed = 0; + private final long INTER_FRAME_PERIOD_MS = 20; + private final long END_TIME_MARGIN_MS = 50; + private boolean hadError = false; + + private boolean isPlayBinReady() { + synchronized (playbinLock) { + return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL); + } + } + + private void resetVideo() throws Exception { + synchronized (playbinLock) { + if (gstPlaybin2 != null) { + if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.stop() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + } + // ready to be played again + if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.setState(State.READY) failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + } + gstPlaybin2.getState(); //NEW + } + } + pauseButton.setText("►"); + progressSlider.setValue(0); + + String durationStr = String.format(durationFormat, 0, 0, 0, + totalHours, totalMinutes, totalSeconds); + progressLabel.setText(durationStr); + } + + /** + * @return true while millisElapsed is greater than END_TIME_MARGIN_MS + * from durationMillis. This is used to indicate when the video has + * ended because for some videos the time elapsed never becomes equal to + * the reported duration of the video. + */ + private boolean hasNotEnded() { + return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS; + } + + @Override + protected Object doInBackground() throws Exception { + + // enable the slider + progressSlider.setEnabled(true); + + int elapsedHours = -1, elapsedMinutes = -1, elapsedSeconds = -1; + ClockTime pos = null; + while (hasNotEnded() && isPlayBinReady() && !isCancelled()) { + + synchronized (playbinLock) { + pos = gstPlaybin2.queryPosition(); + } + millisElapsed = pos.toMillis(); + + // pick out the elapsed hours, minutes, seconds + long secondsElapsed = millisElapsed / 1000; + elapsedHours = (int) secondsElapsed / 3600; + secondsElapsed -= elapsedHours * 3600; + elapsedMinutes = (int) secondsElapsed / 60; + secondsElapsed -= elapsedMinutes * 60; + elapsedSeconds = (int) secondsElapsed; + + String durationStr = String.format(durationFormat, + elapsedHours, elapsedMinutes, elapsedSeconds, + totalHours, totalMinutes, totalSeconds); + + progressLabel.setText(durationStr); + autoTracking = true; + progressSlider.setValue((int) millisElapsed); + autoTracking = false; + + try { + Thread.sleep(INTER_FRAME_PERIOD_MS); + } catch (InterruptedException ex) { + break; + } + } + + // disable the slider + progressSlider.setEnabled(false); + + resetVideo(); + + return null; + } + } //end class progress worker + + /* Thread that extracts and plays a file */ + private class ExtractMedia extends SwingWorker { + + private ProgressHandle progress; + boolean success = false; + private AbstractFile sFile; + private java.io.File jFile; + private String duration; + private String position; + private long extractedBytes; + + ExtractMedia(org.sleuthkit.datamodel.AbstractFile sFile, java.io.File jFile) { + this.sFile = sFile; + this.jFile = jFile; + } + + public long getExtractedBytes() { + return extractedBytes; + } + + @Override + protected Object doInBackground() throws Exception { + success = false; + progress = ProgressHandleFactory.createHandle("Buffering " + sFile.getName(), new Cancellable() { + @Override + public boolean cancel() { + return ExtractMedia.this.cancel(true); + } + }); + progressLabel.setText("Buffering... "); + progress.start(); + progress.switchToDeterminate(100); + try { + extractedBytes = ContentUtils.writeToFile(sFile, jFile, progress, this, true); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error buffering file", ex); + } + success = true; + return null; + } + + /* clean up or start the worker threads */ + @Override + protected void done() { + try { + super.get(); //block and get all exceptions thrown while doInBackground() + } catch (CancellationException ex) { + logger.log(Level.INFO, "Media buffering was canceled."); + } catch (InterruptedException ex) { + logger.log(Level.INFO, "Media buffering was interrupted."); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); + } finally { + progress.finish(); + if (!this.isCancelled()) { + playMedia(); + } + } + } + + void playMedia() { + if (jFile == null || !jFile.exists()) { + progressLabel.setText("Error buffering file"); + return; + } + ClockTime dur = null; + synchronized (playbinLock) { + // must play, then pause and get state to get duration. + if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.pause() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + return; + } + gstPlaybin2.getState(); + dur = gstPlaybin2.queryDuration(); + } + duration = dur.toString(); + durationMillis = dur.toMillis(); + + // pick out the total hours, minutes, seconds + long durationSeconds = (int) durationMillis / 1000; + totalHours = (int) durationSeconds / 3600; + durationSeconds -= totalHours * 3600; + totalMinutes = (int) durationSeconds / 60; + durationSeconds -= totalMinutes * 60; + totalSeconds = (int) durationSeconds; + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressSlider.setMaximum((int) durationMillis); + progressSlider.setMinimum(0); + + synchronized (playbinLock) { + if (gstPlaybin2.play() == StateChangeReturn.FAILURE) { + logger.log(Level.WARNING, "Attempt to call PlayBin2.play() failed."); + infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); + } + } + pauseButton.setText("||"); + videoProgressWorker = new VideoProgressWorker(); + videoProgressWorker.execute(); + } + }); + } + } +} From 3209845c105e9645318dcefaa2735c55e691f3d6 Mon Sep 17 00:00:00 2001 From: Jeff Wallace Date: Thu, 5 Sep 2013 13:48:45 -0400 Subject: [PATCH 12/12] Updated gstreamer form --- .../autopsy/corecomponents/GstVideoPanel.form | 154 +++++++++--------- .../autopsy/corecomponents/GstVideoPanel.java | 101 ++++++------ 2 files changed, 127 insertions(+), 128 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form index 71babdd9f2..de0b9e058e 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.form @@ -22,88 +22,15 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -114,10 +41,83 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java index 70845431ea..8653f74f3f 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/GstVideoPanel.java @@ -440,57 +440,12 @@ public class GstVideoPanel extends MediaViewVideoPanel { // //GEN-BEGIN:initComponents private void initComponents() { + videoPanel = new javax.swing.JPanel(); controlPanel = new javax.swing.JPanel(); - infoLabel = new javax.swing.JLabel(); pauseButton = new javax.swing.JButton(); + infoLabel = new javax.swing.JLabel(); progressSlider = new javax.swing.JSlider(); progressLabel = new javax.swing.JLabel(); - videoPanel = new javax.swing.JPanel(); - - org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N - pauseButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pauseButtonActionPerformed(evt); - } - }); - - progressLabel.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); - org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N - - org.jdesktop.layout.GroupLayout controlPanelLayout = new org.jdesktop.layout.GroupLayout(controlPanel); - controlPanel.setLayout(controlPanelLayout); - controlPanelLayout.setHorizontalGroup( - controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(controlPanelLayout.createSequentialGroup() - .add(infoLabel) - .add(0, 0, Short.MAX_VALUE)) - .add(controlPanelLayout.createSequentialGroup() - .add(pauseButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 45, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) - .add(progressSlider, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 327, Short.MAX_VALUE) - .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) - .add(progressLabel) - .add(17, 17, 17)) - ); - controlPanelLayout.setVerticalGroup( - controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(controlPanelLayout.createSequentialGroup() - .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(controlPanelLayout.createSequentialGroup() - .addContainerGap() - .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING) - .add(pauseButton) - .add(progressSlider, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .add(org.jdesktop.layout.GroupLayout.TRAILING, controlPanelLayout.createSequentialGroup() - .addContainerGap(org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .add(progressLabel) - .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED))) - .add(infoLabel) - .addContainerGap()) - ); org.jdesktop.layout.GroupLayout videoPanelLayout = new org.jdesktop.layout.GroupLayout(videoPanel); videoPanel.setLayout(videoPanelLayout); @@ -500,7 +455,51 @@ public class GstVideoPanel extends MediaViewVideoPanel { ); videoPanelLayout.setVerticalGroup( videoPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(0, 240, Short.MAX_VALUE) + .add(0, 250, Short.MAX_VALUE) + ); + + org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N + pauseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pauseButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(GstVideoPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N + + org.jdesktop.layout.GroupLayout controlPanelLayout = new org.jdesktop.layout.GroupLayout(controlPanel); + controlPanel.setLayout(controlPanelLayout); + controlPanelLayout.setHorizontalGroup( + controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .addContainerGap() + .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .add(pauseButton) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) + .add(progressSlider, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 316, Short.MAX_VALUE) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) + .add(progressLabel) + .addContainerGap()) + .add(controlPanelLayout.createSequentialGroup() + .add(infoLabel) + .add(0, 0, Short.MAX_VALUE)))) + ); + controlPanelLayout.setVerticalGroup( + controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .addContainerGap() + .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) + .add(controlPanelLayout.createSequentialGroup() + .add(controlPanelLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING) + .add(pauseButton) + .add(progressSlider, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) + .add(0, 1, Short.MAX_VALUE)) + .add(progressLabel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) + .add(infoLabel)) ); org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this); @@ -512,9 +511,9 @@ public class GstVideoPanel extends MediaViewVideoPanel { ); layout.setVerticalGroup( layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) - .add(org.jdesktop.layout.GroupLayout.TRAILING, layout.createSequentialGroup() - .add(videoPanel, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED) + .add(layout.createSequentialGroup() + .add(videoPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(controlPanel, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) ); }// //GEN-END:initComponents