Commit 1fa7758b authored by Jim Nelson's avatar Jim Nelson

#2796: Initial cut of VideoMetadata using libquicktime. A more thorough and...

#2796: Initial cut of VideoMetadata using libquicktime.  A more thorough and robust video metadata library would certainly improve our support, but this is good for now.
parent 76cf3f79
......@@ -136,7 +136,9 @@ SRC_FILES = \
file_util.vala \
DesktopIntegration.vala \
FlaggedPage.vala \
MediaInterfaces.vala
MediaInterfaces.vala \
MediaMetadata.vala \
VideoMetadata.vala
ifndef LINUX
SRC_FILES += \
......@@ -285,6 +287,8 @@ EXT_PKGS = \
gexiv2 \
json-glib-1.0
DIRECT_LIBS =
LIBRAW_PKG = \
libraw
......@@ -301,6 +305,9 @@ EXT_PKGS += \
gdk-x11-2.0 \
gstreamer-0.10 \
gstreamer-base-0.10
DIRECT_LIBS += \
libquicktime
endif
# libraw is handled separately (see note below); when libraw-config is no longer needed, the version
......@@ -314,6 +321,8 @@ EXT_PKG_VERSIONS = \
gexiv2 >= 0.2.0 \
json-glib-1.0 >= 0.7.6
DIRECT_LIBS_VERSIONS =
LIBRAW_VERSION = \
0.9.0
......@@ -329,9 +338,12 @@ EXT_PKG_VERSIONS += \
dbus-glib-1 >= 0.80 \
gstreamer-0.10 >= 0.10.28 \
gstreamer-base-0.10 >= 0.10.28
DIRECT_LIBS_VERSIONS += \
libquicktime >= 1.1.4
endif
PKGS = $(EXT_PKGS) $(LOCAL_PKGS) $(LIBRAW_PKG)
VALA_PKGS = $(EXT_PKGS) $(LOCAL_PKGS) $(LIBRAW_PKG)
ifndef BUILD_DIR
BUILD_DIR=src
......@@ -370,10 +382,11 @@ DIST_TAR_BZ2 = $(DIST_TAR).bz2
DIST_TAR_GZ = $(DIST_TAR).gz
PACKAGE_ORIG_GZ = $(PROGRAM)_`parsechangelog | grep Version | sed 's/.*: //'`.orig.tar.gz
VALA_CFLAGS = `pkg-config --cflags $(EXT_PKGS) gthread-2.0` $(foreach hdir,$(HEADER_DIRS),-I$(hdir)) \
VALA_CFLAGS = `pkg-config --cflags $(EXT_PKGS) $(DIRECT_LIBS) gthread-2.0` \
$(foreach hdir,$(HEADER_DIRS),-I$(hdir)) \
$(foreach def,$(DEFINES),-D$(def))
VALA_LDFLAGS = `pkg-config --libs $(EXT_PKGS) gthread-2.0`
VALA_LDFLAGS = `pkg-config --libs $(EXT_PKGS) $(DIRECT_LIBS) gthread-2.0`
ifdef WINDOWS
VALA_DEFINES = -D WINDOWS -D NO_CAMERA -D NO_PRINTING -D NO_PUBLISHING -D NO_LIBUNIQUE -D NO_EXTENDED_POSIX -D NO_SET_BACKGROUND
......@@ -539,9 +552,9 @@ $(VALA_STAMP): $(EXPANDED_SRC_FILES) $(EXPANDED_VAPI_FILES) $(EXPANDED_SRC_HEADE
@ ./minver `$(VALAC) --version | awk '{print $$2}'` $(MIN_VALAC_VERSION) || ( echo 'Shotwell requires Vala compiler $(MIN_VALAC_VERSION) or greater. You are running' `$(VALAC) --version` '\b.'; exit 1 )
ifndef ASSUME_PKGS
ifdef EXT_PKG_VERSIONS
@pkg-config --print-errors --exists '$(EXT_PKG_VERSIONS)'
@pkg-config --print-errors --exists '$(EXT_PKG_VERSIONS) $(DIRECT_LIBS_VERSIONS)'
else ifdef EXT_PKGS
@pkg-config --print-errors --exists $(EXT_PKGS)
@pkg-config --print-errors --exists $(EXT_PKGS) $(DIRECT_LIBS_VERSIONS)
endif
# Check for libraw manually, but not on Windows, where install-deps is used
ifndef WINDOWS
......@@ -551,7 +564,7 @@ endif
@ type msgfmt > /dev/null || ( echo 'msgfmt (usually found in the gettext package) is missing and is required to build Shotwell. ' ; exit 1 )
mkdir -p $(BUILD_DIR)
$(VALAC) --ccode --directory=$(BUILD_DIR) --basedir=src $(VALAFLAGS) \
$(foreach pkg,$(PKGS),--pkg=$(pkg)) \
$(foreach pkg,$(VALA_PKGS),--pkg=$(pkg)) \
$(foreach vapidir,$(VAPI_DIRS),--vapidir=$(vapidir)) \
$(foreach def,$(DEFINES),-X -D$(def)) \
$(foreach hdir,$(HEADER_DIRS),-X -I$(hdir)) \
......
......@@ -1446,7 +1446,7 @@ private class PreparedFilesImportJob : BackgroundJob {
ImportResult result = ImportResult.SUCCESS;
VideoImportParams? video_import_params = null;
PhotoImportParams? photo_import_params = null;
if (prepared_file.is_video) {
if (prepared_file.is_video) {
video_import_params = new VideoImportParams(final_file, import_id,
prepared_file.full_md5, new Thumbnails(),
prepared_file.job.get_exposure_time_override());
......
......@@ -2416,6 +2416,10 @@ public struct VideoID {
}
}
//
// VideoTable
//
public struct VideoRow {
public VideoID video_id;
public string filepath;
......
/* Copyright 2010 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public abstract class MediaMetadata {
public MediaMetadata() {
}
public abstract void read_from_file(File file) throws Error;
public abstract MetadataDateTime? get_creation_date_time();
public abstract string? get_title();
}
public struct MetadataRational {
public int numerator;
public int denominator;
public MetadataRational(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
private bool is_component_valid(int component) {
return (component >= 0) && (component <= 1000000);
}
public bool is_valid() {
return (is_component_valid(numerator) && is_component_valid(denominator));
}
public string to_string() {
return (is_valid()) ? ("%d/%d".printf(numerator, denominator)) : "";
}
}
errordomain MetadataDateTimeError {
INVALID_FORMAT,
UNSUPPORTED_FORMAT
}
public class MetadataDateTime {
private time_t timestamp;
public MetadataDateTime(time_t timestamp) {
this.timestamp = timestamp;
}
public MetadataDateTime.from_exif(string label) throws MetadataDateTimeError {
if (!from_exif_date_time(label, out timestamp))
throw new MetadataDateTimeError.INVALID_FORMAT("%s is not EXIF format date/time", label);
}
public MetadataDateTime.from_iptc(string date, string time) throws MetadataDateTimeError {
// TODO: Support IPTC date/time format
throw new MetadataDateTimeError.UNSUPPORTED_FORMAT("IPTC date/time format not currently supported");
}
public MetadataDateTime.from_xmp(string label) throws MetadataDateTimeError {
TimeVal time_val = TimeVal();
if (!time_val.from_iso8601(label))
throw new MetadataDateTimeError.INVALID_FORMAT("%s is not XMP format date/time", label);
timestamp = time_val.tv_sec;
}
public time_t get_timestamp() {
return timestamp;
}
public string get_exif_label() {
return to_exif_date_time(timestamp);
}
// TODO: get_iptc_date() and get_iptc_time()
public string get_xmp_label() {
TimeVal time_val = TimeVal();
time_val.tv_sec = timestamp;
time_val.tv_usec = 0;
return time_val.to_iso8601();
}
public static bool from_exif_date_time(string date_time, out time_t timestamp) {
Time tm = Time();
if (date_time.scanf("%d:%d:%d %d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,
&tm.second) != 6) {
// for Minolta DiMAGE E223 (colon, instead of space, separates day from hour in exif)
if (date_time.scanf("%d:%d:%d:%d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,
&tm.second) != 6) {
return false;
}
}
// watch for bogosity
if (tm.year <= 1900 || tm.month <= 0 || tm.day < 0 || tm.hour < 0 || tm.minute < 0 || tm.second < 0)
return false;
tm.year -= 1900;
tm.month--;
tm.isdst = -1;
timestamp = tm.mktime();
return true;
}
public static string to_exif_date_time(time_t timestamp) {
return Time.local(timestamp).format("%Y:%m:%d %H:%M:%S");
}
public string to_string() {
return to_exif_date_time(timestamp);
}
}
......@@ -28,107 +28,6 @@ public enum MetadataDomain {
IPTC
}
public struct MetadataRational {
public int numerator;
public int denominator;
public MetadataRational(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
private bool is_component_valid(int component) {
return (!((component < 0) || (component > 1000000)));
}
public bool is_valid() {
return (is_component_valid(numerator) && is_component_valid(denominator));
}
public string to_string() {
return (is_valid()) ? ("%d/%d".printf(numerator, denominator)) : "";
}
}
errordomain MetadataDateTimeError {
INVALID_FORMAT,
UNSUPPORTED_FORMAT
}
public class MetadataDateTime {
private time_t timestamp;
public MetadataDateTime(time_t timestamp) {
this.timestamp = timestamp;
}
public MetadataDateTime.from_exif(string label) throws MetadataDateTimeError {
if (!from_exif_date_time(label, out timestamp))
throw new MetadataDateTimeError.INVALID_FORMAT("%s is not EXIF format date/time", label);
}
public MetadataDateTime.from_iptc(string date, string time) throws MetadataDateTimeError {
// TODO: Support IPTC date/time format
throw new MetadataDateTimeError.UNSUPPORTED_FORMAT("IPTC date/time format not currently supported");
}
public MetadataDateTime.from_xmp(string label) throws MetadataDateTimeError {
TimeVal time_val = TimeVal();
if (!time_val.from_iso8601(label))
throw new MetadataDateTimeError.INVALID_FORMAT("%s is not XMP format date/time", label);
timestamp = time_val.tv_sec;
}
public time_t get_timestamp() {
return timestamp;
}
public string get_exif_label() {
return to_exif_date_time(timestamp);
}
// TODO: get_iptc_date() and get_iptc_time()
public string get_xmp_label() {
TimeVal time_val = TimeVal();
time_val.tv_sec = timestamp;
time_val.tv_usec = 0;
return time_val.to_iso8601();
}
public static bool from_exif_date_time(string date_time, out time_t timestamp) {
Time tm = Time();
if (date_time.scanf("%d:%d:%d %d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,
&tm.second) != 6) {
// for Minolta DiMAGE E223 (colon, instead of space, separates day from hour in exif)
if (date_time.scanf("%d:%d:%d:%d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,
&tm.second) != 6) {
return false;
}
}
// watch for bogosity
if (tm.year <= 1900 || tm.month <= 0 || tm.day < 0 || tm.hour < 0 || tm.minute < 0 || tm.second < 0)
return false;
tm.year -= 1900;
tm.month--;
tm.isdst = -1;
timestamp = tm.mktime();
return true;
}
public static string to_exif_date_time(time_t timestamp) {
return Time.local(timestamp).format("%Y:%m:%d %H:%M:%S");
}
}
public abstract class PhotoPreview {
private string name;
private Dimensions dimensions;
......@@ -182,7 +81,7 @@ public abstract class PhotoPreview {
}
}
public class PhotoMetadata {
public class PhotoMetadata : MediaMetadata {
public enum SetOption {
ALL_DOMAINS,
ONLY_IF_DOMAIN_PRESENT,
......@@ -217,7 +116,7 @@ public class PhotoMetadata {
public PhotoMetadata() {
}
public void read_from_file(File file) throws Error {
public override void read_from_file(File file) throws Error {
exiv2 = new GExiv2.Metadata();
exif = null;
......@@ -747,6 +646,14 @@ public class PhotoMetadata {
remove_tags(DIGITIZED_DATE_TIME_TAGS);
}
public override MetadataDateTime? get_creation_date_time() {
MetadataDateTime? creation = get_exposure_date_time();
if (creation == null)
creation = get_digitized_date_time();
return creation;
}
private static string[] WIDTH_TAGS = {
"Exif.Photo.PixelXDimension",
"Xmp.exif.PixelXDimension",
......@@ -821,7 +728,7 @@ public class PhotoMetadata {
"Xmp.photoshop.Headline"
};
public string? get_title() {
public override string? get_title() {
string? title = has_tag(IPHOTO_TITLE_TAG)
? get_string_interpreted(IPHOTO_TITLE_TAG)
: get_first_string_interpreted(STANDARD_TITLE_TAGS);
......
/* Copyright 2010 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
extern void *lqt_open_read(string filename);
extern int quicktime_close(void *handle);
extern ulong lqt_get_creation_time(void *handle);
extern unowned string? quicktime_get_name(void *handle);
public class VideoMetadata : MediaMetadata {
// Quicktime calendar date/time format is number of seconds since January 1, 1904.
// This converts to UNIX time (66 years + 17 leap days).
private const ulong QUICKTIME_EPOCH_ADJUSTMENT = 2082844800;
private void *lqt_handle = null;
public VideoMetadata() {
}
~VideoMetadata() {
if (lqt_handle != null)
quicktime_close(lqt_handle);
}
public override void read_from_file(File file) throws Error {
lqt_handle = lqt_open_read(file.get_path());
if (lqt_handle == null)
throw new IOError.FAILED("Unable to open %s for video reading", file.get_path());
}
public override MetadataDateTime? get_creation_date_time() {
if (lqt_handle == null)
return null;
ulong creation_time = lqt_get_creation_time(lqt_handle);
if (creation_time < QUICKTIME_EPOCH_ADJUSTMENT)
return null;
creation_time -= QUICKTIME_EPOCH_ADJUSTMENT;
// Due to a bug in libquicktime, some file formats return current time rather than a stored
// time ... allow for one second difference in case there's a clock tick, which I don't
// anticipate, but then again, I don't anticipate a library returning current time for
// a stored value.
ulong current_time = (ulong) time_t();
if (creation_time == current_time)
return null;
else if ((creation_time < current_time) && ((current_time - creation_time) <= 1))
return null;
else if ((creation_time > current_time) && ((creation_time - current_time) <= 1))
return null;
return new MetadataDateTime((time_t) creation_time);
}
public override string? get_title() {
return (lqt_handle != null) ? quicktime_get_name(lqt_handle) : null;
}
}
......@@ -48,7 +48,7 @@ public class VideoReader {
}
public static string[] get_supported_file_extensions() {
string[] result = { "avi", "mpg", "mov", "mts", "ogg", "ogv" };
string[] result = { "avi", "mpg", "mov", "mts", "ogg", "ogv", "mp4" };
return result;
}
......@@ -116,6 +116,20 @@ public class VideoReader {
}
}
try {
VideoMetadata metadata = reader.read_metadata();
MetadataDateTime? creation_date_time = metadata.get_creation_date_time();
if (creation_date_time != null && creation_date_time.get_timestamp() != 0)
exposure_time = creation_date_time.get_timestamp();
string? video_title = metadata.get_title();
if (video_title != null)
title = video_title;
} catch (Error err) {
warning("Unable to read video metadata: %s", err.message);
}
params.row.video_id = VideoID();
params.row.filepath = file.get_path();
params.row.filesize = info.get_size();
......@@ -218,6 +232,13 @@ public class VideoReader {
return clip_duration;
}
public VideoMetadata read_metadata() throws Error {
VideoMetadata metadata = new VideoMetadata();
metadata.read_from_file(File.new_for_path(filepath));
return metadata;
}
}
// NOTE: this class is adapted from the class of the same name in project marina; see
......@@ -534,7 +555,7 @@ public class Video : VideoSource, Flaggable {
VideoTable.get_instance().set_title(backing_row.video_id, title);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return;
return;
}
// if we didn't short-circuit return in the catch clause above, then the change was
// successfully committed to the database, so update it in the in-memory row cache
......@@ -714,9 +735,9 @@ public class Video : VideoSource, Flaggable {
if (get_is_interpretable()) {
lock (backing_row) {
backing_row.is_interpretable = false;
}
try {
}
try {
VideoTable.get_instance().update_is_interpretable(get_video_id(), false);
} catch (DatabaseError e) {
AppWindow.database_error(e);
......@@ -832,6 +853,10 @@ public class Video : VideoSource, Flaggable {
public override void set_master_file(File file) {
// TODO: implement master update for videos
}
public VideoMetadata read_metadata() throws Error {
return (new VideoReader(get_filename())).read_metadata();
}
}
public class VideoSourceCollection : MediaSourceCollection {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment