summaryrefslogtreecommitdiff
path: root/src/VideoMetadata.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/VideoMetadata.vala')
-rw-r--r--src/VideoMetadata.vala655
1 files changed, 655 insertions, 0 deletions
diff --git a/src/VideoMetadata.vala b/src/VideoMetadata.vala
new file mode 100644
index 0000000..100a040
--- /dev/null
+++ b/src/VideoMetadata.vala
@@ -0,0 +1,655 @@
+/* Copyright 2010-2014 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 class VideoMetadata : MediaMetadata {
+
+ private MetadataDateTime timestamp = null;
+ private string title = null;
+ private string comment = null;
+
+ public VideoMetadata() {
+ }
+
+ ~VideoMetadata() {
+ }
+
+ public override void read_from_file(File file) throws Error {
+ QuickTimeMetadataLoader quicktime = new QuickTimeMetadataLoader(file);
+ if (quicktime.is_supported()) {
+ timestamp = quicktime.get_creation_date_time();
+ title = quicktime.get_title();
+ // TODO: is there an quicktime.get_comment ??
+ comment = null;
+ return;
+ }
+ AVIMetadataLoader avi = new AVIMetadataLoader(file);
+ if (avi.is_supported()) {
+ timestamp = avi.get_creation_date_time();
+ title = avi.get_title();
+ comment = null;
+ return;
+ }
+
+ throw new IOError.NOT_SUPPORTED("File %s is not a supported video format", file.get_path());
+ }
+
+ public override MetadataDateTime? get_creation_date_time() {
+ return timestamp;
+ }
+
+ public override string? get_title() {
+ return title;
+ }
+
+ public override string? get_comment() {
+ return comment;
+ }
+
+}
+
+private class QuickTimeMetadataLoader {
+
+ // Quicktime calendar date/time format is number of seconds since January 1, 1904.
+ // This converts to UNIX time (66 years + 17 leap days).
+ public const time_t QUICKTIME_EPOCH_ADJUSTMENT = 2082844800;
+
+ private File file = null;
+
+ public QuickTimeMetadataLoader(File file) {
+ this.file = file;
+ }
+
+ public MetadataDateTime? get_creation_date_time() {
+ return new MetadataDateTime((time_t) get_creation_date_time_for_quicktime());
+ }
+
+ public string? get_title() {
+ // Not supported.
+ return null;
+ }
+
+ // Checks if the given file is a QuickTime file.
+ public bool is_supported() {
+ QuickTimeAtom test = new QuickTimeAtom(file);
+
+ bool ret = false;
+ try {
+ test.open_file();
+ test.read_atom();
+
+ // Look for the header.
+ if ("ftyp" == test.get_current_atom_name()) {
+ ret = true;
+ } else {
+ // Some versions of QuickTime don't have
+ // an ftyp section, so we'll just look
+ // for the mandatory moov section.
+ while(true) {
+ if ("moov" == test.get_current_atom_name()) {
+ ret = true;
+ break;
+ }
+ test.next_atom();
+ test.read_atom();
+ if (test.is_last_atom()) {
+ break;
+ }
+ }
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for QuickTime file for %s: %s", file.get_path(), e.message);
+ }
+
+ try {
+ test.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing Quicktime file: %s", e.message);
+ }
+ return ret;
+ }
+
+ private ulong get_creation_date_time_for_quicktime() {
+ QuickTimeAtom test = new QuickTimeAtom(file);
+ time_t timestamp = 0;
+
+ try {
+ test.open_file();
+ bool done = false;
+ while(!done) {
+ // Look for "moov" section.
+ test.read_atom();
+ if (test.is_last_atom()) break;
+ if ("moov" == test.get_current_atom_name()) {
+ QuickTimeAtom child = test.get_first_child_atom();
+ while (!done) {
+ // Look for "mvhd" section, or break if none is found.
+ child.read_atom();
+ if (child.is_last_atom() || 0 == child.section_size_remaining()) {
+ done = true;
+ break;
+ }
+
+ if ("mvhd" == child.get_current_atom_name()) {
+ // Skip 4 bytes (version + flags)
+ child.read_uint32();
+ // Grab the timestamp.
+ timestamp = child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT;
+ done = true;
+ break;
+ }
+ child.next_atom();
+ }
+ }
+ test.next_atom();
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for QuickTime file: %s", e.message);
+ }
+
+ try {
+ test.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing Quicktime file: %s", e.message);
+ }
+
+ // Some Android phones package videos recorded with their internal cameras in a 3GP
+ // container that looks suspiciously like a QuickTime container but really isn't -- for
+ // the timestamps of these Android 3GP videos are relative to the UNIX epoch
+ // (January 1, 1970) instead of the QuickTime epoch (January 1, 1904). So, if we detect a
+ // QuickTime movie with a negative timestamp, we can be pretty sure it isn't a valid
+ // QuickTime movie that was shot before 1904 but is instead a non-compliant 3GP video
+ // file. If we detect such a video, we correct its time. See this Redmine ticket
+ // (http://redmine.yorba.org/issues/3314) for more information.
+ if (timestamp < 0)
+ timestamp += QUICKTIME_EPOCH_ADJUSTMENT;
+
+ return (ulong) timestamp;
+ }
+}
+
+private class QuickTimeAtom {
+ private GLib.File file = null;
+ private string section_name = "";
+ private uint64 section_size = 0;
+ private uint64 section_offset = 0;
+ private GLib.DataInputStream input = null;
+ private QuickTimeAtom? parent = null;
+
+ public QuickTimeAtom(GLib.File file) {
+ this.file = file;
+ }
+
+ private QuickTimeAtom.with_input_stream(GLib.DataInputStream input, QuickTimeAtom parent) {
+ this.input = input;
+ this.parent = parent;
+ }
+
+ public void open_file() throws GLib.Error {
+ close_file();
+ input = new GLib.DataInputStream(file.read());
+ input.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
+ section_size = 0;
+ section_offset = 0;
+ section_name = "";
+ }
+
+ public void close_file() throws GLib.Error {
+ if (null != input) {
+ input.close();
+ input = null;
+ }
+ }
+
+ private void advance_section_offset(uint64 amount) {
+ section_offset += amount;
+ if (null != parent) {
+ parent.advance_section_offset(amount);
+ }
+ }
+
+ public QuickTimeAtom get_first_child_atom() {
+ // Child will simply have the input stream
+ // but not the size/offset. This works because
+ // child atoms follow immediately after a header,
+ // so no skipping is required to access the child
+ // from the current position.
+ return new QuickTimeAtom.with_input_stream(input, this);
+ }
+
+ public uchar read_byte() throws GLib.Error {
+ advance_section_offset(1);
+ return input.read_byte();
+ }
+
+ public uint32 read_uint32() throws GLib.Error {
+ advance_section_offset(4);
+ return input.read_uint32();
+ }
+
+ public uint64 read_uint64() throws GLib.Error {
+ advance_section_offset(8);
+ return input.read_uint64();
+ }
+
+ public void read_atom() throws GLib.Error {
+ // Read atom size.
+ section_size = read_uint32();
+
+ // Read atom name.
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ section_name = sb.str;
+
+ // Check string.
+ if (section_name.length != 4) {
+ throw new IOError.NOT_SUPPORTED("QuickTime atom name length is invalid for %s",
+ file.get_path());
+ }
+ for (int i = 0; i < section_name.length; i++) {
+ if (!section_name[i].isprint()) {
+ throw new IOError.NOT_SUPPORTED("Bad QuickTime atom in file %s", file.get_path());
+ }
+ }
+
+ if (1 == section_size) {
+ // This indicates the section size is a 64-bit
+ // value, specified below the atom name.
+ section_size = read_uint64();
+ }
+ }
+
+ private void skip(uint64 skip_amount) throws GLib.Error {
+ skip_uint64(input, skip_amount);
+ }
+
+ public uint64 section_size_remaining() {
+ assert(section_size >= section_offset);
+ return section_size - section_offset;
+ }
+
+ public void next_atom() throws GLib.Error {
+ skip(section_size_remaining());
+ section_size = 0;
+ section_offset = 0;
+ }
+
+ public string get_current_atom_name() {
+ return section_name;
+ }
+
+ public bool is_last_atom() {
+ return 0 == section_size;
+ }
+
+}
+
+private class AVIMetadataLoader {
+
+ private File file = null;
+
+ // A numerical date string, i.e 2010:01:28 14:54:25
+ private const int NUMERICAL_DATE_LENGTH = 19;
+
+ // Marker for timestamp section in a Nikon nctg blob.
+ private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13;
+
+ // Size limit to ensure we don't parse forever on a bad file.
+ private const int MAX_STRD_LENGTH = 100;
+
+ public AVIMetadataLoader(File file) {
+ this.file = file;
+ }
+
+ public MetadataDateTime? get_creation_date_time() {
+ return new MetadataDateTime((time_t) get_creation_date_time_for_avi());
+ }
+
+ public string? get_title() {
+ // Not supported.
+ return null;
+ }
+
+ // Checks if the given file is an AVI file.
+ public bool is_supported() {
+ AVIChunk chunk = new AVIChunk(file);
+ bool ret = false;
+ try {
+ chunk.open_file();
+ chunk.read_chunk();
+ // Look for the header and identifier.
+ if ("RIFF" == chunk.get_current_chunk_name() &&
+ "AVI " == chunk.read_name()) {
+ ret = true;
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return ret;
+ }
+
+ // Parses a Nikon nctg tag. Based losely on avi_read_nikon() in FFmpeg.
+ private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error {
+ bool found_date = false;
+ while (chunk.section_size_remaining() > sizeof(uint16)*2) {
+ uint16 tag = chunk.read_uint16();
+ uint16 size = chunk.read_uint16();
+ if (NIKON_NCTG_TIMESTAMP_MARKER == tag) {
+ found_date = true;
+ break;
+ }
+ chunk.skip(size);
+ }
+
+ if (found_date) {
+ // Read numerical date string, example: 2010:01:28 14:54:25
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) {
+ sb.append_c((char) chunk.read_byte());
+ }
+ return sb.str;
+ }
+ return "";
+ }
+
+ // Parses a Fujifilm strd tag. Based on information from:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error {
+ chunk.skip(98); // Ignore 98-byte binary blob.
+ chunk.skip(8); // Ignore the string "FUJIFILM"
+ // Read until we find four colons, then two more chars.
+ int colons = 0;
+ int post_colons = 0;
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ // End of date is two chars past the fourth colon.
+ while (colons <= 4 && post_colons < 2) {
+ char c = (char) chunk.read_byte();
+ if (4 == colons) {
+ post_colons++;
+ }
+ if (':' == c) {
+ colons++;
+ }
+ if (c.isprint()) {
+ sb.append_c(c);
+ }
+ if (sb.len > MAX_STRD_LENGTH) {
+ return ""; // Give up searching.
+ }
+ }
+
+ if (sb.str.length < NUMERICAL_DATE_LENGTH) {
+ return "";
+ }
+ // Date is now at the end of the string.
+ return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH);
+ }
+
+ // Recursively read file until the section is found.
+ private string? read_section(AVIChunk chunk) throws GLib.Error {
+ while (true) {
+ chunk.read_chunk();
+ string name = chunk.get_current_chunk_name();
+ if ("IDIT" == name) {
+ return chunk.section_to_string();
+ } else if ("nctg" == name) {
+ return read_nikon_nctg_tag(chunk);
+ } else if ("strd" == name) {
+ return read_fuji_strd_tag(chunk);
+ }
+
+ if ("LIST" == name) {
+ chunk.read_name(); // Read past list name.
+ string result = read_section(chunk.get_first_child_chunk());
+ if (null != result) {
+ return result;
+ }
+ }
+
+ if (chunk.is_last_chunk()) {
+ break;
+ }
+ chunk.next_chunk();
+ }
+ return null;
+ }
+
+ // Parses a date from a string.
+ // Largely based on GStreamer's avi/gstavidemux.c
+ // and the information here:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private ulong parse_date(string sdate) {
+ if (sdate.length == 0) {
+ return 0;
+ }
+
+ Date date = Date();
+ uint seconds = 0;
+ int year, month, day, hour, min, sec;
+ char weekday[4];
+ char monthstr[4];
+
+ if (sdate[0].isdigit()) {
+ // Format is: 2005:08:17 11:42:43
+ // Format is: 2010/11/30/ 19:42
+ // Format is: 2010/11/30 19:42
+ string tmp = sdate.dup();
+ tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces
+ sec = 0;
+ int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec);
+ if(result < 5) {
+ return 0;
+ }
+ date.set_dmy((DateDay) day, (DateMonth) month, (DateYear) year);
+ seconds = sec + min * 60 + hour * 3600;
+ } else {
+ // Format is: Mon Mar 3 09:44:56 2008
+ if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour,
+ out min, out sec, out year)) {
+ return 0; // Error
+ }
+ date.set_dmy((DateDay) day, month_from_string((string) monthstr), (DateYear) year);
+ seconds = sec + min * 60 + hour * 3600;
+ }
+
+ Time time = Time();
+ date.to_time(out time);
+
+ // watch for overflow (happens on quasi-bogus dates, like Year 200)
+ time_t tm = time.mktime();
+ ulong result = tm + seconds;
+ if (result < tm) {
+ debug("Overflow for timestamp in video file %s", file.get_path());
+
+ return 0;
+ }
+
+ return result;
+ }
+
+ private DateMonth month_from_string(string s) {
+ switch (s.down()) {
+ case "jan":
+ return DateMonth.JANUARY;
+ case "feb":
+ return DateMonth.FEBRUARY;
+ case "mar":
+ return DateMonth.MARCH;
+ case "apr":
+ return DateMonth.APRIL;
+ case "may":
+ return DateMonth.MAY;
+ case "jun":
+ return DateMonth.JUNE;
+ case "jul":
+ return DateMonth.JULY;
+ case "aug":
+ return DateMonth.AUGUST;
+ case "sep":
+ return DateMonth.SEPTEMBER;
+ case "oct":
+ return DateMonth.OCTOBER;
+ case "nov":
+ return DateMonth.NOVEMBER;
+ case "dec":
+ return DateMonth.DECEMBER;
+ }
+ return DateMonth.BAD_MONTH;
+ }
+
+ private ulong get_creation_date_time_for_avi() {
+ AVIChunk chunk = new AVIChunk(file);
+ ulong timestamp = 0;
+ try {
+ chunk.open_file();
+ chunk.nonsection_skip(12); // Advance past 12 byte header.
+ string sdate = read_section(chunk);
+ if (null != sdate) {
+ timestamp = parse_date(sdate.strip());
+ }
+ } catch (GLib.Error e) {
+ debug("Error while reading AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return timestamp;
+ }
+}
+
+private class AVIChunk {
+ private GLib.File file = null;
+ private string section_name = "";
+ private uint64 section_size = 0;
+ private uint64 section_offset = 0;
+ private GLib.DataInputStream input = null;
+ private AVIChunk? parent = null;
+ private const int MAX_STRING_TO_SECTION_LENGTH = 1024;
+
+ public AVIChunk(GLib.File file) {
+ this.file = file;
+ }
+
+ private AVIChunk.with_input_stream(GLib.DataInputStream input, AVIChunk parent) {
+ this.input = input;
+ this.parent = parent;
+ }
+
+ public void open_file() throws GLib.Error {
+ close_file();
+ input = new GLib.DataInputStream(file.read());
+ input.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN);
+ section_size = 0;
+ section_offset = 0;
+ section_name = "";
+ }
+
+ public void close_file() throws GLib.Error {
+ if (null != input) {
+ input.close();
+ input = null;
+ }
+ }
+
+ public void nonsection_skip(uint64 skip_amount) throws GLib.Error {
+ skip_uint64(input, skip_amount);
+ }
+
+ public void skip(uint64 skip_amount) throws GLib.Error {
+ advance_section_offset(skip_amount);
+ skip_uint64(input, skip_amount);
+ }
+
+ public AVIChunk get_first_child_chunk() {
+ return new AVIChunk.with_input_stream(input, this);
+ }
+
+ private void advance_section_offset(uint64 amount) {
+ if ((section_offset + amount) > section_size)
+ amount = section_size - section_offset;
+
+ section_offset += amount;
+ if (null != parent) {
+ parent.advance_section_offset(amount);
+ }
+ }
+
+ public uchar read_byte() throws GLib.Error {
+ advance_section_offset(1);
+ return input.read_byte();
+ }
+
+ public uint16 read_uint16() throws GLib.Error {
+ advance_section_offset(2);
+ return input.read_uint16();
+ }
+
+ public void read_chunk() throws GLib.Error {
+ // don't use checked reads here because they advance the section offset, which we're trying
+ // to determine here
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ section_name = sb.str;
+ section_size = input.read_uint32();
+ section_offset = 0;
+ }
+
+ public string read_name() throws GLib.Error {
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ return sb.str;
+ }
+
+ public void next_chunk() throws GLib.Error {
+ skip(section_size_remaining());
+ section_size = 0;
+ section_offset = 0;
+ }
+
+ public string get_current_chunk_name() {
+ return section_name;
+ }
+
+ public bool is_last_chunk() {
+ return section_size == 0;
+ }
+
+ public uint64 section_size_remaining() {
+ assert(section_size >= section_offset);
+ return section_size - section_offset;
+ }
+
+ // Reads section contents into a string.
+ public string section_to_string() throws GLib.Error {
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ while (section_offset < section_size) {
+ sb.append_c((char) read_byte());
+ if (sb.len > MAX_STRING_TO_SECTION_LENGTH) {
+ return sb.str;
+ }
+ }
+ return sb.str;
+ }
+
+}
+