]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
Added EDL support over HTSP
authorkendrak24 <kendrak24@gmail.com>
Sun, 26 Jan 2014 17:05:00 +0000 (18:05 +0100)
committerkendrak24 <kendrak24@gmail.com>
Sun, 26 Jan 2014 17:05:00 +0000 (18:05 +0100)
Makefile
src/dvr/dvr.h
src/dvr/dvr_cutpoints.c [new file with mode: 0644]
src/htsp_server.c

index bfe7abd1e91f8cf6e5abdfeff75c3915ba19b4ab..89de7bc1c9b039923440f0769b5330bf0e1ca31c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -145,6 +145,7 @@ SRCS += src/plumbing/tsfix.c \
 SRCS += src/dvr/dvr_db.c \
        src/dvr/dvr_rec.c \
        src/dvr/dvr_autorec.c \
+       src/dvr/dvr_cutpoints.c \
 
 SRCS += src/webui/webui.c \
        src/webui/comet.c \
index df043689d8bdf7e33daa3ad1c5a821ba05ab62e4..0253f42bd6bbe3890f33227f9a584294bdc2ad89 100644 (file)
@@ -402,4 +402,44 @@ void dvr_inotify_init ( void );
 void dvr_inotify_add  ( dvr_entry_t *de );
 void dvr_inotify_del  ( dvr_entry_t *de );
 
+/**
+ * Cutpoints support
+ **/
+
+/**
+ * This is the max number of lines that will be read 
+ * from a cutpoint file (e.g. EDL or Comskip). 
+ * This is a safety against large files containing non-cutpoint/garbage data.
+ **/
+#define DVR_MAX_READ_CUTFILE_LINES 10000
+/**
+ * This is the max number of entries that will be used
+ * from a cutpoint file (e.g. EDL or Comskip). 
+ * This is a safety against using up resources due to
+ * potentially large files containing weird data.
+ **/
+#define DVR_MAX_CUT_ENTRIES 5000
+/**
+ * Max line length allowed in a cutpoints file. Excess will be ignored.
+ **/
+#define DVR_MAX_CUTPOINT_LINE 128
+
+
+typedef struct dvr_cutpoint {
+  TAILQ_ENTRY(dvr_cutpoint) dc_link;
+  uint64_t dc_start_ms;
+  uint64_t dc_end_ms;
+  enum {
+    DVR_CP_CUT,
+    DVR_CP_MUTE,
+    DVR_CP_SCENE,
+    DVR_CP_COMM
+  } dc_type;
+} dvr_cutpoint_t;
+
+typedef TAILQ_HEAD(,dvr_cutpoint) dvr_cutpoint_list_t;
+
+dvr_cutpoint_list_t *dvr_get_cutpoint_list (uint32_t id);
+void dvr_cutpoint_list_destroy (dvr_cutpoint_list_t *list);
+
 #endif /* DVR_H  */
diff --git a/src/dvr/dvr_cutpoints.c b/src/dvr/dvr_cutpoints.c
new file mode 100644 (file)
index 0000000..a2a391a
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <string.h>
+
+#include "tvheadend.h"
+#include "dvr.h"
+
+/**
+ * Replaces the extension of a filename with a different extension.
+ * filename, in: full path to file.
+ * new_ext, in: new extension including leading '.'., e.g. ".edl"
+ * new_filename, in: pre-allocated char*, out: filename with the new extension.
+ * Return 1 on success, otherwise 0.
+ **/
+static int 
+dvr_switch_file_extension(const char *filename, const char *new_ext, char *new_filename)
+{
+  char *ext = strrchr(filename, '.');
+
+  // No '.' found. Probably not a good path/file then...
+  if(ext == NULL) {
+    return 0;
+  }
+
+  int len = (ext - filename);
+  if(len <= 0) {
+    return 0;
+  }
+
+  // Allocate length for stripped filename + new_ext + "\0" 
+  int ext_len = strlen(new_ext);
+
+  // Build the new filename.
+  memcpy(new_filename, filename, len);
+  memcpy(&new_filename[len], new_ext, ext_len);
+  new_filename[len + ext_len] = 0;
+
+  return 1;
+}
+
+/**
+ * Parse EDL data.
+ *   filename, in: full path to EDL file.
+ *   cut_list, in: empty list. out: the list filled with data.
+ *                 Don't forget to call dvr_cutpoint_list_destroy for the cut_list when done.
+ *   return:   number of read valid lines.
+ *
+ * Example of EDL file content:
+ *
+ * 2.00    98.36   3
+ * 596.92  665.92  3
+ * 1426.68 2160.16 3
+ *
+ **/ 
+static int 
+dvr_parse_edl(const char *filename, dvr_cutpoint_list_t *cut_list)
+{
+  char line[DVR_MAX_CUTPOINT_LINE];
+  int line_count = 0, valid_lines = 0, action = 0;
+  float start = 0.0f, end = 0.0f;
+  dvr_cutpoint_t *cutpoint;
+
+  FILE *file = fopen(filename, "r");
+
+  // No file found. Which is perfectly ok.
+  if (file == NULL)
+    return -1;
+
+  while(line_count < DVR_MAX_READ_CUTFILE_LINES) {
+    if(fgets(line, DVR_MAX_CUTPOINT_LINE, file) == NULL)
+      break;      
+    line_count++;
+    if (sscanf(line, "%f\t%f\t%d", &start, &end, &action) == 3) {
+      // Sanity checks...
+      if(start < 0 || end < 0 || end < start || start == end ||
+         action < DVR_CP_CUT || action > DVR_CP_COMM) {
+        tvhwarn("DVR", 
+               "Insane entry: start=%f, end=%f. Skipping.", start, end);
+        continue;
+      }
+
+      cutpoint = calloc(1, sizeof(dvr_cutpoint_t));
+      if(cutpoint == NULL) {
+       fclose(file);
+        return 0;
+      }
+
+      cutpoint->dc_start_ms = (int) (start * 1000.0f);
+      cutpoint->dc_end_ms   = (int) (end * 1000.0f);
+      cutpoint->dc_type     = action;
+
+      TAILQ_INSERT_TAIL(cut_list, cutpoint, dc_link);
+        
+      valid_lines++;
+
+      if(valid_lines >= DVR_MAX_CUT_ENTRIES)
+       break;
+    }
+  }
+  fclose(file);
+
+  return valid_lines;
+}
+
+
+/**
+ * Parse comskip data.
+ *   filename, in: full path to comskip file.
+ *   cut_list, in: empty list. out: the list filled with data.
+ *                 Don't forget to call dvr_cutpoint_list_destroy for the cut_list when done.
+ *   return:   number of read valid lines.
+ *
+ * Example of comskip file content (format v2):
+ *
+ * FILE PROCESSING COMPLETE  53999 FRAMES AT  2500
+ * -------------------
+ * 50      2459
+ * 14923   23398
+ * 42417   54004
+ *
+ **/ 
+static int 
+dvr_parse_comskip(const char *filename, dvr_cutpoint_list_t *cut_list)
+{
+  char line[DVR_MAX_CUTPOINT_LINE];
+  float frame_rate = 0.0f;
+  int line_count = 0, valid_lines = 0, start = 0, end = 0;
+  dvr_cutpoint_t *cutpoint;
+
+  FILE *file = fopen(filename, "r");
+
+  // No file found. Which is perfectly ok.
+  if (file == NULL) 
+    return -1;
+
+  while(line_count < DVR_MAX_READ_CUTFILE_LINES) {
+    if(fgets(line, DVR_MAX_CUTPOINT_LINE, file) == NULL)
+      break;
+    line_count++;
+    if (sscanf(line, "FILE PROCESSING COMPLETE %*d FRAMES AT %f", &frame_rate) == 1)
+      continue;
+    if(frame_rate > 0.0f && sscanf(line, "%d\t%d", &start, &end) == 2) {
+      // Sanity checks...
+      if(start < 0 || end < 0 || end < start || start == end) {
+        tvherror("DVR", 
+               "Insane EDL entry: start=%d, end=%d. Skipping.", start, end);
+        continue;
+      }
+
+      // Support frame rate stated as both 25 and 2500
+      frame_rate /= (frame_rate > 1000.0f ? 100.0f : 1.0f);
+
+      cutpoint = calloc(1, sizeof(dvr_cutpoint_t));
+      if(cutpoint == NULL) {
+       fclose(file);
+        return 0;
+      }
+
+      // Convert frame numbers to timestamps (in ms)
+      cutpoint->dc_start_ms = (int) ((start * 1000) / frame_rate);
+      cutpoint->dc_end_ms   = (int) ((end * 1000) / frame_rate);
+      // Comskip don't have different actions, so use DVR_CP_COMM (Commercial skip)
+      cutpoint->dc_type     = DVR_CP_COMM;
+
+      TAILQ_INSERT_TAIL(cut_list, cutpoint, dc_link);
+        
+      valid_lines++;
+
+      if(valid_lines >= DVR_MAX_CUT_ENTRIES)
+       break;
+    }
+  }
+  fclose(file);
+
+  return valid_lines;
+}
+
+
+/**
+ * Return cutpoint data for a recording (if present). 
+ **/
+dvr_cutpoint_list_t *
+dvr_get_cutpoint_list (uint32_t dvr_entry_id)
+{
+  dvr_entry_t *de;
+
+  if ((de = dvr_entry_find_by_id(dvr_entry_id)) == NULL) 
+    return NULL;
+  if (de->de_filename == NULL)
+    return NULL;
+
+  char *dc_filename = alloca(strlen(de->de_filename) + 4);
+  if(dc_filename == NULL) 
+    return NULL;
+
+  // First we try with comskip file. (.txt)
+  if(!dvr_switch_file_extension(de->de_filename, ".txt", dc_filename))
+    return NULL;
+
+  dvr_cutpoint_list_t *cuts = calloc(1, sizeof(dvr_cutpoint_list_t)); 
+  if (cuts == NULL) 
+    return NULL;
+  TAILQ_INIT(cuts);
+
+  if(dvr_parse_comskip(dc_filename, cuts) == -1) {
+    // Then try with edl file. (.edl)
+    if(!dvr_switch_file_extension(de->de_filename, ".edl", dc_filename)) {
+      dvr_cutpoint_list_destroy(cuts);
+      return NULL;
+    }
+    if(dvr_parse_edl(dc_filename, cuts) == -1) {
+      // No cutpoint file found
+      dvr_cutpoint_list_destroy(cuts);
+      return NULL;
+    }
+  }
+
+  return cuts;
+}
+
+/***************************
+ * Helpers
+ ***************************/
+
+void 
+dvr_cutpoint_list_destroy (dvr_cutpoint_list_t *list)
+{
+  if(!list) return;
+  dvr_cutpoint_t *cp;
+  while ((cp = TAILQ_FIRST(list))) {
+    TAILQ_REMOVE(list, cp, dc_link);
+    free(cp);
+  }
+  free(list);
+}
index 990194f6bde312491022938da685790e7e465bba..5408ec36b37d3c892b057ee3dc28c26c70054325 100644 (file)
@@ -1272,6 +1272,54 @@ htsp_method_deleteDvrEntry(htsp_connection_t *htsp, htsmsg_t *in)
   return out;
 }
 
