diff options
Diffstat (limited to 'src/VideoMetadata.vala')
-rw-r--r-- | src/VideoMetadata.vala | 655 |
1 files changed, 0 insertions, 655 deletions
diff --git a/src/VideoMetadata.vala b/src/VideoMetadata.vala deleted file mode 100644 index 49ba8ef..0000000 --- a/src/VideoMetadata.vala +++ /dev/null @@ -1,655 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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; - } - -} - |