File Coverage

blib/lib/DVB/Epg.pm
Criterion Covered Total %
statement 463 550 84.1
branch 111 172 64.5
condition 44 110 40.0
subroutine 39 45 86.6
pod 16 16 100.0
total 673 893 75.3


line stmt bran cond sub pod time code
1             package DVB::Epg;
2              
3             =head1 NAME
4              
5             DVB::Epg - Generate MPEG-2 transport stream chunk containing DVB Event Information table.
6              
7             =head1 SYNOPSIS
8              
9             This module allows generating of DVB EPG service by building EIT p/f and schedule tables.
10             First some event information must be added to the system. A sqlite database for storage is used.
11             Based on this event information the library builds the standardized EIT, which can then be
12             export as a MPEG-2 Transport Stream/chunk for playout. The result of the whole process is an EIT
13             inside a MTS.
14              
15             use DVB::Epg;
16              
17             my $myEpg = DVB::Epg->new( 'eitfile');
18              
19             # create empty database
20             $myEpg->initdb();
21              
22             # add program to EIT for which to generate EPG
23             $myEpg->addEit( 18, 9019, 1024, 15, 8, 1);
24            
25             # add dummy event data to database
26             my $event = {};
27             $event->{start} = time;
28             $event->{stop} = time+100;
29             $event->{uid} = 15;
30             $myEpg->addEvent( $event);
31            
32             # generate EPG tables to database
33             $myEpg->updateEit( 18);
34              
35             # export EIT as MTS from database
36             my $mts = $myEpg->getEit( 18);
37              
38             The Library can handle multiple services and multiple tables.
39              
40             =head1 CLASS C
41              
42             =head2 METHODS
43              
44             =cut
45              
46             package DVB::EventInformationTable;
47              
48             package DVB::Epg;
49              
50 1     1   22623 use 5.010;
  1         3  
  1         38  
51 1     1   12 use strict;
  1         2  
  1         26  
52 1     1   4 use warnings;
  1         5  
  1         25  
53 1     1   924 use utf8;
  1         10  
  1         5  
54 1     1   5825 use DBI qw(:sql_types);
  1         17271  
  1         474  
55 1     1   1068 use Storable qw(freeze thaw);
  1         3208  
  1         69  
56 1     1   7 use Carp;
  1         1  
  1         87  
57 1     1   6 use Exporter;
  1         1  
  1         36  
58 1     1   770 use POSIX qw(ceil);
  1         7420  
  1         6  
59              
60 1     1   1237 use vars qw($VERSION @ISA @EXPORT);
  1         2  
  1         4262  