+/**
+ * Return cutpoint data for a recording (if present). 
+ *
+ * Request message fields:
+ * id                 u32    required   DVR entry id
+ *
+ * Result message fields:
+ * cutpoints          msg[]  optional   List of cutpoint entries, if a file is 
+ *                                      found and has some valid data.
+ *
+ * Cutpoint fields:
+ * start              u32    required   Cut start time in ms.
+ * end                u32    required   Cut end time in ms.
+ * type               u32    required   Action type: 
+ *                                      0=Cut, 1=Mute, 2=Scene, 
+ *                                      3=Commercial break.
+ **/
+static htsmsg_t *
+htsp_method_getDvrCutpoints(htsp_connection_t *htsp, htsmsg_t *in)
+{
+  uint32_t dvrEntryId;
+  if (htsmsg_get_u32(in, "id", &dvrEntryId))
+    return htsp_error("Missing argument 'id'");
+
+  htsmsg_t *msg = htsmsg_create_map();
+
+  dvr_cutpoint_list_t *list = dvr_get_cutpoint_list(dvrEntryId); 
+
+  if (list != NULL) {
+    htsmsg_t *cutpoint_list = htsmsg_create_list();
+    dvr_cutpoint_t *cp;
+    TAILQ_FOREACH(cp, list, dc_link) {
+      htsmsg_t *cutpoint = htsmsg_create_map();
+      htsmsg_add_u32(cutpoint, "start", cp->dc_start_ms);
+      htsmsg_add_u32(cutpoint, "end", cp->dc_end_ms);
+      htsmsg_add_u32(cutpoint, "type", cp->dc_type);
+
+      htsmsg_add_msg(cutpoint_list, NULL, cutpoint);
+    }
+    htsmsg_add_msg(msg, "cutpoints", cutpoint_list);
+  }
+  
+  // Cleanup...
+  dvr_cutpoint_list_destroy(list);
+
+  return msg;
+}
+
 /**
  * Request a ticket for a http url pointing to a channel or dvr
  */
