diff --git a/Makefile b/Makefile index bfe7abd1..89de7bc1 100644 --- 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 \ diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index df043689..0253f42b 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -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 index 00000000..a2a391a6 --- /dev/null +++ b/src/dvr/dvr_cutpoints.c @@ -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 . + */ +#include + +#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); +} diff --git a/src/htsp_server.c b/src/htsp_server.c index 990194f6..5408ec36 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -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]))