61              
62             our $VERSION = "0.51";
63             our @ISA = qw(Exporter);
64             our @EXPORT = qw();
65              
66             =head3 new( $dbfile )
67              
68             Class initialization with sqlite3 database filename.
69             Open existing or create new sqlite database.
70              
71             =cut
72              
73             sub new {
74 1     1 1 12 my $this = shift;
75 1   33     7 my $class = ref($this) || $this;
76 1         2 my $self = {};
77              
78 1         4 $self->{filename} = shift;
79 1 50       11 $self->{dbh} = DBI->connect( "dbi:SQLite:" . $self->{filename} ) or return;
80            
81 1         10116 $self->{dbh}->{sqlite_unicode} = 1;
82 1         13 $self->{dbh}->do( "PRAGMA synchronous = OFF; PRAGMA temp_store = MEMORY; PRAGMA auto_vacuum = NONE; PRAGMA cache_size = 4000000; " );
83              
84 1         568 bless( $self, $class );
85 1         8 return $self;
86             }
87              
88             =head3 dbh( )
89              
90             Return database handle for direct control.
91              
92             =cut
93              
94             sub dbh {
95 0     0 1 0 return $_[0]->{dbh};
96             }
97              
98             =head3 initdb( )
99              
100             Initialize database with some basic table structure;
101              
102             =cut
103              
104             sub initdb {
105 1     1 1 3 my $self = shift;
106 1         2 my $dbh = $self->{dbh};
107              
108 1         6 $dbh->do("BEGIN TRANSACTION");
109              
110 1         68 $dbh->do( "DROP TABLE IF EXISTS event");
111 1         1120 $dbh->do( "DROP TABLE IF EXISTS eit");
112 1         602 $dbh->do( "DROP TABLE IF EXISTS eit_version");
113 1         246 $dbh->do( "DROP TABLE IF EXISTS section");
114              
115 1         385 $dbh->do( "CREATE TABLE event ( event_id INTEGER,
116             uid INTEGER,
117             start DATE,
118             stop DATE,
119             info BLOB,
120             timestamp DATE,
121             PRIMARY KEY( uid, event_id))");
122              
123 1         226 $dbh->do( "CREATE TABLE eit ( pid INTEGER,
124             service_id INTEGER,
125             original_network_id INTEGER,
126             transport_stream_id INTEGER,
127             uid INTEGER,
128             maxsegments INTEGER,
129             actual INTEGER,
130             comment TEXT,
131             PRIMARY KEY( pid, original_network_id, transport_stream_id, service_id))");
132              
133 1         198 $dbh->do( "CREATE TABLE eit_version ( pid INTEGER,
134             service_id INTEGER,
135             table_id INTEGER,
136             version_number INTEGER,
137             timestamp DATE,
138             PRIMARY KEY( pid, service_id, table_id))");
139              
140 1         177 $dbh->do( "CREATE TABLE section ( pid INTEGER,
141             table_id INTEGER,
142             service_id INTEGER,
143             section_number INTEGER,
144             dump BLOB,
145             PRIMARY KEY( pid, service_id, table_id, section_number))");
146              
147             # define triggers that set timestamps on each event when updated
148 1         165 $dbh->do( "CREATE TRIGGER event_timestamp_insert
149             AFTER INSERT ON event
150             BEGIN
151             UPDATE event
152             SET timestamp = DATETIME('NOW')
153             WHERE event_id = new.event_id
154             AND uid = new.uid;
155             END;");
156              
157 1         161 $dbh->do( "CREATE TRIGGER event_timestamp_update
158             AFTER UPDATE ON event
159             BEGIN
160             UPDATE event
161             SET timestamp = DATETIME('NOW')
162             WHERE event_id = new.event_id
163             AND uid = new.uid;
164             END;");
165              
166 1         155 $dbh->do( "CREATE TRIGGER eit_delete
167             AFTER DELETE ON eit FOR EACH ROW
168             BEGIN
169             DELETE FROM eit_version
170             WHERE eit_version.pid = old.pid
171             AND eit_version.service_id = old.service_id;
172             DELETE FROM section
173             WHERE section.pid = old.pid
174             AND section.service_id = old.service_id;
175             END;");
176              
177 1         209 return $dbh->do("COMMIT");
178             }
179              
180             =head3 addEvent( $event)
181              
182             Add an $event to event table.
183             $event must be reference to hash containing at least
184             fields: $event->{start}, $event->{stop}, $event->{uid}
185              
186             start, stop MUST be in EPOCH
187              
188             Optional fields are:
189             $event->{id}, $event->{running_status}, $event->{free_CA_mode}
190             and $event->{descriptors}
191              
192             Return event_key of inserted row.
193              
194             =cut
195              
196             sub addEvent {
197 204     204 1 6117 my $self = shift;
198 204         365 my ($event) = @_;
199 204         340 my $dbh = $self->{dbh};
200              
201 204 50 33     1924 if ( !exists $event->{uid}
      33        
202             or !exists $event->{stop}
203             or !exists $event->{start}
204             or $event->{stop} <= $event->{start}) {
205 0         0 return;
206             }
207              
208 204         546 $event->{duration} = $event->{stop} - $event->{start};
209 204 50       510 $event->{running_status} = exists $event->{running_status} ? $event->{running_status} & 1 : 0;
210 204 50       557 $event->{free_CA_mode} = exists $event->{free_CA_mode} ? $event->{free_CA_mode} & 1 : 0;
211              
212             # in case when no event_id is defined
213 204 100       474 if ( !defined $event->{id}) {
214              
215             # find highest event_id currently used
216 104         964 my @row = $dbh->selectrow_array( "SELECT event_id FROM event WHERE "
217             . "uid = '$event->{uid}' "
218             . "ORDER BY event_id DESC LIMIT 1" );
219              
220 104         18158 my $last_event_id;
221              
222             # check if query returned result
223 104 100       257 if ( $#row == 0 ) {
224 101         143 $last_event_id = $row[0];
225 101 50       320 if ( $last_event_id >= 0xffff ) {
226              
227             # check step by step if index from 0 on are in use
228 0         0 my $num = $dbh->prepare(
229             "SELECT event_id FROM event WHERE "
230             . "uid = '$event->{uid}' "
231             . "ORDER BY event_id" );
232 0         0 $num->execute();
233 0         0 my $lastused = -1;
234 0         0 my $result;
235 0         0 while ( $result = $num->fetch() ) {
236 0 0       0 if ( ${$result}[0] - $lastused > 1 ) {
  0         0  
237 0         0 $last_event_id = $lastused + 1;
238 0         0 last;
239             }
240 0         0 $lastused = ${$result}[0];
  0         0  
241             }
242             }
243             else {
244              
245             # and increment by 1
246 101         133 ++$last_event_id;
247             }
248             }
249             else {
250              
251             # there is no result, no events exist
252 3         4 $last_event_id = 0;
253             }
254 104         380 $event->{id} = $last_event_id;
255             }
256              
257             # limit to 16 bit (integer)
258 204         317 $event->{id} &= 0xffff;
259              
260             # prepare the insertation
261 204         1586 my $insert = $dbh->prepare(
262             "INSERT or REPLACE INTO event VALUES ( $event->{id}, $event->{uid},
263             datetime( $event->{start},'unixepoch'), datetime( $event->{stop},'unixepoch'), ?, NULL)"
264             );
265 204 50       19882 return if !$insert;
266              
267             # bind blob and insert event
268 204         572 $insert->bind_param( 1, freeze($event), SQL_BLOB );
269 204 50       119934 if ( $insert->execute() ) {
270 204         3444 return $event->{id};
271             }
272             else {
273 0         0 return;
274             }
275             }
276              
277             =head3 listEvent( $uid, $event_id, $start, $stop, $touch)
278              
279             List events with $uid in cronological order.
280              
281             $event_id, $start, $stop, $touch are optional parameters.
282             $event_id is used as selection filter.
283             $start, $stop are used as interval specification.
284             If $touch is defined only elements with timestamp newer than
285             $touch are returned.
286              
287             Return array of events.
288              
289             =cut
290              
291             sub listEvent {
292 2     2 1 18 my $self = shift;
293 2         8 my ( $uid, $event_id, $start, $stop, $touch ) = @_;
294 2         6 my $dbh = $self->{dbh};
295              
296 2 50       8 if ( ! defined $uid) {
297 0         0 return;
298             }
299              
300 2 50       34 my $sel = $dbh->prepare( "SELECT event_id, uid, strftime('%s',start) AS start, "
    50          
    50          
    50          
301             . " strftime('%s',stop) AS time, info, strftime('%s',timestamp) AS timestamp FROM event "
302             . " WHERE uid=$uid "
303             . ( defined $event_id ? " AND event_id = $event_id" : "" )
304             . ( defined $start ? " AND start >= datetime( $start,'unixepoch')" : "")
305             . ( defined $stop ? " AND stop <= datetime( $stop,'unixepoch')" : "" )
306             . ( defined $touch ? " AND timestamp > datetime( $touch,'unixepoch')" : "")
307             . " ORDER BY start" );
308 2         2178 $sel->execute();
309              
310 2         7 my ( $_event_id, $_uid, $_start, $_stop, $_info, $_timestamp );
311 2         19 $sel->bind_columns( \( $_event_id, $_uid, $_start, $_stop, $_info, $_timestamp ) );
312              
313 2         78 my @list;
314              
315 2         31 while ( $sel->fetch ) {
316 300         676 my $data = thaw($_info);
317 300         6037 $data->{event_id} = $_event_id;
318 300         402 $data->{uid} = $_uid;
319 300         362 $data->{start} = $_start;
320 300         319 $data->{stop} = $_stop;
321 300         474 $data->{timestamp} = $_timestamp;
322 300         2054 push( @list, $data );
323             }
324 2         426 return @list;
325             }
326              
327             =head3 deleteEvent( $uid, $event_id, $start_min, $start_max, $stop_min, $stop_max)
328              
329             Delete events with $uid.
330              
331             $event_id, $stop_min, $stop_max, $start_min and $start_max are optional parameters.
332             $uid and $event_id are used as selection filter.
333              
334             Delete events that have start in between $start_min, $start_max and stop in between
335             $stop_min, $stop_max. Use only defined markers.
336              
337             Return number of deleted events.
338              
339             =cut
340              
341             sub deleteEvent {
342 4     4 1 4964 my $self = shift;
343 4         11 my ( $uid, $event_id, $start_min, $start_max, $stop_min, $stop_max) = @_;
344 4         10 my $dbh = $self->{dbh};
345            
346 4 100       88 return $dbh->do( "DELETE FROM event WHERE 1"
    100          
    100          
    100          
    100          
    100          
347             . ( defined $uid ? " AND uid=$uid" : "" )
348             . ( defined $event_id ? " AND event_id=$event_id" : "" )
349             . ( defined $start_min ? " AND start >= datetime( $start_min,'unixepoch')" : "")
350             . ( defined $start_max ? " AND start < datetime( $start_max,'unixepoch')" : "")
351             . ( defined $stop_min ? " AND stop > datetime( $stop_min,'unixepoch')" : "")
352             . ( defined $stop_max ? " AND stop <= datetime( $stop_max,'unixepoch')" : "")
353             );
354             }
355              
356             =head3 addEit( $pid, $service_id, $original_network_id, $transport_stream_id, $uid, $maxsegments, $actual, $comment)
357              
358             Add eit generator rule.
359             Maxsegments defines how far in the future the tables should be generated - each segment defines 3 hour period.
360             All parameters must be defined.
361              
362             Return 1 on success.
363              
364             =cut
365              
366             sub addEit {
367 3     3 1 1814 my $self = shift;
368 3         8 my ( $pid, $service_id, $original_network_id, $transport_stream_id, $uid, $maxsegments, $actual, $comment) = @_;
369 3         7 my $dbh = $self->{dbh};
370              
371 3 50 33     51 if ( !defined $pid
      33        
      33        
      33        
      33        
      33        
372             or !defined $service_id
373             or !defined $original_network_id
374             or !defined $transport_stream_id
375             or !defined $uid
376             or !defined $maxsegments
377             or !defined $actual) {
378 0         0 return;
379             };
380              
381 3 50       9 $comment = "" if ! defined $comment;
382 3         34 return $dbh->do( "INSERT or REPLACE INTO eit VALUES ( $pid, $service_id, $original_network_id, $transport_stream_id, $uid, $maxsegments, $actual, '$comment')");
383              
384             # $maxsegments, $actual,"."\"\"".")");
385             }
386              
387             =head3 listEit( )
388              
389             List eit generator rules.
390              
391             Return reference to an array of arrays of rules.
392              
393             =cut
394              
395             sub listEit {
396 1     1 1 1096 my $self = shift;
397 1         2 my $dbh = $self->{dbh};
398              
399 1         12 return $dbh->selectall_arrayref( "SELECT * FROM eit ORDER BY pid, uid");
400             }
401              
402             =head3 listPid( )
403              
404             List all destination pid defined in eit generator rules.
405              
406             Return array of pids.
407              
408             =cut
409              
410             sub listPid {
411 0     0 1 0 my $self = shift;
412 0         0 my $dbh = $self->{dbh};
413              
414 0         0 my $ref = $dbh->selectcol_arrayref( "SELECT DISTINCT pid FROM eit ORDER BY pid");
415 0 0       0 return ( defined ($ref) ? @{$ref} : () );
  0         0  
416             }
417              
418             =head3 deleteEit( $pid, $service_id, $original_network_id, $transport_stream_id)
419              
420             Delete eit generator rule.
421             Parameters are optional.
422              
423             Return number of deleted rules.
424              
425             =cut
426              
427             sub deleteEit {
428 2     2 1 1125 my $self = shift;
429 2         4 my ( $pid, $service_id, $original_network_id, $transport_stream_id) = @_;
430 2         5 my $dbh = $self->{dbh};
431              
432 2 100       31 return $dbh->do( "DELETE FROM eit WHERE 1"
    100          
    50          
    50          
433             . ( defined $pid ? " AND pid=$pid" : "" )
434             . ( defined $service_id ? " AND service_id=$service_id" : "" )
435             . ( defined $original_network_id ? " AND original_network_id=$original_network_id" : "" )
436             . ( defined $transport_stream_id ? " AND transport_stream_id=$transport_stream_id" : "" ) );
437             }
438              
439             =head3 updateEit( $pid )
440              
441             Use eit rules for updateing Eit sections of given $pid in database.
442              
443             Return 1 on success.
444             Return 0 if sections are already uptodate.
445             Return undef on error;
446              
447             =cut
448              
449             sub updateEit {
450 4     4 1 806 my $self = shift;
451 4         10 my $pid = shift;
452 4         187 my $dbh = $self->{dbh};
453 4         10 my $updated = 0;
454              
455 4 50       17 if ( !defined $pid) {
456 0         0 return;
457             }
458              
459 4         214 my $sel = $dbh->prepare("SELECT * FROM eit WHERE pid=$pid");
460              
461 4         1300 $sel->execute();
462              
463 4         9 my $ret;
464             my $rule;
465 4         163 while ( $rule = $sel->fetchrow_hashref ) {
466              
467             # first calculate present/following
468 4         29 $ret = $self->updateEitPresent($rule);
469 4 50       1581 if( ! defined $ret) {
470 0         0 return;
471             };
472 4         11 $updated |= $ret;
473              
474             # and then calculate schedule
475 4 100       56 if ( $rule->{maxsegments} > 0 ) {
476 2         13 $ret = $self->updateEitSchedule($rule);
477 2 50       8 if( ! defined $ret) {
478 0         0 return;
479             };
480 2         45 $updated |= $ret;
481             }
482             }
483 4         100 return $updated;
484             }
485              
486             =head3 updateEitPresent( $rule, $forced)
487              
488             Update eit sections for given $rule.
489             $rule is reference to hash containing keys:
490             pid, service_id, original_network_id, transport_stream_id, service_id, maxsegments, actual
491              
492             Update sections only if there are changes in event table of schedule since last update or
493             the $forced flag is set to 1.
494              
495             Return undef if failed.
496             Return 0 if sections are already uptodate.
497             Return 1 after updating sections.
498              
499             =cut
500              
501             sub updateEitPresent {
502 4     4 1 8 my $self = shift;
503 4         9 my $rule = shift;
504 4   50     22 my $forced = shift // 0;
505 4         10 my $dbh = $self->{dbh};
506              
507             # extend the $rule information
508 4 50       18 $rule->{table_id} = $rule->{actual} == 1 ? 0x4e : 0x4f;
509              
510 4         28 my $present_following = new DVB::EventInformationTable($rule);
511              
512             # lookup version_number used at last generation of eit and timestamp
513 4         44 my $select = $dbh->prepare( "SELECT version_number, strftime('%s',timestamp) FROM eit_version "
514             ." WHERE pid=$rule->{pid} AND table_id=$rule->{table_id} AND service_id=$rule->{service_id}" );
515              
516 4         575 $select->execute();
517 4         34 my ( $last_version_number, $last_update_timestamp ) = $select->fetchrow_array();
518              
519 4 50       16 if( $forced) {
520 0         0 $last_update_timestamp = 0;
521             }
522              
523             # if lookup wasn't succesfull we need to update the eit anyway
524 4 100       15 if ( !defined $last_version_number ) {
525 3         4 $last_update_timestamp = 0;
526 3         4 $last_version_number = 0;
527             }
528              
529              
530             # always use this time in queries
531 4         10 my $current_time = time();
532              
533             # find last started event
534 4         34 $select = $dbh->prepare( "SELECT event_id, strftime('%s',start) AS start, strftime('%s',stop) AS stop, "
535             . " info, strftime('%s',timestamp) AS timestamp FROM event "
536             . " WHERE uid=$rule->{uid} AND start <= datetime( $current_time,'unixepoch') ORDER BY start DESC LIMIT 1" );
537 4         1369 $select->execute();
538              
539 4         153 my $last_started_event = $select->fetchrow_hashref;
540              
541             # find following event
542 4         43 $select = $dbh->prepare( "SELECT event_id, strftime('%s',start) AS start, strftime('%s',stop) AS stop, "
543             . " info, strftime('%s',timestamp) AS timestamp FROM event "
544             . " WHERE uid=$rule->{uid} AND start > datetime( $current_time,'unixepoch') ORDER BY start LIMIT 1" );
545 4         3735 $select->execute();
546              
547 4         152 my $following_event = $select->fetchrow_hashref;
548              
549 4         18 my $buildEit = 0;
550              
551             # check if we need an update
552             # is the last started event still lasting
553 4 100 66     41 if ( defined $last_started_event && $last_started_event->{stop} > $current_time ) {
554              
555             # was the start already published or is there a change in the event data
556 3 50 66     30 if (
      33        
      66        
557             $last_started_event->{start} > $last_update_timestamp
558             || # present event started after last update of eit
559             $last_started_event->{timestamp} > $last_update_timestamp
560             || # present event was modified since last update of eit
561             defined $following_event
562             && $following_event->{timestamp} > $last_update_timestamp
563             ) # following event was modified since last update of eit
564             {
565 2         5 $buildEit = 1;
566             }
567             }
568             else {
569              
570             # last event is over - there is a gap now
571              
572             # was the end of the last event published or is there a change in event data of following event
573 1 50 33     12 if ( defined $last_started_event && $last_started_event->{stop} > $last_update_timestamp
      0        
      33        
574             || # end of last started event was not pulished
575             defined $following_event && $following_event->{timestamp} > $last_update_timestamp
576             ) # followig event was modified
577             {
578 1         5 $buildEit = 1;
579             }
580             }
581              
582 4 100       32 return 0 if !$buildEit;
583              
584 3         5 my $pevent;
585              
586             # if there is a current event add it to table
587             # or add an empty section
588 3 100 66     23 if ( defined $last_started_event && $last_started_event->{stop} > $current_time ) {
589 2         8 $pevent = _unfreezeEvent( $last_started_event );
590 2         5 $pevent->{running_status} = 4;
591             }
592 3         16 $present_following->add2Section( 0, $pevent );
593              
594             # if there is a following event add it to table
595 3         5 my $fevent;
596 3 50       10 if ( defined $following_event ) {
597 3         8 $fevent = _unfreezeEvent( $following_event );
598 3 50       11 $fevent->{running_status} = ( $following_event->{start} - $current_time ) < 20 ? 2 : 1;
599             }
600 3         8 $present_following->add2Section( 1, $fevent );
601              
602             #
603             # Add this to playout and update version
604 3         4 ++$last_version_number;
605              
606             # Remove all section of this table
607             return
608 3 50       27 if !$dbh->do( "DELETE FROM section WHERE pid=$rule->{pid} AND service_id=$rule->{service_id} AND table_id=$rule->{table_id}" );
609              
610 3         630 my $insert = $dbh->prepare( "INSERT INTO section VALUES ( $rule->{pid}, $rule->{table_id}, $rule->{service_id}, ?, ?)");
611 3 50       167 return if !$insert;
612              
613 3         14 my $sections = $present_following->getSections($last_version_number);
614              
615 3         23 foreach my $section_number ( keys %$sections ) {
616 6         60 $insert->bind_param( 1, $section_number );
617 6         33 $insert->bind_param( 2, $sections->{$section_number}, SQL_BLOB );
618 6         2766 $insert->execute();
619             }
620 3         63 return $dbh->do( "INSERT OR REPLACE INTO eit_version VALUES ($rule->{pid}, $rule->{service_id}, "
621             . "$rule->{table_id}, $last_version_number, datetime( $current_time,'unixepoch'))"
622             );
623             }
624              
625             =head3 updateEitSchedule( $rule)
626              
627             Update eit playout packet for given $rule.
628             $rule is reference to hash containing keys:
629             pid, service_id, original_network_id, transport_stream_id, service_id, maxsegments, actual
630              
631             =cut
632              
633             sub updateEitSchedule {
634 2     2 1 4 my $self = shift;
635 2         4 my $rule = shift;
636 2         6 my $dbh = $self->{dbh};
637              
638 2         7 my $num_subtable = int( ( $rule->{maxsegments} - 1 ) / 32 );
639              
640             # always use this time in queries
641 2         4 my $current_time = time();
642              
643 2         6 my $last_midnight = int( $current_time / ( 24 * 60 * 60 ) ) * 24 * 60 * 60;
644              
645             # iterate over all subtables
646 2         4 my $subtable_count = 0;
647 2         7 while ( $subtable_count <= $num_subtable ) {
648              
649             # extend the $rule information
650 4 50       16 $rule->{table_id} =
651             ( $rule->{actual} == 1 ? 0x50 : 0x60 ) + $subtable_count;
652              
653 4         19 my $schedule = new DVB::EventInformationTable($rule);
654              
655             # lookup version_number used at last generation of eit and timestamp
656 4         35 my $select = $dbh->prepare(
657             "SELECT version_number, strftime('%s',timestamp) FROM eit_version
658             WHERE pid=$rule->{pid} AND table_id=$rule->{table_id} AND service_id=$rule->{service_id}"
659             );
660 4         593 $select->execute();
661 4         40 my ( $last_version_number, $last_update_timestamp ) =
662             $select->fetchrow_array();
663              
664             # if lookup wasn't succesfull we need to update the eit anyway
665 4 100       11 if ( !defined $last_version_number ) {
666 2         3 $last_update_timestamp = 0;
667 2         5 $last_version_number = 0;
668             }
669              
670             # first segment number in this subtable
671 4         7 my $first_segment = $subtable_count * 32;
672              
673             # start of subtable interval
674 4         9 my $subtable_start = $last_midnight + $first_segment * 3 * 60 * 60;
675              
676             # last segment in this subtable (actually it is the next of the last)
677 4 100       17 my $last_segment =
678             $rule->{maxsegments} >= $first_segment + 32
679             ? $first_segment + 32
680             : $rule->{maxsegments};
681              
682             # end of subtable interval and maxsegments
683 4         9 my $subtable_stop = $last_midnight + $last_segment * 3 * 60 * 60;
684              
685             # find last modification time of events in this subtable
686 4         38 $select = $dbh->prepare( "SELECT strftime('%s',timestamp) AS timestamp FROM event "
687             . "WHERE uid=$rule->{uid} "
688             . "AND start >= datetime( $subtable_start,'unixepoch') "
689             . "AND start < datetime( $subtable_stop,'unixepoch') "
690             . "ORDER BY timestamp DESC LIMIT 1" );
691 4         2100 $select->execute();
692 4   50     86 my ($last_event_modification) = $select->fetchrow_array() || 0;
693              
694             # has there any event stopped since last update
695             # if yes this event can be removed from schedule
696 4         53 my ($n) = $dbh->selectrow_array( "SELECT count(*) FROM event "
697             . "WHERE uid=$rule->{uid} "
698             . "AND stop > datetime( $last_update_timestamp,'unixepoch') "
699             . "AND stop < datetime( $current_time,'unixepoch')" );
700              
701             # skip this subtable if there is no need for updating
702 4 50 66     1596 next if $last_update_timestamp >= $last_midnight
      66        
703             and $last_event_modification <= $last_update_timestamp
704             and $n == 0;
705              
706             # iterate over each segment
707 2         6 my $segment_count = $first_segment;
708 2         7 while ( $segment_count < $last_segment ) {
709              
710             # segment start is in future
711 35 100       87 if ( $last_midnight + $segment_count * 3 * 60 * 60 >= $current_time ) {
    100          
712 27         264 $select = $dbh->prepare( "SELECT event_id, strftime('%s',start) AS start, "
713             . "strftime('%s',stop) AS stop, info, "
714             . "strftime('%s',timestamp) AS timestamp FROM event "
715             . "WHERE uid=$rule->{uid} "
716             . "AND start >= datetime( " . ( $last_midnight + $segment_count * 3 * 60 * 60 ) . ",'unixepoch') "
717             . "AND start < datetime( " . ( $last_midnight + ( $segment_count + 1 ) * 3 * 60 * 60 ) . ",'unixepoch') "
718             . "ORDER BY start" );
719 27         10684 $select->execute();
720              
721 27         50 my $event;
722 27         638 while ( $event = $select->fetchrow_hashref ) {
723 78         374 my $ue = _unfreezeEvent($event);
724 78         92 $ue->{running_status} = 1;
725 78         165 $schedule->add2Segment( $segment_count, $ue );
726             # TODO what if all sections are in use
727             }
728             }
729              
730             # segment stop is in past
731             elsif ( $last_midnight + ( $segment_count + 1 ) * 3 * 60 * 60 - 1 < $current_time ) {
732             # add empty segment
733 7         19 $schedule->add2Section( ( $segment_count % 32 ) * 8 );
734             }
735              
736             # segment start is in past but segment end is in future
737             else {
738 1         11 $select = $dbh->prepare( "SELECT event_id, strftime('%s',start) AS start, strftime('%s',stop) AS stop, "
739             . "info, strftime('%s',timestamp) AS timestamp FROM event "
740             . "WHERE uid=$rule->{uid} "
741             . "AND stop >= datetime( $current_time,'unixepoch') "
742             . "AND start < datetime( " . ( $last_midnight + ( $segment_count + 1 ) * 3 * 60 * 60 ) . ",'unixepoch') "
743             . "ORDER BY start");
744 1         352 $select->execute();
745              
746 1         2 my $event;
747 1         27 while ( $event = $select->fetchrow_hashref ) {
748 2         19 my $ue = _unfreezeEvent($event);
749 2 100       6 $ue->{running_status} = $event->{start} < $current_time ? 4 : 1;
750 2         6 $schedule->add2Segment( $segment_count, $ue );
751             # TODO what if all sections are in use
752             }
753             }
754 35         143 ++$segment_count;
755             }
756              
757             # Add subtable to playout and update version
758 2         5 ++$last_version_number;
759              
760             # Remove all section of this table
761 2 50       21 return if !$dbh->do( "DELETE FROM section "
762             . "WHERE pid=$rule->{pid} "
763             . "AND service_id=$rule->{service_id} "
764             . "AND table_id=$rule->{table_id}" );
765              
766 2         362 my $insert = $dbh->prepare( "INSERT INTO section VALUES ( $rule->{pid}, $rule->{table_id}, $rule->{service_id}, ?, ?)" );
767 2 50       108 return if !$insert;
768              
769 2         10 my $sections = $schedule->getSections($last_version_number);
770              
771 2         21 foreach my $section_number ( keys %$sections ) {
772 35         266 $insert->bind_param( 1, $section_number );
773 35         203 $insert->bind_param( 2, $sections->{$section_number}, SQL_BLOB );
774 35         16092 $insert->execute();
775             }
776              
777 2 50       58 return if !$dbh->do( "INSERT OR REPLACE INTO eit_version VALUES ( $rule->{pid}, $rule->{service_id}, $rule->{table_id}, $last_version_number, datetime( $current_time,'unixepoch'))");
778             }
779             continue {
780 4         1241 ++$subtable_count;
781             }
782 2         9 return 0;
783             }
784              
785             =head3 getEit( $pid, $timeFrame)
786              
787             Build final EIT from all sections in table for given $pid and $timeFrame.
788              
789             Return the complete TS chunk to be played within the timeframe.
790             Default timeframe should be 60s.
791             Return undef on error.
792              
793             =cut
794              
795             sub getEit {
796 3     3 1 8 my $self = shift;
797 3         5 my $pid = shift;
798 3         6 my $timeFrame = shift; # this is the time frame for which we are building the fragment of the TS
799 3         7 my $dbh = $self->{dbh};
800              
801 3 50       11 if ( !defined $pid) {
802 0         0 return;
803             }
804 3 50 33     23 if( !defined $timeFrame or $timeFrame < 10) {
805 0         0 return;
806             }
807              
808             # fetch all sections from database
809 3         24 my $sel = $dbh->prepare( "SELECT table_id, service_id, section_number, dump FROM section WHERE pid=$pid" );
810 3         529 $sel->execute();
811              
812 3         8 my ( $_table_id, $_service_id, $_section_number, $_dump );
813 3         28 $sel->bind_columns( \( $_table_id, $_service_id, $_section_number, $_dump ) );
814              
815 3         173 my %pfSections = ( present => { packetCount => 0, mts => ''},
816             following => { packetCount => 0, mts => ''});
817 3         29 my $pfFrequency = ceil($timeFrame / 1.7); # DON'T CHANGE THIS, IT IS THE BASIC CYCLE
818             # the repetition period must be at least 2s by
819            
820 3         6 my @otherSections;
821 3         6 my $allPacketCount = 0;
822              
823             # convert section into MPEG transport stream package and store in hash with
824             # some basic information for building up the final MTS
825             # the sections are grouped by present, following and other
826 3         33 while ( $sel->fetch ) {
827 41         47 my $section;
828 41         70 my $mts = _packetize( $pid, $_dump );
829 41         89 $section->{mts} = $mts;
830 41         85 $section->{size} = length($mts) / 188;
831 41         98 $section->{frequency} = $self->getSectionFrequency( $_table_id, $_section_number, $timeFrame );
832 41         60 $section->{table_id} = $_table_id;
833 41         50 $section->{service_id} = $_service_id;
834 41         53 $section->{section_number} = $_section_number;
835              
836             # p/f table have a higher repetition rate (every 2s) and therefore are grouped separate
837 41 100       61 if( $_table_id == 0x4e) {
838 6         8 $section->{frequency} = $pfFrequency;
839 6 100       13 if( $_section_number == 0) {
840 3         8 $pfSections{present}{packetCount} += $section->{size};
841 3         11 $pfSections{present}{mts} .= $section->{mts};
842             }
843             else {
844 3         9 $pfSections{following}{packetCount} += $section->{size};
845 3         10 $pfSections{following}{mts} .= $section->{mts};
846             }
847             }
848             else {
849 35         53 push( @otherSections, $section);
850             }
851 41         468 $allPacketCount += $section->{frequency} * $section->{size};
852             }
853              
854             # calculate available space for other sections than present following
855 3         12 my $nettoSpace = $allPacketCount - $pfFrequency * ( $pfSections{present}{packetCount} + $pfSections{following}{packetCount});
856             # we are going to put the sections as following
857             # PRESENT other FOLLOWING other PRESENT other FOLLOWING other ....
858             # therefore we have 2 x $pfFrequency gaps to fill up with other sections
859 3         8 my $interPfGap = $nettoSpace / (2*$pfFrequency);
860             # it is intentionally decimal number, if there are a small number of sections
861              
862             # based on nettoSpace we can calculate the
863             # specifical spacing between each repetition of a section
864 3         8 foreach my $section ( @otherSections) {
865 35         70 $section->{spacing} = int( $nettoSpace / $section->{frequency} + .5 ) - $section->{size} - 1;
866              
867             # this will be used to call down, when the next repetition should occur
868 35         55 $section->{nextApply} = 0;
869              
870             # has the section already been played
871 35         46 $section->{played} = 0;
872             }
873            
874             # printf( " all: %4i netto: %4i gap: %4i rest: %4i\n", $allPacketCount, $nettoSpace, $interPfGap, $nettoSpace-$pfFrequency*$interPfGap);
875              
876             # let's build the stream
877 3         7 my $pfCount = 2*$pfFrequency;
878 3         6 my $finalMts = '';
879 3         4 my $gapSpace = 0;
880 3         8 while ( $pfCount > 0 ) {
881              
882             # put alternating present and following mts in the stream
883 108 100       182 if( $pfCount % 2 == 0) {
884 54         88 $finalMts .= $pfSections{present}{mts};
885 54         55 $allPacketCount -= $pfSections{present}{packetCount};
886             }
887             else {
888 54         95 $finalMts .= $pfSections{following}{mts};
889 54         61 $allPacketCount -= $pfSections{following}{packetCount};
890             }
891              
892 108         101 $pfCount -= 1;
893              
894             # now fill up the gap with other section
895 108         88 $gapSpace += $interPfGap;
896              
897             # at last iteration we need to put all remaining packets in the stream
898 108 100       172 $gapSpace = $allPacketCount if $pfCount == 0;
899              
900 108         101 my $sectionCount = 0;
901              
902 108   66     334 while( $gapSpace > 0 && $allPacketCount > 0) {
903             # sort sections by number when it has to apply, frequency and size
904 830 50       1615 @otherSections = sort {
905 37         86 $a->{nextApply} <=> $b->{nextApply}
906             || $b->{frequency} <=> $a->{frequency}
907             # || int(rand(3))-1
908             } @otherSections;
909            
910 37         80 my $j = 0;
911            
912 37         40 $sectionCount += 1;
913 37         50 my $numInsertedPackets = $otherSections[$j]->{size};
914              
915 37         39 $gapSpace -= $numInsertedPackets;
916              
917             # add sections to output
918 37         122 $finalMts .= $otherSections[$j]->{mts};
919              
920 37         44 $otherSections[$j]->{frequency} -= 1;
921 37         42 $otherSections[$j]->{nextApply} = $otherSections[$j]->{spacing};
922 37         46 $otherSections[$j]->{played} = 1;
923              
924 37         33 $allPacketCount -= $numInsertedPackets;
925              
926             # printf( " j: %3i size: %2i gapspace: %3i pfcount: %2i all: %3i\n", $j, $otherSections[$j]->{size}, $gapSpace, $pfCount, $allPacketCount);
927              
928             # if all repetitions have been done, remove section from pool
929 37 100       74 if ( $otherSections[0]->{frequency} == 0 ) {
930 35         48 splice( @otherSections, 0, 1); # remove finished sections
931             }
932              
933 37         77 $j = 0;
934             # correct counters for all sections that have been already played
935 37         75 while ( $j <= $#otherSections ) {
936 651 100       1048 $otherSections[$j]->{nextApply} -= $numInsertedPackets if $otherSections[$j]->{played};
937 651         1151 $j += 1;
938             }
939              
940             }
941             }
942              
943             # correct continuity counter
944 3         5 my $continuity_counter = 0;
945 3         9 for ( my $j = 3 ; $j < length($finalMts) ; $j += 188 ) {
946 285         328 substr( $finalMts, $j, 1, chr( 0b00010000 | ( $continuity_counter & 0x0f ) ) );
947 285         472 $continuity_counter += 1;
948             }
949              
950 3         157 return $finalMts;
951             }
952              
953             =head3 getSectionFrequency( $table_id, $section_number, $timeFrame)
954              
955             Make lookup by $table_id and $section_number and return how often this section
956             has to be repeated in the given interval. Default interval ($timeFrame) is 60 seconds.
957              
958             =cut
959              
960             sub getSectionFrequency {
961 41     41 1 46 my $self = shift;
962 41         40 my $table_id = shift;
963 41         34 my $section_number = shift;
964 41         44 my $timeFrame = shift;
965 41 50       86 $timeFrame = 60 if !defined $timeFrame;
966              
967             # according to some scandinavian and australian specification we use following
968             # repetition rate:
969             # EITp/f actual - every <2s
970             # EITp/f other - every <10s
971             # EITsched actual 1 day - every 10s
972             # EITsched actual other days - every 30s
973             # EITsched other 1 day - every 30s
974             # EITsched other other days - every 30s
975             # THE FREQUENCY FOR PRESENT/FOLLOWING TABLE 0X4E IS DEFINED IN THE CALLING SUBROUTINE
976 41 50       78 return ceil($timeFrame / 8) if $table_id == 0x4f;
977 41 100 100     183 return ceil($timeFrame / 10) if ( $table_id == 0x50) and ( $section_number < (1 * 24 / 3 )); # days * 24 / 3
978 40         127 return ceil($timeFrame / 30);
979             }
980              
981             =head3 getLastError( )
982              
983             Return last db operation error.
984              
985             =cut
986              
987             sub getLastError {
988 0     0 1 0 my $self = shift;
989 0         0 my $dbh = $self->{dbh};
990              
991 0         0 return $dbh->errstr;
992             }
993              
994             =head3 _packetize( $pid, $section)
995              
996             Generate MPEG transport stream for defined $pid and $section in database.
997             Continuity counter starts at 0;
998             Return MTS.
999              
1000             =cut
1001              
1002             sub _packetize {
1003 41     41   54 my $pid = shift;
1004 41         69 my $data = "\x00" . shift; # add the pointer field at the beginning
1005 41         45 my $continuity_counter = 0;
1006 41         39 my $packet_payload_size = 188 - 4;
1007 41         55 my $data_len = length($data);
1008             # 'pointer_field' is only in the packet, carrying first byte of this section.
1009             # Therefore this packet has 'payload_unit_start_indicator' equal '1'.
1010             # All other packets don't have a 'pointer_filed' and therefore
1011             # 'payload_unit_start_indicator' is cleared
1012             #
1013 41         33 my $offs = 0;
1014 41         42 my $mts = "";
1015              
1016 41         107 while ( my $payload = substr( $data, $offs, $packet_payload_size ) ) {
1017              
1018             # Add stuffing byte to payload
1019 130         142 my $stuffing_bytes = $packet_payload_size - length($payload);
1020 130         236 while ( $stuffing_bytes-- ) { $payload .= "\xff"; }
  3973         5762  
1021              
1022             # Header + Payload:
1023 130 100       205 my $payload_unit_start_indicator = $offs == 0 ? 0b0100 << 12 : 0; # payload_unit_start_indicator
1024 130         296 my $packet = pack( "CnC",
1025             0x47,
1026             $pid | $payload_unit_start_indicator,
1027             0b00010000 | ( $continuity_counter & 0x0f ) ) . $payload;
1028 130         163 $mts .= $packet;
1029 130         113 $offs += $packet_payload_size;
1030 130         120 ++$continuity_counter;
1031 130 100       399 last if $offs > $data_len - 1;
1032             }
1033 41         106 return $mts;
1034             }
1035              
1036             =head3 _unfreezeEvent( $event)
1037              
1038             $event is a reference to hash containing elements of a row in event table.
1039             Thaw the info field and update all other keys from field values.
1040              
1041             Return reference to updated info hash.
1042              
1043             =cut
1044              
1045             sub _unfreezeEvent {
1046 85     85   110 my $row = shift;
1047              
1048 85 50       169 return if !$row;
1049              
1050 85         242 my $event = thaw( $row->{info} );
1051 85         1889 $event->{event_id} = $row->{event_id};
1052 85         133 $event->{start} = $row->{start};
1053 85         126 $event->{stop} = $row->{stop};
1054 85         195 $event->{duration} = $row->{stop} - $row->{start};
1055 85         131 return $event;
1056             }
1057              
1058             =head1 CLASS C
1059              
1060             =head2 METHODS
1061              
1062             =cut
1063              
1064             package DVB::EventInformationTable;
1065              
1066 1     1   10 use strict;
  1         1  
  1         36  
1067 1     1   5 use warnings;
  1         1  
  1         40  
1068 1     1   937 use Digest::CRC qw(crc);
  1         10578  
  1         161  
1069 1     1   27 use Carp;
  1         3  
  1         95  
1070 1     1   7 use Exporter;
  1         2  
  1         39  
1071 1     1   5 use vars qw(@ISA @EXPORT);
  1         2  
  1         2466  
1072              
1073             our @ISA = qw(Exporter);
1074             our @EXPORT = qw();
1075              
1076             =head3 new( $rule )
1077              
1078             EIT subtable initialization with information taken from $rule.
1079              
1080             =cut
1081              
1082             sub new {
1083 8     8   17 my $this = shift;
1084 8 50       22 my $rule = shift or return;
1085 8   33     36 my $class = ref($this) || $this;
1086 8         20 my $self = {};
1087              
1088 8         24 bless( $self, $class );
1089              
1090 8         39 $self->{table} = 'EIT';
1091 8         20 $self->{table_id} = $rule->{table_id};
1092 8         17 $self->{pid} = $rule->{pid};
1093 8         18 $self->{service_id} = $rule->{service_id};
1094 8         15 $self->{last_section_number} = undef;
1095 8         17 $self->{transport_stream_id} = $rule->{transport_stream_id};
1096 8         15 $self->{original_network_id} = $rule->{original_network_id};
1097 8         23 $self->{uid} = $rule->{uid};
1098 8         15 $self->{segment_last_section_number} = undef;
1099              
1100 8 100       24 if ( $rule->{maxsegments} == 0 ) {
1101              
1102             # there is just present/following
1103 2         6 $self->{last_table_id} = $self->{table_id};
1104             }
1105             else {
1106              
1107             # we have more subtables
1108 6         17 my $st = int( $rule->{maxsegments} / 32 );
1109 6 50       20 if ( $rule->{actual} == 1 ) {
1110 6         16 $self->{last_table_id} = 0x50 + $st;
1111             }
1112             else {
1113 0         0 $self->{last_table_id} = 0x60 + $st;
1114             }
1115             }
1116 8         20 $self->{sections} = [];
1117              
1118 8         19 return $self;
1119             }
1120              
1121             =head3 add2Segment( $segment_number, $event)
1122              
1123             Add $event to segment with number $segment_number.
1124             $event is reference to hash containin event data.
1125              
1126             Return 1 on success.
1127             Return undef on error.
1128              
1129             =cut
1130              
1131             sub add2Segment {
1132 80     80   82 my $self = shift;
1133 80         82 my $segment_number = shift;
1134 80         72 my $event = shift;
1135              
1136 80 50 33     255 if ( !defined $segment_number or !defined $event ) {
1137 0         0 return;
1138             }
1139              
1140 80         105 my $target_section = ( $segment_number % 32 ) * 8;
1141 80         85 my $largest_target_section = $target_section + 8;
1142 80         73 my $size;
1143              
1144 80   33     155 while ( ( ( $size = $self->add2Section( $target_section, $event ) ) == -1 ) and $target_section < $largest_target_section ) {
1145 0         0 ++$target_section;
1146             }
1147 80         1806 return $size;
1148             }
1149              
1150             =head3 add2Section ( $section_number, $event)
1151              
1152             Add $event to section with number $section_number.
1153             $event is reference to hash containin event data.
1154              
1155             Return binary $size of all events in section (always < 4078)
1156             or negativ if section is full, undef on error.
1157              
1158             =cut
1159              
1160             sub add2Section {
1161 93     93   89 my $self = shift;
1162 93         85 my $section_number = shift;
1163 93         80 my $event = shift;
1164              
1165 93 50       164 return if !defined $section_number;
1166              
1167 93   100     295 my $section_size = length( $self->{sections}[$section_number] // "" );
1168              
1169             # add empty event
1170 93 100       157 if ( !defined $event ) {
1171 8         16 $self->{sections}[$section_number] .= "";
1172 8         15 return $section_size;
1173             }
1174              
1175 85         92 my $alldescriptors = "";
1176              
1177             # iterate over event descriptors
1178 85         81 foreach ( @{ $event->{descriptors} } ) {
  85         188  
1179 83         125 $alldescriptors .= _getDescriptorBin($_);
1180             }
1181              
1182 85         114 my $descriptor_loop_length = length($alldescriptors);
1183              
1184             # build binary presentation
1185 85         190 my $struct = pack( 'na5a3na*',
1186             $event->{event_id},
1187             _epoch2mjd( $event->{start} ),
1188             _int2bcd( $event->{duration} ),
1189             ( ( ( ( $event->{running_status} & 0x07 ) << 1 ) + ( $event->{free_CA_mode} & 0x01 )) << 12) + $descriptor_loop_length,
1190             $alldescriptors
1191             );
1192              
1193 85         120 my $struct_size = length($struct);
1194              
1195             # add to section if enough space left
1196 85 50       153 if ( $section_size + $struct_size < 4078 ) {
1197 85         200 $self->{sections}[$section_number] .= $struct;
1198 85         341 return $section_size + $struct_size;
1199             }
1200             else {
1201              
1202 0         0 return -1;
1203             }
1204             }
1205              
1206             =head3 getSections ()
1207              
1208             Return reference to hash of sections with section_number as key and section as value.
1209              
1210             =cut
1211              
1212             sub getSections {
1213 5     5   10 my $self = shift;
1214 5   50     16 my $version_number = shift // 0;
1215 5         10 my $sections = {};
1216              
1217 5         6 my $last_section_number = $#{ $self->{sections} };
  5         22  
1218 5         14 my $num_segments = int( $last_section_number / 8 );
1219              
1220 5         7 my $current_segment = 0;
1221              
1222             # iterate over segments
1223 5         16 while ( $current_segment <= $num_segments ) {
1224              
1225             # find last used section in this segment
1226 38         59 my $i = 7;
1227 38   66     336 while ( $i >= 0 and !defined $self->{sections}[ $current_segment * 8 + $i ] ) {
1228 263         1015 --$i;
1229             }
1230 38         64 my $segment_last_section_number = $i + $current_segment * 8;
1231              
1232             # iterate over sections in this segment and add them to final hash
1233 38         50 my $current_section = $current_segment * 8;
1234 38         93 while ( $current_section <= $segment_last_section_number ) {
1235 41         86 my $section_length = length( $self->{sections}[$current_section] ) + 15;
1236 41         378 my $struct = pack( 'CnnCCCnnCCa*',
1237             $self->{table_id},
1238             ( (0x01) << 15 ) + 0x7000 + $section_length, # section_syntax_indicator is always 1
1239             $self->{service_id}, 0xc0 + ( $version_number & 0x1f << 1 ) + 0x01, # current_next indicator MUST be always 1
1240             $current_section,
1241             $last_section_number,
1242             $self->{transport_stream_id},
1243             $self->{original_network_id},
1244             $segment_last_section_number,
1245             $self->{last_table_id},
1246             $self->{sections}[$current_section]
1247             );
1248 41         180 my $crc = crc( $struct, 32, 0xffffffff, 0x00000000, 0, 0x04C11DB7, 0, 0);
1249              
1250             # add the binary to result
1251 41         284605 $sections->{$current_section} = $struct . pack( "N", $crc );
1252 41         215 ++$current_section;
1253             }
1254 38         136 ++$current_segment;
1255             }
1256 5         18 return $sections;
1257             }
1258              
1259             =head3 _getDescriptorBin ( $descriptor)
1260              
1261             Return binary representation of $descriptor.
1262              
1263             =cut
1264              
1265             sub _getDescriptorBin {
1266 83     83   82 my $descriptor = shift;
1267 83         82 my $struct;
1268              
1269 83 50       143 if ( $descriptor->{descriptor_tag} == 0x4d ) {
    0          
    0          
1270              
1271             # short_event_descriptor
1272 83         86 my $descriptor_tag = 0x4d;
1273 83         72 my $descriptor_length;
1274 83   50     215 my $language_code = _getByteString( $descriptor->{language_code} // 'slv');
1275 83         228 my $codepage_prefix = _getByteString( $descriptor->{codepage_prefix});
1276 83   50     238 my $raw_event_name = $descriptor->{event_name} // '';
1277 83   50     158 my $raw_text = $descriptor->{text} // '';
1278            
1279 83         83 my $codepage_prefix_length = length( $codepage_prefix );
1280              
1281 83         85 my $event_name = "";
1282 83 50       145 if ( $raw_event_name ne "") {
1283 83         124 $event_name = $codepage_prefix . substr( _getByteString($raw_event_name), 0, 255 - 5 - $codepage_prefix_length );
1284             }
1285 83         322 my $event_name_length = length( $event_name );
1286              
1287 83         79 my $text = "";
1288 83 50       142 if ( $raw_text ne "") {
1289 83         112 $text = $codepage_prefix . substr( _getByteString($raw_text), 0, 255 - 5 - $event_name_length - $codepage_prefix_length );
1290             }
1291 83         238 my $text_length = length( $text );
1292              
1293 83         88 $descriptor_length = $event_name_length + $text_length + 5;
1294 83         250 $struct = pack( "CCa3Ca*Ca*",
1295             $descriptor_tag, $descriptor_length, $language_code,
1296             $event_name_length, $event_name, $text_length, $text );
1297              
1298             }
1299             elsif ( $descriptor->{descriptor_tag} == 0x55 ) {
1300              
1301             # parental_rating_descriptor
1302 0         0 my $descriptor_tag = 0x55;
1303 0         0 my $descriptor_length;
1304              
1305 0         0 my $substruct = '';
1306 0         0 foreach ( @{ $descriptor->{list} } ) {
  0         0  
1307 0   0     0 my $country_code = _getByteString( $_->{country_code} // 'SVN');
1308 0   0     0 my $rating = $_->{rating} // 0;
1309 0         0 $substruct .= pack( "a3C", $country_code, $rating );
1310             }
1311 0         0 $descriptor_length = length($substruct);
1312 0         0 $struct = pack( "CCa*", $descriptor_tag, $descriptor_length, $substruct );
1313             }
1314             elsif ( $descriptor->{descriptor_tag} == 0x4e ) {
1315              
1316             # extended_event_descriptor
1317 0         0 $struct = _getExtendedEventDescriptorBin( $descriptor );
1318             }
1319             else {
1320 0         0 return "";
1321             }
1322              
1323 83         230 return $struct;
1324             }
1325              
1326             =head3 _getByteString ( $string)
1327              
1328             Convert $string containing only byte characters.
1329             This is for avoiding any problems with UTF8.
1330             Those string must be converted before entering data into database.
1331              
1332             Return converted string.
1333              
1334             =cut
1335              
1336             sub _getByteString {
1337 332     332   401 my $string = shift;
1338 332 100       696 return "" if ! $string;
1339 249         1841 return pack( "C*", unpack( "U*", $string ) );
1340             }
1341              
1342             =head3 _getExtendedEventDescriptorBin( $descriptor)
1343              
1344             Return 1 or many Extended Event Descriptors
1345              
1346             =cut
1347              
1348             sub _getExtendedEventDescriptorBin {
1349 0     0   0 my $descriptor = shift;
1350 0         0 my $struct = "";
1351              
1352             # skip if nothing to do
1353 0 0 0     0 return '' if !exists $descriptor->{text} || !defined $descriptor->{text} || $descriptor->{text} eq "";
      0        
1354              
1355 0         0 my $fulltext = _getByteString( $descriptor->{text} );
1356 0         0 my $full_text_length = length($fulltext);
1357              
1358             # the limit for this is 16 x 255 by numbers of extended event descriptors
1359             # also is a limit the max. section size 4096
1360             # let's say the max is 1024
1361 0 0       0 if ( $full_text_length > 1010 ) {
1362 0         0 my $firstPart = substr( $fulltext, 1010 ); # shorten text
1363 0         0 $fulltext = $firstPart;
1364 0         0 $full_text_length = length($fulltext);
1365             }
1366              
1367             # split up the text into multiple Extended Event Descriptors
1368 0         0 my $maxTextLength = 255 - 6;
1369 0         0 my $last_descriptor_number = int( $full_text_length / $maxTextLength );
1370              
1371 0         0 my $descriptor_tag = 0x4e;
1372 0   0     0 my $language_code = _getByteString( $descriptor->{language_code} // 'slv');
1373 0         0 my $codepage_prefix = _getByteString( $descriptor->{codepage_prefix});
1374 0         0 my $codepage_prefix_length = length($codepage_prefix);
1375 0         0 my $descriptor_length;
1376 0         0 my $length_of_items = 0;
1377 0         0 my $text;
1378             my $text_length;
1379 0         0 my $descriptor_number = 0;
1380              
1381 0         0 while ( $descriptor_number <= $last_descriptor_number ) {
1382 0         0 $text = $codepage_prefix . substr( $fulltext, 0, $maxTextLength - $codepage_prefix_length, '' );
1383 0         0 $text_length = length($text);
1384 0         0 $descriptor_length = $text_length + 6;
1385 0         0 $struct .= pack( "CCCa3CCa*",
1386             $descriptor_tag,
1387             $descriptor_length,
1388             $descriptor_number << 4 | $last_descriptor_number,
1389             $language_code,
1390             $length_of_items,
1391             $text_length,
1392             $text
1393             );
1394 0         0 ++$descriptor_number;
1395             }
1396 0         0 return $struct;
1397             }
1398              
1399             =head3 _int2bcd( $time)
1400              
1401             Convert integer $time in seconds into 24 bit time BCD format (hour:minute:seconds).
1402              
1403             =cut
1404              
1405             sub _int2bcd {
1406 170     170   235 my $time = shift;
1407 170         208 my $hour = int( $time / ( 60 * 60 ) );
1408 170         176 my $min = int( $time / 60 ) % 60;
1409 170         154 my $sec = $time % 60;
1410 170         419 my $struct = pack( 'CCC',
1411             int( $hour / 10 ) * 6 + $hour,
1412             int( $min / 10 ) * 6 + $min,
1413             int( $sec / 10 ) * 6 + $sec );
1414 170         546 return $struct;
1415             }
1416              
1417             =head3 _bcd2int( $bcd)
1418              
1419             Convert time in 24 bit BCD format (hour:minute:seconds) in seconds from midnight;
1420              
1421             =cut
1422              
1423             sub _bcd2int {
1424 0     0   0 my $bcd = shift;
1425 0         0 my ( $hour, $min, $sec ) = unpack( 'H2H2H2', $bcd );
1426 0         0 my $int = ( $hour * 60 + $min ) * 60 + $sec;
1427 0         0 return $int;
1428             }
1429              
1430             =head3 _epoch2mjd( $time)
1431              
1432             Convert epoch $time into 40 bit Modified Julian Date and time BCD format.
1433              
1434             =cut
1435              
1436             sub _epoch2mjd {
1437 85     85   92 my $time = shift;
1438 85         278 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = gmtime($time);
1439 85         96 ++$mon;
1440              
1441 85 50 33     316 my $l = $mon == 1 || $mon == 2 ? 1 : 0;
1442 85         198 my $MJD = 14956 + $mday + int( ( $year - $l ) * 365.25 ) + int( ( $mon + 1 + $l * 12 ) * 30.6001 );
1443 85         196 my $struct = pack( 'na*', $MJD, _int2bcd( $time % ( 60 * 60 * 24 ) ) );
1444 85         237 return $struct;
1445             }
1446              
1447             =head3 _mjd2epoch( $time)
1448              
1449             Convert 40 bit Modified Julian Date and time BCD format into epoch.
1450              
1451             =cut
1452              
1453             sub _mjd2epoch {
1454 0     0     my $combined = shift;
1455 0           my ( $mjd, $bcd ) = unpack( 'na3', $combined );
1456              
1457 0           my ( $y, $m );
1458 0           $y = int( ( $mjd - 15078.2 ) / 365.25 );
1459 0           $m = int( ( $mjd - 14956 - int( $y * 365.25 ) ) / 30.6001 );
1460 0 0 0       my $k = $m == 14 || $m == 15 ? 1 : 0;
1461 0           my $year = $y + $k;
1462 0           my $mon = $m - 1 - $k * 12 - 1;
1463 0           my $mday = $mjd - 14956 - int( $y * 365.25 ) - int( $m * 30.6001 );
1464 0           my $epoch = mktime( 0, 0, 1, $mday, $mon, $year, 0, 0, 0 ) + bcd2int($bcd);
1465 0           return $epoch;
1466             }
1467              
1468             =head1 AUTHOR
1469              
1470             Bojan Ramsak, C<< >>
1471              
1472             =head1 BUGS
1473              
1474             Please report any bugs or feature requests to C, or through
1475             the web interface at L. I will be notified, and then you'll
1476             automatically be notified of progress on your bug as I make changes.
1477              
1478             =head1 SUPPORT
1479              
1480             You can find documentation for this module with the perldoc command.
1481              
1482             perldoc DVB::Epg
1483              
1484             You can also look for information at:
1485              
1486             =head1 ACKNOWLEDGEMENTS
1487              
1488              
1489             =head1 LICENSE AND COPYRIGHT
1490              
1491             Copyright 2012 Bojan Ramsak.
1492              
1493             This program is free software; you can redistribute it and/or modify it
1494             under the terms of the Artistic License v2.0
1495              
1496             See http://www.opensource.org/licenses/Artistic-2.0 for more information.
1497              
1498             =cut
1499              
1500             1; # End of DVB::Epg