File Coverage

blib/lib/Image/ExifTool/WTV.pm
Criterion Covered Total %
statement 83 92 90.2
branch 37 56 66.0
condition 8 15 53.3
subroutine 6 6 100.0
pod 0 3 0.0
total 134 172 77.9


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: WTV.pm
3             #
4             # Description: Read WTV meta information
5             #
6             # Revisions: 2018-05-30 - P. Harvey Created
7             #
8             # References: 1) https://wiki.multimedia.cx/index.php?title=WTV
9             #------------------------------------------------------------------------------
10              
11             package Image::ExifTool::WTV;
12              
13 1     1   4515 use strict;
  1         2  
  1         36  
14 1     1   5 use vars qw($VERSION);
  1         2  
  1         39  
15 1     1   5 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         1684  
16              
17             $VERSION = '1.00';
18              
19             sub ProcessMetadata($$$);
20              
21             my %timeInfo = (
22             # time looks like 100 ns intervals since 0:00 UTC Jan 1, 0001 (ref PH)
23             ValueConv => q{ # (719162 days from 0001:01:01 to 1970:01:01)
24             my $t = $val / 1e7 - 719162*24*3600;
25             return Image::ExifTool::ConvertUnixTime($t) . 'Z';
26             },
27             PrintConv => '$self->ConvertDateTime($val)',
28             );
29              
30             my %bool = ( PrintConv => { 0 => 'No', 1 => 'Yes' }, PrintConvColumns => 2 );
31              
32             # WTV chunks
33             %Image::ExifTool::WTV::Main = (
34             GROUPS => { 0 => 'WTV', 1 => 'WTV', 2 => 'Video' },
35             NOTES => 'Tags found in Windows recorded TV (WTV) videos.',
36             # 'timeline.table.0.header.Events' (not decoded)
37             # 'timeline.table.0.entries.Events' (not decoded)
38             # 'timeline' (not decoded)
39             # 'table.0.header.legacy_attrib' (not decoded)
40             'table.0.entries.legacy_attrib' => {
41             Name => 'Metdata',
42             SubDirectory => { TagTable => 'Image::ExifTool::WTV::Metadata' },
43             },
44             # 'table.0.redirector.legacy_attrib' (not decoded)
45             # 'table.0.header.time' (not decoded)
46             # 'table.0.entries.time' (not decoded)
47             );
48              
49             # Note: Many of these tags are similar to those found in Image::ExifTool::Microsoft::Xtra
50             # and Image::ExifTool::ASF::ExtendedDescr
51             %Image::ExifTool::WTV::Metadata = (
52             GROUPS => { 0 => 'WTV', 1 => 'WTV', 2 => 'Video' },
53             PROCESS_PROC => \&ProcessMetadata,
54             NOTES => 'ExifTool will extract any tag found, even if not in this table.',
55             VARS => { NO_ID => 1 },
56             'Duration' => {
57             Name => 'Duration',
58             ValueConv => '$val/1e7',
59             PrintConv => 'ConvertDuration($val)',
60             },
61             'Title' => { },
62             'WM/Genre' => 'Genre',
63             'WM/Language' => 'Language',
64             'WM/MediaClassPrimaryID' => 'MediaClassPrimaryID',
65             'WM/MediaClassSecondaryID' => 'MediaClassSecondaryID',
66             'WM/MediaCredits' => 'MediaCredits',
67             'WM/MediaIsDelay' => { Name => 'MediaIsDelay', %bool },
68             'WM/MediaIsFinale' => { Name => 'MediaIsFinale', %bool },
69             'WM/MediaIsLive' => { Name => 'MediaIsLive', %bool },
70             'WM/MediaIsMovie' => { Name => 'MediaIsMovie', %bool },
71             'WM/MediaIsPremiere' => { Name => 'MediaIsPremiere', %bool },
72             'WM/MediaIsRepeat' => { Name => 'MediaIsRepeat', %bool },
73             'WM/MediaIsSAP' => { Name => 'MediaIsSAP', %bool },
74             'WM/MediaIsSport' => { Name => 'MediaIsSport', %bool },
75             'WM/MediaIsStereo' => { Name => 'MediaIsStereo', %bool, Groups => { 2 => 'Audio' } },
76             'WM/MediaIsSubtitled' => { Name => 'MediaIsSubtitled',%bool },
77             'WM/MediaIsTape' => { Name => 'MediaIsTape', %bool },
78             'WM/MediaNetworkAffiliation'=> 'MediaNetworkAffiliation',
79             'WM/MediaOriginalBroadcastDateTime' => {
80             Name => 'MediaOriginalBroadcastDateTime',
81             Groups => { 2 => 'Time' },
82             ValueConv => '$val =~ tr/-T/: /; $val',
83             PrintConv => '$self->ConvertDateTime($val)',
84             },
85             'WM/MediaOriginalChannel' => { Name => 'MediaOriginalChannel' },
86             'WM/MediaOriginalChannelSubNumber' => { Name => 'MediaOriginalChannelSubNumber' },
87             'WM/MediaOriginalRunTime' => {
88             Name => 'MediaOriginalRunTime',
89             ValueConv => '$val / 1e7',
90             PrintConv => 'ConvertDuration($val)',
91             },
92             'WM/MediaStationCallSign' => 'MediaStationCallSign',
93             'WM/MediaStationName' => 'MediaStationName',
94             'WM/MediaThumbAspectRatioX' => 'MediaThumbAspectRatioX',
95             'WM/MediaThumbAspectRatioY' => 'MediaThumbAspectRatioY',
96             'WM/MediaThumbHeight' => 'MediaThumbHeight',
97             'WM/MediaThumbRatingAttributes' => { Name => 'MediaThumbRatingAttributes' },
98             'WM/MediaThumbRatingLevel' => 'MediaThumbRatingLevel',
99             'WM/MediaThumbRatingSystem' => 'MediaThumbRatingSystem',
100             'WM/MediaThumbRet' => 'MediaThumbRet',
101             'WM/MediaThumbStride' => 'MediaThumbStride',
102             'WM/MediaThumbTimeStamp' => { Name => 'MediaThumbTimeStamp', Notes => 'unknown units', Unknown => 1 },
103             'WM/MediaThumbWidth' => 'MediaThumbWidth',
104             'WM/OriginalReleaseTime' => {
105             Name => 'OriginalReleaseTime',
106             Groups => { 2 => 'Time' },
107             ValueConv => '$val=~tr/-T/: /; $val',
108             PrintConv => '$self->ConvertDateTime($val)',
109             },
110             'WM/ParentalRating' => 'ParentalRating',
111             'WM/ParentalRatingReason' => 'ParentalRatingReason',
112             'WM/Provider' => 'Provider',
113             'WM/ProviderCopyright' => 'ProviderCopyright',
114             'WM/ProviderRating' => 'ProviderRating',
115             'WM/SubTitle' => 'Subtitle',
116             'WM/SubTitleDescription' => 'SubtitleDescription',
117             'WM/VideoClosedCaptioning' => { Name => 'VideoClosedCaptioning', %bool },
118             'WM/WMRVATSCContent' => { Name => 'ATSCContent', %bool },
119             'WM/WMRVActualSoftPostPadding' => 'ActualSoftPostPadding',
120             'WM/WMRVActualSoftPrePadding' => 'ActualSoftPrePadding',
121             'WM/WMRVBitrate' => { Name => 'Bitrate', Notes => 'unknown units', Unknown => 1 },
122             'WM/WMRVBrandingImageID' => 'BrandingImageID',
123             'WM/WMRVBrandingName' => 'BrandingName',
124             'WM/WMRVContentProtected' => { Name => 'ContentProtected', %bool },
125             'WM/WMRVContentProtectedPercent' => 'ContentProtectedPercent',
126             'WM/WMRVDTVContent' => { Name => 'DTVContent', %bool },
127             'WM/WMRVEncodeTime' => { Name => 'EncodeTime', Groups => { 2 => 'Time' }, %timeInfo },
128             'WM/WMRVEndTime' => { Name => 'EndTime', Groups => { 2 => 'Time' }, %timeInfo },
129             'WM/WMRVExpirationDate' => { Name => 'ExpirationDate', Groups => { 2 => 'Time' }, %timeInfo, Unknown => 1 },
130             'WM/WMRVExpirationSpan' => { Name => 'ExpirationSpan', Notes => 'unknown units', Unknown => 1 },
131             'WM/WMRVHDContent' => { Name => 'HDContent', %bool },
132             'WM/WMRVHardPostPadding' => 'HardPostPadding',
133             'WM/WMRVHardPrePadding' => 'HardPrePadding',
134             'WM/WMRVInBandRatingAttributes' => 'InBandRatingAttributes',
135             'WM/WMRVInBandRatingLevel' => 'InBandRatingLevel',
136             'WM/WMRVInBandRatingSystem' => 'InBandRatingSystem',
137             'WM/WMRVKeepUntil' => 'KeepUntil',
138             'WM/WMRVOriginalSoftPostPadding'=> 'OriginalSoftPostPadding',
139             'WM/WMRVOriginalSoftPrePadding' => 'OriginalSoftPrePadding',
140             'WM/WMRVProgramID' => 'ProgramID',
141             'WM/WMRVQuality' => 'Quality',
142             'WM/WMRVRequestID' => 'RequestID',
143             'WM/WMRVScheduleItemID' => 'ScheduleItemID',
144             'WM/WMRVSeriesUID' => 'SeriesUID',
145             'WM/WMRVServiceID' => 'ServiceID',
146             'WM/WMRVWatched' => { Name => 'Watched', %bool },
147             );
148              
149             #------------------------------------------------------------------------------
150             # Read specified sectors from the file
151             # Inputs: 0) raf ref, 1) sector table ref, 2) offset in sector table, 3) sector size
152             # Returns: Data or undef on error
153             sub ReadSectors($$$$)
154             {
155 3     3 0 9 my ($raf, $secPt, $pos, $secSize) = @_;
156 3         5 my ($data, $buff);
157 3         9 while ($pos <= length($$secPt) - 4) {
158 31         80 my $sec = Get32u($secPt, $pos);
159 31 50       62 return undef if $sec == 0xffff; # (just in case)
160 31 100       55 last unless $sec; # a null marks the end of the sector table
161 29 100       59 defined($data) ? ($data .= $buff) : ($data = $buff);
162 29 50 33     77 return undef unless $raf->Seek($sec*$secSize,0) and $raf->Read($buff,$secSize) == $secSize;
163 29         86 $pos += 4;
164             }
165 3 100       31 return defined($data) ? $data . $buff : $buff;
166             }
167              
168             #------------------------------------------------------------------------------
169             # Process WTV metadata
170             # Inputs: 0) ExifTool object reference, 1) dirInfo reference, 2) tag table ref
171             # Returns: 1 on success
172             sub ProcessMetadata($$$)
173             {
174 1     1 0 3 my ($et, $dirInfo, $tagTablePtr) = @_;
175 1         2 my $dataPt = $$dirInfo{DataPt};
176 1         2 my $pos = 0;
177 1         3 my $end = length $$dataPt;
178 1         5 $et->VerboseDir('WTV Metadata', undef, $end);
179 1         7 while ($pos + 0x18 < $end) {
180 74 100       171 last unless substr($$dataPt,$pos,16) eq "\x5a\xfe\xd7\x6d\xc8\x1d\x8f\x4a\x99\x22\xfa\xb1\x1c\x38\x14\x53";
181 73         170 my $fmt = Get32u($dataPt, $pos + 0x10);
182 73         161 my $len = Get32u($dataPt, $pos + 0x14);
183 73         125 my $str = '';
184 73         101 $pos += 0x18;
185 73         92 for (;;) {
186 1501 50       2385 $pos + 2 > $end and $et->Warn('Corrupt metadata directory'), last;
187 1501         1984 my $ch = substr($$dataPt, $pos, 2);
188 1501         1817 $pos += 2;
189 1501 100       2441 last if $ch eq "\0\0";
190 1428         1777 $str .= $ch;
191             }
192 73 50       140 last if $pos + $len > $end;
193 73         203 my $tag = $et->Decode($str, 'UCS2', undef, 'UTF8');
194 73         159 my $dat = substr($$dataPt, $pos, $len);
195             # add tag if not already there
196 73 50       230 unless ($$tagTablePtr{$tag}) {
197 0         0 my $name = $tag;
198 0         0 $name =~ s{^(WTV_Metadata_)?WM/(WMRV)?}{};
199 0         0 AddTagToTable($tagTablePtr, $tag, $name);
200 0         0 $et->VPrint(0, $$et{INDENT}, "[adding WTV:$name]\n");
201             }
202 73         95 my $val;
203 73 100 100     248 if ($fmt==0 or $fmt==3) { # int32u or boolean32
    100          
    100          
    50          
204 40         92 $val = Get32s(\$dat, 0);
205             } elsif ($fmt == 1) { # string
206 23         55 $val = $et->Decode($dat, 'UCS2');
207             } elsif ($fmt == 6) { # GUID
208 2         7 $val = unpack('H*', $dat);
209             } elsif ($fmt == 4) { # int64u (date/time values use this)
210 8         32 $val = Get64u(\$dat, 0);
211             } else {
212 0         0 $val = $dat;
213 0         0 $fmt = "Unknown($fmt)";
214             }
215 73         295 $et->HandleTag($tagTablePtr, $tag, $val,
216             Format => "format $fmt",
217             Size => length $dat,
218             );
219 73         238 $et->VerboseDump(\$dat);
220 73         217 $pos += $len;
221             }
222             }
223              
224             #------------------------------------------------------------------------------
225             # Extract information from a WTV video
226             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
227             # Returns: 1 on success, 0 if this wasn't a valid WTV file
228             sub ProcessWTV($$)
229             {
230 1     1 0 4 my ($et, $dirInfo) = @_;
231 1         2 my $raf = $$dirInfo{RAF};
232 1         4 my $verbose = $et->Options('Verbose');
233 1         3 my ($buff, $tagTablePtr, $pos, $len);
234              
235             # verify this is a valid WTV file
236 1 50       4 return 0 unless $raf->Read($buff, 0x60) == 0x60;
237 1 50       18 return 0 unless $buff =~ /^\xb7\xd8\x00\x20\x37\x49\xda\x11\xa6\x4e\x00\x07\xe9\x5e\xad\x8d/;
238 1         11 $et->SetFileType();
239 1         6 SetByteOrder('II');
240             # 0x28 - int32u: sector size? (=0x1000) (PH NC)
241             # 0x38 - int32u: sector number for main WTV directory (PH assume this is a sector table, NC)
242             # 0x58 - int32u: total number of sectors in file
243 1         8 my $secSize = Get32u(\$buff, 0x28);
244             # in case I'm wrong about this, constrain sector size to
245             # either 0x1000 (standard) or 0x100 (ExifTool test file) - PH
246 1 50 33     8 $secSize = 0x1000 unless $secSize == 0x1000 or $secSize == 0x100;
247 1         8 $buff = ReadSectors($raf, \$buff, 0x38, $secSize); # read the WTV directory
248 1 50       10 return 0 unless defined $buff;
249 1         5 $tagTablePtr = GetTagTable('Image::ExifTool::WTV::Main');
250             # parse the WTV directory
251 1         17 $et->VerboseDir('WTV');
252 1         7 for ($pos=0; $pos
253 9 100       26 unless (substr($buff,$pos,0x10) eq "\x92\xb7\x74\x91\x59\x70\x70\x44\x88\xdf\x06\x3b\x82\xcc\x21\x3d") {
254 1 50       4 $et->Warn("WTV directory wasn't at expected location") unless $pos;
255 1         3 last;
256             }
257 8         23 $len = Get32u(\$buff, $pos+0x10);
258 8 50       27 last if $pos + $len > length($buff);
259 8         18 my $n = Get32u(\$buff, $pos + 0x20);
260 8 50       54 0x28 + $n*2 + 8 > $len and $et->Warn('WTV directory error'), last;
261 8         34 my $tag = $et->Decode(substr($buff,$pos+0x28,$n*2), 'UCS2', undef, 'UTF8');
262 8         22 my $ptr = $pos + 0x28 + $n * 2;
263 8         21 my $flg = Get32u(\$buff, $ptr + 4);
264 8 50       21 if ($verbose) {
265 0         0 my $s = Get32s(\$buff, $ptr);
266 0 0       0 $s = sprintf('0x%x', $s) unless $s < 0;
267 0         0 $et->VPrint(1,"- Tag '${tag}' (sector=$s, flag=$flg)");
268             }
269 8 100 33     40 next unless $$tagTablePtr{$tag} and ($flg == 0 or $flg == 1);
      66        
270 1         4 my $sec = substr($buff, $ptr, 4);
271 1         4 my $data = ReadSectors($raf, \$sec, 0, $secSize);
272 1 50       3 last unless defined $data;
273             # read sectors from table if necessary (flag=1 indicates a sector table)
274 1 50       7 $data = ReadSectors($raf, \$data, 0, $secSize) if $flg == 1;
275 1 50       5 defined $data or $et->Warn("Error fetching data for $tag"), next;
276 1         6 $et->HandleTag($tagTablePtr, $tag, $data);
277             }
278 1         5 return 1;
279             }
280              
281             1; # end
282              
283             __END__