@@ -1794,36 +1842,37 @@ struct {
   htsmsg_t *(*fn)(htsp_connection_t *htsp, htsmsg_t *in);
   int privmask;
 } htsp_methods[] = {
-  { "hello",                    htsp_method_hello,          ACCESS_ANONYMOUS},
-  { "authenticate",             htsp_method_authenticate,   ACCESS_ANONYMOUS},
-  { "getDiskSpace",             htsp_method_getDiskSpace,   ACCESS_STREAMING},
-  { "getSysTime",               htsp_method_getSysTime,     ACCESS_STREAMING},
-  { "enableAsyncMetadata",      htsp_method_async,          ACCESS_STREAMING},
-  { "getEvent",                 htsp_method_getEvent,       ACCESS_STREAMING},
-  { "getEvents",                htsp_method_getEvents,      ACCESS_STREAMING},
-  { "epgQuery",                 htsp_method_epgQuery,       ACCESS_STREAMING},
-  { "getEpgObject",             htsp_method_getEpgObject,   ACCESS_STREAMING},
-  { "addDvrEntry",              htsp_method_addDvrEntry,    ACCESS_RECORDER},
-  { "updateDvrEntry",           htsp_method_updateDvrEntry, ACCESS_RECORDER},
-  { "cancelDvrEntry",           htsp_method_cancelDvrEntry, ACCESS_RECORDER},
-  { "deleteDvrEntry",           htsp_method_deleteDvrEntry, ACCESS_RECORDER},
-  { "getTicket",                htsp_method_getTicket,      ACCESS_STREAMING},
-  { "subscribe",                htsp_method_subscribe,      ACCESS_STREAMING},
-  { "unsubscribe",              htsp_method_unsubscribe,    ACCESS_STREAMING},
-  { "subscriptionChangeWeight", htsp_method_change_weight,  ACCESS_STREAMING},
-  { "subscriptionSeek",         htsp_method_skip,           ACCESS_STREAMING},
-  { "subscriptionSkip",         htsp_method_skip,           ACCESS_STREAMING},
-  { "subscriptionSpeed",        htsp_method_speed,          ACCESS_STREAMING},
-  { "subscriptionLive",         htsp_method_live,           ACCESS_STREAMING},
-  { "subscriptionFilterStream", htsp_method_filter_stream,  ACCESS_STREAMING},
+  { "hello",                    htsp_method_hello,           ACCESS_ANONYMOUS},
+  { "authenticate",             htsp_method_authenticate,    ACCESS_ANONYMOUS},
+  { "getDiskSpace",             htsp_method_getDiskSpace,    ACCESS_STREAMING},
+  { "getSysTime",               htsp_method_getSysTime,      ACCESS_STREAMING},
+  { "enableAsyncMetadata",      htsp_method_async,           ACCESS_STREAMING},
+  { "getEvent",                 htsp_method_getEvent,        ACCESS_STREAMING},
+  { "getEvents",                htsp_method_getEvents,       ACCESS_STREAMING},
+  { "epgQuery",                 htsp_method_epgQuery,        ACCESS_STREAMING},
+  { "getEpgObject",             htsp_method_getEpgObject,    ACCESS_STREAMING},
+  { "addDvrEntry",              htsp_method_addDvrEntry,     ACCESS_RECORDER},
+  { "updateDvrEntry",           htsp_method_updateDvrEntry,  ACCESS_RECORDER},
+  { "cancelDvrEntry",           htsp_method_cancelDvrEntry,  ACCESS_RECORDER},
+  { "deleteDvrEntry",           htsp_method_deleteDvrEntry,  ACCESS_RECORDER},
+  { "getDvrCutpoints",          htsp_method_getDvrCutpoints, ACCESS_RECORDER},
+  { "getTicket",                htsp_method_getTicket,       ACCESS_STREAMING},
+  { "subscribe",                htsp_method_subscribe,       ACCESS_STREAMING},
+  { "unsubscribe",              htsp_method_unsubscribe,     ACCESS_STREAMING},
+  { "subscriptionChangeWeight", htsp_method_change_weight,   ACCESS_STREAMING},
+  { "subscriptionSeek",         htsp_method_skip,            ACCESS_STREAMING},
+  { "subscriptionSkip",         htsp_method_skip,            ACCESS_STREAMING},
+  { "subscriptionSpeed",        htsp_method_speed,           ACCESS_STREAMING},
+  { "subscriptionLive",         htsp_method_live,            ACCESS_STREAMING},
+  { "subscriptionFilterStream", htsp_method_filter_stream,   ACCESS_STREAMING},
 #if ENABLE_LIBAV
-  { "getCodecs",                htsp_method_getCodecs,      ACCESS_STREAMING},
+  { "getCodecs",                htsp_method_getCodecs,       ACCESS_STREAMING},
 #endif
-  { "fileOpen",                 htsp_method_file_open,      ACCESS_RECORDER},
-  { "fileRead",                 htsp_method_file_read,      ACCESS_RECORDER},
-  { "fileClose",                htsp_method_file_close,     ACCESS_RECORDER},
-  { "fileStat",                 htsp_method_file_stat,      ACCESS_RECORDER},
-  { "fileSeek",                 htsp_method_file_seek,      ACCESS_RECORDER},
+  { "fileOpen",                 htsp_method_file_open,       ACCESS_RECORDER},
+  { "fileRead",                 htsp_method_file_read,       ACCESS_RECORDER},
+  { "fileClose",                htsp_method_file_close,      ACCESS_RECORDER},
+  { "fileStat",                 htsp_method_file_stat,       ACCESS_RECORDER},
+  { "fileSeek",                 htsp_method_file_seek,       ACCESS_RECORDER},
 };
 
 #define NUM_METHODS (sizeof(htsp_methods) / sizeof(htsp_methods[0]))