Added EDL support over HTSP
This commit is contained in:
parent
b10cfcf406
commit
d9bc3b6d41
4 changed files with 365 additions and 28 deletions
1
Makefile
1
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 \
|
||||
|
|
|
@ -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 */
|
||||
|
|
247
src/dvr/dvr_cutpoints.c
Normal file
247
src/dvr/dvr_cutpoints.c
Normal file
|
@ -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);
|
||||
}
|
|
@ -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]))
|
||||
|
|
Loading…
Add table
Reference in a new issue