889 lines
19 KiB
C
889 lines
19 KiB
C
/*
|
|
* Private Video Recorder
|
|
* Copyright (C) 2007 Andreas Öman
|
|
*
|
|
* 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 <pthread.h>
|
|
#include <assert.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/ioctl.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <dirent.h>
|
|
|
|
#include <libavutil/avstring.h>
|
|
|
|
#include <libhts/htscfg.h>
|
|
|
|
#include "tvhead.h"
|
|
#include "channels.h"
|
|
#include "subscriptions.h"
|
|
#include "pvr.h"
|
|
#include "epg.h"
|
|
#include "dispatch.h"
|
|
#include "buffer.h"
|
|
#include "ffmuxer.h"
|
|
#include "spawn.h"
|
|
|
|
static int pvr_id_ceil; /* number generator for database entries */
|
|
|
|
struct pvr_rec_list pvrr_global_list;
|
|
|
|
static void pvr_database_save(pvr_rec_t *pvrr);
|
|
static void pvr_database_erase(pvr_rec_t *pvrr);
|
|
static void pvr_database_load(void);
|
|
static void pvr_fsm(pvr_rec_t *pvrr);
|
|
static void pvr_subscription_callback(struct th_subscription *s,
|
|
subscription_event_t event,
|
|
void *opaque);
|
|
|
|
static void *pvr_recorder_thread(void *aux);
|
|
|
|
static void postrec(pvr_rec_t *pvrr);
|
|
|
|
/**
|
|
* Initialize PVR framework
|
|
*/
|
|
void
|
|
pvr_init(void)
|
|
{
|
|
pvr_database_load();
|
|
}
|
|
|
|
|
|
/**
|
|
* For the given event, return pvr recording entry (if we have a pvr
|
|
* recording entry that matches the event)
|
|
*/
|
|
pvr_rec_t *
|
|
pvr_get_by_entry(event_t *e)
|
|
{
|
|
pvr_rec_t *pvrr;
|
|
channel_t *ch = e->e_channel;
|
|
|
|
LIST_FOREACH(pvrr, &ch->ch_pvrrs, pvrr_channel_link) {
|
|
if(pvrr->pvrr_start >= e->e_start &&
|
|
pvrr->pvrr_stop <= e->e_start + e->e_duration) {
|
|
return pvrr;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find the pvr record entry based on increasing index
|
|
*/
|
|
pvr_rec_t *
|
|
pvr_get_log_entry(int e)
|
|
{
|
|
pvr_rec_t *pvrr = LIST_FIRST(&pvrr_global_list);
|
|
|
|
while(pvrr) {
|
|
if(e == 0)
|
|
return pvrr;
|
|
e--;
|
|
pvrr = LIST_NEXT(pvrr, pvrr_global_link);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Find the pvr record entry based on reference tag
|
|
*/
|
|
pvr_rec_t *
|
|
pvr_get_tag_entry(int e)
|
|
{
|
|
pvr_rec_t *pvrr;
|
|
|
|
LIST_FOREACH(pvrr, &pvrr_global_list, pvrr_global_link)
|
|
if(pvrr->pvrr_ref == e)
|
|
return pvrr;
|
|
return NULL;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Inform clients about PVR entry status update
|
|
*/
|
|
void
|
|
pvr_inform_status_change(pvr_rec_t *pvrr)
|
|
{
|
|
event_t *e;
|
|
|
|
|
|
e = epg_event_find_by_time(pvrr->pvrr_channel, pvrr->pvrr_start);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Free a pvr entry
|
|
*/
|
|
static void
|
|
pvr_free(pvr_rec_t *pvrr)
|
|
{
|
|
dtimer_disarm(&pvrr->pvrr_timer);
|
|
LIST_REMOVE(pvrr, pvrr_global_link);
|
|
LIST_REMOVE(pvrr, pvrr_channel_link);
|
|
free(pvrr->pvrr_title);
|
|
free(pvrr->pvrr_desc);
|
|
free(pvrr->pvrr_creator);
|
|
free(pvrr->pvrr_printname);
|
|
free(pvrr->pvrr_filename);
|
|
free(pvrr);
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Abort a current recording
|
|
*/
|
|
int
|
|
pvr_abort(pvr_rec_t *pvrr)
|
|
{
|
|
if(pvrr->pvrr_status != HTSTV_PVR_STATUS_RECORDING)
|
|
return -1;
|
|
|
|
pvrr->pvrr_error = HTSTV_PVR_STATUS_ABORTED;
|
|
pvr_fsm(pvrr);
|
|
|
|
pvr_database_save(pvrr);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Clear current entry (only works if we are not recording)
|
|
*/
|
|
int
|
|
pvr_clear(pvr_rec_t *pvrr)
|
|
{
|
|
if(pvrr->pvrr_status == HTSTV_PVR_STATUS_RECORDING)
|
|
return -1;
|
|
|
|
pvr_database_erase(pvrr);
|
|
pvr_free(pvrr);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Destroy all PVRs based on the given channel
|
|
*/
|
|
void
|
|
pvr_destroy_by_channel(channel_t *ch)
|
|
{
|
|
pvr_rec_t *pvrr;
|
|
|
|
while((pvrr = LIST_FIRST(&ch->ch_pvrrs)) != NULL) {
|
|
if(pvrr->pvrr_status == HTSTV_PVR_STATUS_RECORDING)
|
|
pvr_abort(pvrr);
|
|
|
|
pvr_clear(pvrr);
|
|
pvr_free(pvrr);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Insert a pvr entry skeleton into the list and start FSM
|
|
*/
|
|
static void
|
|
pvr_link_pvrr(pvr_rec_t *pvrr)
|
|
{
|
|
pvrr->pvrr_ref = tag_get();
|
|
|
|
pvrr->pvrr_printname = strdup(pvrr->pvrr_title ?: "");
|
|
|
|
LIST_INSERT_HEAD(&pvrr_global_list, pvrr, pvrr_global_link);
|
|
|
|
switch(pvrr->pvrr_status) {
|
|
case HTSTV_PVR_STATUS_FILE_ERROR:
|
|
case HTSTV_PVR_STATUS_DISK_FULL:
|
|
case HTSTV_PVR_STATUS_ABORTED:
|
|
case HTSTV_PVR_STATUS_BUFFER_ERROR:
|
|
case HTSTV_PVR_STATUS_NONE:
|
|
break;
|
|
|
|
case HTSTV_PVR_STATUS_SCHEDULED:
|
|
case HTSTV_PVR_STATUS_RECORDING:
|
|
pvrr->pvrr_status = HTSTV_PVR_STATUS_SCHEDULED;
|
|
pvr_fsm(pvrr);
|
|
break;
|
|
}
|
|
|
|
pvr_inform_status_change(pvrr);
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove log info about all completed recordings
|
|
*/
|
|
void
|
|
pvr_clear_all_completed(void)
|
|
{
|
|
pvr_rec_t *pvrr, *next;
|
|
for(pvrr = LIST_FIRST(&pvrr_global_list); pvrr != NULL; pvrr = next) {
|
|
next = LIST_NEXT(pvrr, pvrr_global_link);
|
|
|
|
switch(pvrr->pvrr_status) {
|
|
case HTSTV_PVR_STATUS_SCHEDULED:
|
|
case HTSTV_PVR_STATUS_RECORDING:
|
|
break;
|
|
default:
|
|
pvr_database_erase(pvrr);
|
|
pvr_free(pvrr);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Create a PVR entry based on a given event
|
|
*/
|
|
pvr_rec_t *
|
|
pvr_schedule_by_event(event_t *e, const char *creator)
|
|
{
|
|
channel_t *ch = e->e_channel;
|
|
time_t start = e->e_start;
|
|
time_t stop = e->e_start + e->e_duration;
|
|
time_t now;
|
|
pvr_rec_t *pvrr;
|
|
|
|
time(&now);
|
|
|
|
if(stop < now)
|
|
return NULL;
|
|
|
|
/* Try to see if we already have a scheduled or active recording */
|
|
|
|
LIST_FOREACH(pvrr, &ch->ch_pvrrs, pvrr_channel_link)
|
|
if(pvrr->pvrr_start == start && pvrr->pvrr_stop == stop)
|
|
break;
|
|
|
|
if(pvrr != NULL)
|
|
return NULL; /* Already exists */
|
|
|
|
pvrr = calloc(1, sizeof(pvr_rec_t));
|
|
pvrr->pvrr_status = HTSTV_PVR_STATUS_SCHEDULED;
|
|
pvrr->pvrr_channel = ch;
|
|
LIST_INSERT_HEAD(&ch->ch_pvrrs, pvrr, pvrr_channel_link);
|
|
|
|
pvrr->pvrr_start = start;
|
|
pvrr->pvrr_stop = stop;
|
|
pvrr->pvrr_title = e->e_title ? strdup(e->e_title) : NULL;
|
|
pvrr->pvrr_desc = e->e_desc ? strdup(e->e_desc) : NULL;
|
|
pvrr->pvrr_creator = strdup(creator);
|
|
pvr_link_pvrr(pvrr);
|
|
pvr_database_save(pvrr);
|
|
return pvrr;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Record based on a channel
|
|
*/
|
|
pvr_rec_t *
|
|
pvr_schedule_by_channel_and_time(channel_t *ch, int duration,
|
|
const char *creator)
|
|
{
|
|
time_t now = dispatch_clock;
|
|
time_t start = now;
|
|
time_t stop = now + duration;
|
|
pvr_rec_t *pvrr;
|
|
|
|
pvrr = calloc(1, sizeof(pvr_rec_t));
|
|
pvrr->pvrr_status = HTSTV_PVR_STATUS_SCHEDULED;
|
|
pvrr->pvrr_channel = ch;
|
|
LIST_INSERT_HEAD(&ch->ch_pvrrs, pvrr, pvrr_channel_link);
|
|
|
|
pvrr->pvrr_start = start;
|
|
pvrr->pvrr_stop = stop;
|
|
pvrr->pvrr_title = strdup("Manual recording");
|
|
pvrr->pvrr_desc = NULL;
|
|
pvrr->pvrr_creator = strdup(creator);
|
|
|
|
pvr_link_pvrr(pvrr);
|
|
pvr_database_save(pvrr);
|
|
return pvrr;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* The recording database is real simple.
|
|
*
|
|
* We just store meta information about each recording in a separate
|
|
* textfile stored in a directory
|
|
*
|
|
*/
|
|
|
|
static void
|
|
pvr_database_save(pvr_rec_t *pvrr)
|
|
{
|
|
char buf[400];
|
|
FILE *fp;
|
|
|
|
if(pvrr->pvrr_id == 0)
|
|
pvrr->pvrr_id = ++pvr_id_ceil;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/recordings/%d", settings_dir, pvrr->pvrr_id);
|
|
|
|
if((fp = settings_open_for_write(buf)) == NULL)
|
|
return;
|
|
|
|
fprintf(fp, "channel = %s\n", pvrr->pvrr_channel->ch_name);
|
|
fprintf(fp, "start = %ld\n", pvrr->pvrr_start);
|
|
fprintf(fp, "stop = %ld\n", pvrr->pvrr_stop);
|
|
|
|
if(pvrr->pvrr_title != NULL)
|
|
fprintf(fp, "title = %s\n", pvrr->pvrr_title);
|
|
|
|
if(pvrr->pvrr_desc != NULL)
|
|
fprintf(fp, "description = %s\n", pvrr->pvrr_desc);
|
|
|
|
if(pvrr->pvrr_creator != NULL)
|
|
fprintf(fp, "creator = %s\n", pvrr->pvrr_creator);
|
|
|
|
if(pvrr->pvrr_filename != NULL)
|
|
fprintf(fp, "filename = %s\n", pvrr->pvrr_filename);
|
|
|
|
fprintf(fp, "status = %c\n", pvrr->pvrr_status);
|
|
fclose(fp);
|
|
}
|
|
|
|
/**
|
|
* Erase status from a recording
|
|
*/
|
|
static void
|
|
pvr_database_erase(pvr_rec_t *pvrr)
|
|
{
|
|
char buf[400];
|
|
|
|
if(pvrr->pvrr_id == 0)
|
|
return;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/recordings/%d", settings_dir, pvrr->pvrr_id);
|
|
unlink(buf);
|
|
}
|
|
|
|
/**
|
|
* Load database
|
|
*/
|
|
static void
|
|
pvr_database_load(void)
|
|
{
|
|
struct config_head cl;
|
|
char buf[400];
|
|
struct dirent *d;
|
|
const char *channel, *title, *desc, *fname, *status, *creator;
|
|
DIR *dir;
|
|
time_t start, stop;
|
|
pvr_rec_t *pvrr;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/recordings", settings_dir);
|
|
|
|
if((dir = opendir(buf)) == NULL)
|
|
return;
|
|
|
|
while((d = readdir(dir)) != NULL) {
|
|
|
|
if(d->d_name[0] == '.')
|
|
continue;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/recordings/%s", settings_dir, d->d_name);
|
|
|
|
TAILQ_INIT(&cl);
|
|
config_read_file0(buf, &cl);
|
|
|
|
channel = config_get_str_sub(&cl, "channel", NULL);
|
|
start = atoi(config_get_str_sub(&cl, "start", "0"));
|
|
stop = atoi(config_get_str_sub(&cl, "stop", "0"));
|
|
|
|
title = config_get_str_sub(&cl, "title", NULL);
|
|
desc = config_get_str_sub(&cl, "description", NULL);
|
|
fname = config_get_str_sub(&cl, "filename", NULL);
|
|
status = config_get_str_sub(&cl, "status", NULL);
|
|
creator = config_get_str_sub(&cl, "creator", NULL);
|
|
|
|
if(channel != NULL && start && stop && title && status) {
|
|
pvrr = calloc(1, sizeof(pvr_rec_t));
|
|
|
|
pvrr->pvrr_channel = channel_find_by_name(channel, 1);
|
|
pvrr->pvrr_start = start;
|
|
pvrr->pvrr_stop = stop;
|
|
pvrr->pvrr_status = *status;
|
|
pvrr->pvrr_filename = fname ? strdup(fname) : NULL;
|
|
pvrr->pvrr_title = title ? strdup(title) : NULL;
|
|
pvrr->pvrr_desc = desc ? strdup(desc) : NULL;
|
|
pvrr->pvrr_creator = creator ? strdup(creator) : NULL;
|
|
|
|
pvrr->pvrr_id = atoi(d->d_name);
|
|
|
|
if(pvrr->pvrr_id > pvr_id_ceil)
|
|
pvr_id_ceil = pvrr->pvrr_id;
|
|
|
|
pvr_link_pvrr(pvrr);
|
|
}
|
|
config_free0(&cl);
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Replace any slash chars in a string with dash
|
|
*/
|
|
static void
|
|
deslashify(char *s)
|
|
{
|
|
int i, len = strlen(s);
|
|
for(i = 0; i < len; i++) if(s[i] == '/')
|
|
s[i] = '-';
|
|
}
|
|
|
|
|
|
/**
|
|
* Filename generator
|
|
*
|
|
* - convert from utf8
|
|
* - avoid duplicate filenames
|
|
*
|
|
*/
|
|
static void
|
|
pvr_generate_filename(pvr_rec_t *pvrr)
|
|
{
|
|
char fullname[1000];
|
|
char *x;
|
|
int tally = 0;
|
|
struct stat st;
|
|
char *name = pvrr->pvrr_title;
|
|
|
|
char *chname;
|
|
char *filename;
|
|
|
|
|
|
if(pvrr->pvrr_filename != NULL) {
|
|
free(pvrr->pvrr_filename);
|
|
pvrr->pvrr_filename = NULL;
|
|
}
|
|
|
|
pvrr->pvrr_fmt_lavfname = "matroska";
|
|
pvrr->pvrr_fmt_postfix = "mkv";
|
|
|
|
filename = utf8tofilename(name && name[0] ? name : "untitled");
|
|
deslashify(filename);
|
|
|
|
chname = utf8tofilename(pvrr->pvrr_channel->ch_name);
|
|
deslashify(chname);
|
|
|
|
snprintf(fullname, sizeof(fullname), "%s/%s-%s.%s",
|
|
config_get_str("pvrdir", "."), chname, filename,
|
|
pvrr->pvrr_fmt_postfix);
|
|
|
|
while(1) {
|
|
if(stat(fullname, &st) == -1) {
|
|
tvhlog(LOG_DEBUG, "pvr", "File \"%s\" -- %s -- Using for recording",
|
|
fullname, strerror(errno));
|
|
break;
|
|
}
|
|
|
|
tvhlog(LOG_DEBUG, "pvr", "Overwrite protection, file \"%s\" exists",
|
|
fullname);
|
|
|
|
tally++;
|
|
snprintf(fullname, sizeof(fullname), "%s/%s-%s-%d.%s",
|
|
config_get_str("pvrdir", "."), chname, filename, tally,
|
|
pvrr->pvrr_fmt_postfix);
|
|
}
|
|
|
|
pvrr->pvrr_filename = strdup(fullname);
|
|
|
|
if(pvrr->pvrr_printname != NULL)
|
|
free(pvrr->pvrr_printname);
|
|
|
|
x = strrchr(pvrr->pvrr_filename, '/');
|
|
pvrr->pvrr_printname = strdup(x ? x + 1 : pvrr->pvrr_filename);
|
|
|
|
free(filename);
|
|
free(chname);
|
|
}
|
|
|
|
|
|
/**
|
|
* Timeout fired, call FSM
|
|
*/
|
|
static void
|
|
pvr_fsm_timeout(void *aux, int64_t now)
|
|
{
|
|
pvr_rec_t *pvrr = aux;
|
|
pvr_fsm(pvrr);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Main PVR state machine
|
|
*/
|
|
static void
|
|
pvr_fsm(pvr_rec_t *pvrr)
|
|
{
|
|
time_t delta;
|
|
time_t now;
|
|
th_ffmuxer_t *tffm = &pvrr->pvrr_tffm;
|
|
|
|
dtimer_disarm(&pvrr->pvrr_timer);
|
|
|
|
time(&now);
|
|
|
|
switch(pvrr->pvrr_status) {
|
|
case HTSTV_PVR_STATUS_NONE:
|
|
break;
|
|
|
|
case HTSTV_PVR_STATUS_SCHEDULED:
|
|
delta = pvrr->pvrr_start - 30 - now;
|
|
|
|
if(delta > 0) {
|
|
dtimer_arm(&pvrr->pvrr_timer, pvr_fsm_timeout, pvrr, delta);
|
|
break;
|
|
}
|
|
|
|
delta = pvrr->pvrr_stop - now;
|
|
|
|
if(delta <= 0) {
|
|
tvhlog(LOG_NOTICE, "pvr", "\"%s\" - Recording skipped, "
|
|
"program has already come to pass", pvrr->pvrr_printname);
|
|
pvrr->pvrr_status = HTSTV_PVR_STATUS_DONE;
|
|
pvr_inform_status_change(pvrr);
|
|
pvr_database_save(pvrr);
|
|
break;
|
|
}
|
|
|
|
/* Add a timer that fires when recording ends */
|
|
|
|
dtimer_arm(&pvrr->pvrr_timer, pvr_fsm_timeout, pvrr, delta);
|
|
|
|
TAILQ_INIT(&pvrr->pvrr_pktq);
|
|
pthread_cond_init(&pvrr->pvrr_pktq_cond, NULL);
|
|
pthread_mutex_init(&pvrr->pvrr_pktq_mutex, NULL);
|
|
|
|
pvrr->pvrr_status = HTSTV_PVR_STATUS_RECORDING;
|
|
pvr_inform_status_change(pvrr);
|
|
tffm->tffm_state = TFFM_WAIT_SUBSCRIPTION; /* cant use set_state() since
|
|
tffm_printname is not
|
|
initialized */
|
|
|
|
pvrr->pvrr_s = subscription_create(pvrr->pvrr_channel, 1000, "pvr",
|
|
pvr_subscription_callback,
|
|
pvrr, 0);
|
|
pvrr->pvrr_error = HTSTV_PVR_STATUS_DONE; /* assume everything will
|
|
work out ok */
|
|
break;
|
|
|
|
case HTSTV_PVR_STATUS_RECORDING:
|
|
/* recording completed (or aborted, or failed or somthing) */
|
|
pvrr->pvrr_status = pvrr->pvrr_error;
|
|
pvr_inform_status_change(pvrr);
|
|
pvr_database_save(pvrr);
|
|
|
|
subscription_unsubscribe(pvrr->pvrr_s);
|
|
dtimer_disarm(&pvrr->pvrr_timer);
|
|
postrec(pvrr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* PVR new packet received
|
|
*/
|
|
static void
|
|
pvrr_packet_input(th_muxer_t *tm, th_stream_t *st, th_pkt_t *pkt)
|
|
{
|
|
pvr_rec_t *pvrr = tm->tm_opaque;
|
|
|
|
if(pvrr->pvrr_dts_offset == AV_NOPTS_VALUE)
|
|
pvrr->pvrr_dts_offset = pkt->pkt_dts;
|
|
|
|
pkt = pkt_copy(pkt);
|
|
|
|
pkt->pkt_dts -= pvrr->pvrr_dts_offset;
|
|
pkt->pkt_pts -= pvrr->pvrr_dts_offset;
|
|
|
|
if(pkt->pkt_dts < 0 || pkt->pkt_pts < 0) {
|
|
pkt_deref(pkt);
|
|
return;
|
|
}
|
|
|
|
pthread_mutex_lock(&pvrr->pvrr_pktq_mutex);
|
|
TAILQ_INSERT_TAIL(&pvrr->pvrr_pktq, pkt, pkt_queue_link);
|
|
pvrr->pvrr_pktq_len++;
|
|
pthread_cond_signal(&pvrr->pvrr_pktq_cond);
|
|
pthread_mutex_unlock(&pvrr->pvrr_pktq_mutex);
|
|
}
|
|
|
|
|
|
/**
|
|
* We've got a transport now, start recording
|
|
*/
|
|
static void
|
|
pvrr_transport_available(pvr_rec_t *pvrr, th_transport_t *t)
|
|
{
|
|
th_ffmuxer_t *tffm = &pvrr->pvrr_tffm;
|
|
th_muxer_t *tm = &tffm->tffm_muxer;
|
|
AVFormatContext *fctx;
|
|
AVOutputFormat *fmt;
|
|
char urlname[500];
|
|
char printname[500];
|
|
int err;
|
|
|
|
tm->tm_opaque = pvrr;
|
|
tm->tm_new_pkt = pvrr_packet_input;
|
|
|
|
pvr_generate_filename(pvrr);
|
|
|
|
/* Find lavf format */
|
|
|
|
fmt = guess_format(pvrr->pvrr_fmt_lavfname, NULL, NULL);
|
|
if(fmt == NULL) {
|
|
tvhlog(LOG_ERR, "pvr",
|
|
"\"%s\" - Unable to open file format \"%s\" for output",
|
|
pvrr->pvrr_printname, pvrr->pvrr_fmt_lavfname);
|
|
pvrr->pvrr_error = HTSTV_PVR_STATUS_FILE_ERROR;
|
|
pvr_fsm(pvrr);
|
|
return;
|
|
}
|
|
|
|
/* Init format context */
|
|
|
|
fctx = av_alloc_format_context();
|
|
|
|
av_strlcpy(fctx->title, pvrr->pvrr_title ?: "",
|
|
sizeof(fctx->title));
|
|
|
|
av_strlcpy(fctx->comment, pvrr->pvrr_desc ?: "",
|
|
sizeof(fctx->comment));
|
|
|
|
av_strlcpy(fctx->copyright, pvrr->pvrr_channel->ch_name,
|
|
sizeof(fctx->copyright));
|
|
|
|
|
|
fctx->oformat = fmt;
|
|
|
|
/* Open output file */
|
|
|
|
snprintf(urlname, sizeof(urlname), "file:%s", pvrr->pvrr_filename);
|
|
|
|
if((err = url_fopen(&fctx->pb, urlname, URL_WRONLY)) < 0) {
|
|
tvhlog(LOG_ERR, "pvr",
|
|
"\"%s\" - Unable to create output file \"%s\" -- %s\n",
|
|
pvrr->pvrr_printname, pvrr->pvrr_filename,
|
|
strerror(AVUNERROR(err)));
|
|
av_free(fctx);
|
|
pvrr->pvrr_error = HTSTV_PVR_STATUS_FILE_ERROR;
|
|
pvr_fsm(pvrr);
|
|
return;
|
|
}
|
|
|
|
snprintf(printname, sizeof(printname), "pvr: \"%s\"", pvrr->pvrr_printname);
|
|
|
|
tffm_open(tffm, t, fctx, printname);
|
|
|
|
pthread_create(&pvrr->pvrr_ptid, NULL, pvr_recorder_thread, pvrr);
|
|
LIST_INSERT_HEAD(&t->tht_muxers, tm, tm_transport_link);
|
|
}
|
|
|
|
/**
|
|
* We've lost our transport, stop recording
|
|
*/
|
|
static void
|
|
pvrr_transport_unavailable(pvr_rec_t *pvrr, th_transport_t *t)
|
|
{
|
|
th_ffmuxer_t *tffm = &pvrr->pvrr_tffm;
|
|
th_muxer_t *tm = &tffm->tffm_muxer;
|
|
th_muxstream_t *tms;
|
|
th_pkt_t *pkt;
|
|
|
|
LIST_REMOVE(tm, tm_transport_link);
|
|
|
|
pvrr->pvrr_dts_offset = AV_NOPTS_VALUE;
|
|
|
|
tffm_set_state(tffm, TFFM_STOP);
|
|
pthread_cond_signal(&pvrr->pvrr_pktq_cond);
|
|
pthread_join(pvrr->pvrr_ptid, NULL);
|
|
|
|
tffm_close(tffm);
|
|
|
|
/* Remove any pending packet for queue */
|
|
|
|
while((pkt = TAILQ_FIRST(&pvrr->pvrr_pktq)) != NULL)
|
|
pkt_deref(pkt);
|
|
|
|
/* Destroy muxstreams */
|
|
|
|
while((tms = LIST_FIRST(&tm->tm_streams)) != NULL) {
|
|
LIST_REMOVE(tms, tms_muxer_link0);
|
|
free(tms);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* We get a callback here when the subscription status is updated,
|
|
* ie, when we are attached to a transport and when we are detached
|
|
*/
|
|
static void
|
|
pvr_subscription_callback(struct th_subscription *s,
|
|
subscription_event_t event, void *opaque)
|
|
{
|
|
th_transport_t *t = s->ths_transport;
|
|
pvr_rec_t *pvrr = opaque;
|
|
|
|
switch(event) {
|
|
case TRANSPORT_AVAILABLE:
|
|
pvrr_transport_available(pvrr, t);
|
|
break;
|
|
|
|
case TRANSPORT_UNAVAILABLE:
|
|
pvrr_transport_unavailable(pvrr, t);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recorder thread
|
|
*/
|
|
static void *
|
|
pvr_recorder_thread(void *aux)
|
|
{
|
|
th_pkt_t *pkt;
|
|
pvr_rec_t *pvrr = aux;
|
|
th_ffmuxer_t *tffm = &pvrr->pvrr_tffm;
|
|
char *t, txt2[50];
|
|
int run = 1;
|
|
time_t now;
|
|
|
|
ctime_r(&pvrr->pvrr_stop, txt2);
|
|
t = strchr(txt2, '\n');
|
|
if(t != NULL)
|
|
*t = 0;
|
|
|
|
tvhlog(LOG_INFO, "pvr", "\"%s\" - Recording started, ends at %s",
|
|
pvrr->pvrr_printname, txt2);
|
|
|
|
|
|
pthread_mutex_lock(&pvrr->pvrr_pktq_mutex);
|
|
|
|
while(run) {
|
|
switch(tffm->tffm_state) {
|
|
case TFFM_WAIT_FOR_START:
|
|
|
|
time(&now);
|
|
if(now >= pvrr->pvrr_start)
|
|
tffm_set_state(tffm, TFFM_WAIT_AUDIO_LOCK);
|
|
break;
|
|
|
|
case TFFM_WAIT_AUDIO_LOCK:
|
|
case TFFM_WAIT_VIDEO_LOCK:
|
|
case TFFM_RUNNING:
|
|
case TFFM_COMMERCIAL:
|
|
break;
|
|
|
|
default:
|
|
run = 0;
|
|
continue;
|
|
}
|
|
|
|
if(pvrr->pvrr_stop < now)
|
|
break;
|
|
|
|
if((pkt = TAILQ_FIRST(&pvrr->pvrr_pktq)) == NULL) {
|
|
pthread_cond_wait(&pvrr->pvrr_pktq_cond, &pvrr->pvrr_pktq_mutex);
|
|
continue;
|
|
}
|
|
|
|
TAILQ_REMOVE(&pvrr->pvrr_pktq, pkt, pkt_queue_link);
|
|
pvrr->pvrr_pktq_len--;
|
|
pthread_mutex_unlock(&pvrr->pvrr_pktq_mutex);
|
|
|
|
tffm_record_packet(tffm, pkt);
|
|
pkt_deref(pkt);
|
|
|
|
pthread_mutex_lock(&pvrr->pvrr_pktq_mutex);
|
|
}
|
|
pthread_mutex_unlock(&pvrr->pvrr_pktq_mutex);
|
|
|
|
tvhlog(LOG_INFO, "pvr", "\"%s\" - Recording completed",
|
|
pvrr->pvrr_printname);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* After recording is completed, execute a program of users choice
|
|
*/
|
|
|
|
static struct strtab pvrrstatustab[] = {
|
|
{ "ok", HTSTV_PVR_STATUS_DONE },
|
|
{ "aborted", HTSTV_PVR_STATUS_ABORTED },
|
|
{ "transponder", HTSTV_PVR_STATUS_NO_TRANSPONDER },
|
|
{ "file-error", HTSTV_PVR_STATUS_FILE_ERROR },
|
|
{ "disk-full", HTSTV_PVR_STATUS_DISK_FULL },
|
|
{ "buffer-error", HTSTV_PVR_STATUS_BUFFER_ERROR },
|
|
};
|
|
|
|
static void
|
|
postrec(pvr_rec_t *pvrr)
|
|
{
|
|
const char *prog, *status;
|
|
const char *argv[16];
|
|
|
|
if((prog = config_get_str("pvrpostproc", NULL)) == NULL)
|
|
return;
|
|
|
|
if((status = val2str(pvrr->pvrr_status, pvrrstatustab)) == NULL)
|
|
return;
|
|
|
|
argv[0] = prog;
|
|
argv[1] = pvrr->pvrr_filename;
|
|
argv[2] = status;
|
|
argv[3] = "default"; /* recording class, currently unused */
|
|
argv[4] = pvrr->pvrr_channel->ch_name;
|
|
argv[5] = pvrr->pvrr_creator;
|
|
argv[6] = pvrr->pvrr_title ?: "";
|
|
argv[7] = pvrr->pvrr_desc ?: "";
|
|
argv[8] = NULL;
|
|
|
|
spawnv(prog, (void *)argv);
|
|
}
|