* Add support for prioritized recordings. The user can chose among five
different priorities.
+ * Fix various issues realted to the Recording Schedule user interface.
+ The UI now includes better visual feedback on what's going on and if
+ there are errors. Also recordings that do not complete successfully
+ are correctly reported as failed entries. Ticket #131
+
hts-tvheadend (2.10) hts; urgency=high
* Fix a crash in HTSP server.
} dvr_entry_sched_state_t;
+typedef enum {
+ DVR_RS_PENDING,
+ DVR_RS_WAIT_AUDIO_LOCK,
+ DVR_RS_WAIT_VIDEO_LOCK,
+ DVR_RS_RUNNING,
+ DVR_RS_COMMERCIAL,
+ DVR_RS_ERROR,
+} dvr_rs_state_t;
+
+
typedef struct dvr_entry {
int de_refcnt; /* Modification is protected under global_lock */
epg_episode_t de_episode;
- char *de_error;
+ uint32_t de_dont_reschedule;
+ /**
+ * Major State
+ */
dvr_entry_sched_state_t de_sched_state;
- uint32_t de_dont_reschedule;
+ /**
+ * Recording state (onyl valid if de_sched_state == DVR_RECORDING)
+ */
+ dvr_rs_state_t de_rec_state;
+
+ /**
+ * Number of errors (only to be modified by the recording thread)
+ */
+ uint32_t de_errors;
+
+ /**
+ * Last error, see SM_CODE_ defines
+ */
+ uint32_t de_last_error;
+
/**
* Autorec linkage
*/
struct dvr_rec_stream_list de_streams;
AVFormatContext *de_fctx;
- enum {
- DE_RS_WAIT_AUDIO_LOCK = 0,
- DE_RS_WAIT_VIDEO_LOCK,
- DE_RS_RUNNING,
- DE_RS_COMMERCIAL,
- } de_rec_state;
-
int de_header_written;
} dvr_entry_t;
/**
* Prototypes
*/
+
+void dvr_entry_notify(dvr_entry_t *de);
+
+const char *dvr_entry_status(dvr_entry_t *de);
+
+const char *dvr_entry_schedstatus(dvr_entry_t *de);
+
void dvr_entry_create_by_autorec(event_t *e, dvr_autorec_entry_t *dae);
dvr_entry_t *dvr_entry_create_by_event(event_t *e, const char *creator,
void dvr_rec_subscribe(dvr_entry_t *de);
-void dvr_rec_unsubscribe(dvr_entry_t *de);
+void dvr_rec_unsubscribe(dvr_entry_t *de, int stopcode);
dvr_entry_t *dvr_entry_find_by_id(int id);
#include "dvr.h"
#include "notify.h"
#include "htsp.h"
+#include "streaming.h"
char *dvr_storage;
char *dvr_format;
static void dvr_timer_expire(void *aux);
static void dvr_timer_start_recording(void *aux);
+/**
+ * Return printable status for a dvr entry
+ */
+const char *
+dvr_entry_status(dvr_entry_t *de)
+{
+ switch(de->de_sched_state) {
+ case DVR_SCHEDULED:
+ return "Scheduled for recording";
+
+ case DVR_RECORDING:
+
+ switch(de->de_rec_state) {
+ case DVR_RS_PENDING:
+ return "Pending start";
+ case DVR_RS_WAIT_AUDIO_LOCK:
+ return "Waiting for audio lock";
+ case DVR_RS_WAIT_VIDEO_LOCK:
+ return "Waiting for video lock";
+ case DVR_RS_RUNNING:
+ return "Running";
+ case DVR_RS_COMMERCIAL:
+ return "Commercial break";
+ case DVR_RS_ERROR:
+ return streaming_code2txt(de->de_last_error);
+ default:
+ return "Invalid";
+ }
+
+ case DVR_COMPLETED:
+ if(de->de_last_error)
+ return streaming_code2txt(de->de_last_error);
+ else
+ return "Completed OK";
+
+ default:
+ return "Invalid";
+ }
+}
+
+
+/**
+ *
+ */
+const char *
+dvr_entry_schedstatus(dvr_entry_t *de)
+{
+ switch(de->de_sched_state) {
+ case DVR_SCHEDULED:
+ return "scheduled";
+ case DVR_RECORDING:
+ if(de->de_last_error)
+ return "recordingError";
+ else
+ return "recording";
+ case DVR_COMPLETED:
+ if(de->de_last_error)
+ return "completedError";
+ else
+ return "completed";
+ default:
+ return "unknown";
+ }
+}
+
/**
*
}
+/**
+ *
+ */
+void
+dvr_entry_notify(dvr_entry_t *de)
+{
+ htsmsg_t *m = htsmsg_create_map();
+
+ htsmsg_add_u32(m, "updateEntry", 1);
+ htsmsg_add_u32(m, "id", de->de_id);
+ htsmsg_add_str(m, "status", dvr_entry_status(de));
+ htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de));
+ notify_by_msg("dvrdb", m);
+}
+
/**
*
tvh_str_set(&de->de_desc, htsmsg_get_str(c, "description"));
tvh_str_set(&de->de_filename, htsmsg_get_str(c, "filename"));
- tvh_str_set(&de->de_error, htsmsg_get_str(c, "error"));
+
+ htsmsg_get_u32(c, "errorcode", &de->de_last_error);
+ htsmsg_get_u32(c, "errors", &de->de_errors);
htsmsg_get_u32(c, "noresched", &de->de_dont_reschedule);
htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri));
- if(de->de_error != NULL)
- htsmsg_add_str(m, "error", de->de_error);
+ if(de->de_last_error)
+ htsmsg_add_u32(m, "errorcode", de->de_last_error);
+
+ if(de->de_errors)
+ htsmsg_add_u32(m, "errors", de->de_errors);
htsmsg_add_u32(m, "noresched", de->de_dont_reschedule);
*
*/
static void
-dvr_stop_recording(dvr_entry_t *de, const char *errmsg)
+dvr_stop_recording(dvr_entry_t *de, int stopcode)
{
- dvr_rec_unsubscribe(de);
+ dvr_rec_unsubscribe(de, stopcode);
de->de_sched_state = DVR_COMPLETED;
- tvh_str_set(&de->de_error, errmsg);
tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": "
"End of program: %s",
de->de_title, de->de_channel->ch_name,
- de->de_error ?: "Program ended");
+ streaming_code2txt(de->de_last_error) ?: "Program ended");
- dvrdb_changed();
dvr_entry_save(de);
htsp_dvr_entry_update(de);
+ dvr_entry_notify(de);
gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de,
de->de_stop + dvr_retention_days * 86400);
static void
dvr_timer_stop_recording(void *aux)
{
- dvr_entry_t *de = aux;
- dvr_stop_recording(de, NULL);
+ dvr_stop_recording(aux, 0);
}
dvr_entry_t *de = aux;
de->de_sched_state = DVR_RECORDING;
+ de->de_rec_state = DVR_RS_PENDING;
tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\" recorder starting",
de->de_title, de->de_channel->ch_name);
- dvrdb_changed();
+ dvr_entry_notify(de);
htsp_dvr_entry_update(de);
-
dvr_rec_subscribe(de);
gtimer_arm_abs(&de->de_timer, dvr_timer_stop_recording, de,
case DVR_RECORDING:
de->de_dont_reschedule = 1;
- dvr_stop_recording(de, "Aborted by user");
+ dvr_stop_recording(de, SM_CODE_ABORTED);
return de;
case DVR_COMPLETED:
dvr_entry_purge(dvr_entry_t *de)
{
if(de->de_sched_state == DVR_RECORDING)
- dvr_stop_recording(de, "Channel removed from system");
+ dvr_stop_recording(de, SM_CODE_SOURCE_DELETED);
dvr_entry_remove(de);
}
*
*/
void
-dvr_rec_unsubscribe(dvr_entry_t *de)
+dvr_rec_unsubscribe(dvr_entry_t *de, int stopcode)
{
assert(de->de_s != NULL);
pthread_join(de->de_thread, NULL);
de->de_s = NULL;
+
+ de->de_last_error = stopcode;
}
}
+/**
+ *
+ */
+static void
+dvr_rec_set_state(dvr_entry_t *de, dvr_rs_state_t newstate, int error)
+{
+ de->de_rec_state = newstate;
+ de->de_last_error = error;
+ if(error)
+ de->de_errors++;
+ dvr_entry_notify(de);
+}
+
/**
*
*/
pthread_mutex_lock(&global_lock);
dvr_rec_start(de, sm->sm_data);
de->de_header_written = 0;
- de->de_rec_state = DE_RS_WAIT_AUDIO_LOCK;
+ dvr_rec_set_state(de, DVR_RS_WAIT_AUDIO_LOCK, 0);
pthread_mutex_unlock(&global_lock);
break;
case SMT_STOP:
- tvhlog(sm->sm_code ? LOG_ERR : LOG_INFO,
- "pvr", "Recording stopped: \"%s\": %s",
- de->de_filename ?: de->de_title,
- streaming_code2txt(sm->sm_code));
+
+ if(sm->sm_code == 0) {
+ /* Completed */
+
+ de->de_last_error = 0;
+
+ tvhlog(LOG_INFO,
+ "pvr", "Recording completed: \"%s\"",
+ de->de_filename ?: de->de_title);
+
+ } else {
+
+ if(de->de_last_error != sm->sm_code) {
+ dvr_rec_set_state(de, DVR_RS_ERROR, sm->sm_code);
+
+ tvhlog(LOG_ERR,
+ "pvr", "Recording stopped: \"%s\": %s",
+ de->de_filename ?: de->de_title,
+ streaming_code2txt(sm->sm_code));
+ }
+ }
+
dvr_thread_epilog(de);
break;
case SMT_TRANSPORT_STATUS:
+ printf("TSS: %x\n", sm->sm_code);
if(sm->sm_code & TSS_PACKETS) {
} else if(sm->sm_code & (TSS_GRACEPERIOD | TSS_ERRORS)) {
- dvr_rec_fatal_error(de, "Source problems: %s",
- transport_tss2text(sm->sm_code));
+
+ int code = SM_CODE_UNDEFINED_ERROR;
+
+
+ if(sm->sm_code & TSS_NO_DESCRAMBLER)
+ code = SM_CODE_NO_DESCRAMBLER;
+
+ if(sm->sm_code & TSS_NO_ACCESS)
+ code = SM_CODE_NO_ACCESS;
+
+ if(de->de_last_error != code) {
+ dvr_rec_set_state(de, DVR_RS_ERROR, code);
+ tvhlog(LOG_ERR,
+ "pvr", "Streaming error: \"%s\": %s",
+ de->de_filename ?: de->de_title,
+ streaming_code2txt(code));
+ }
}
break;
case SMT_NOSTART:
- dvr_rec_fatal_error(de, "Unable to start -- %s",
- streaming_code2txt(sm->sm_code));
+
+ if(de->de_last_error != sm->sm_code) {
+ dvr_rec_set_state(de, DVR_RS_ERROR, sm->sm_code);
+
+ tvhlog(LOG_ERR,
+ "pvr", "Recording unable to start: \"%s\": %s",
+ de->de_filename ?: de->de_title,
+ streaming_code2txt(sm->sm_code));
+ }
break;
case SMT_MPEGTS:
default:
break;
- case DE_RS_WAIT_AUDIO_LOCK:
+ case DVR_RS_WAIT_AUDIO_LOCK:
if(ctx->codec_type != CODEC_TYPE_AUDIO || drs->drs_decoded)
break;
}
if(is_all_decoded(de, CODEC_TYPE_AUDIO))
- de->de_rec_state = DE_RS_WAIT_VIDEO_LOCK;
+ dvr_rec_set_state(de, DVR_RS_WAIT_VIDEO_LOCK, 0);
break;
- case DE_RS_WAIT_VIDEO_LOCK:
+ case DVR_RS_WAIT_VIDEO_LOCK:
if(ctx->codec_type != CODEC_TYPE_VIDEO || drs->drs_decoded)
break;
/* All Audio & Video decoded, start recording */
- de->de_rec_state = DE_RS_RUNNING;
+ dvr_rec_set_state(de, DVR_RS_RUNNING, 0);
if(!de->de_header_written) {
}
/* FALLTHRU */
- case DE_RS_RUNNING:
+ case DVR_RS_RUNNING:
if(de->de_header_written == 0)
break;
de->de_ts_com_start = pkt->pkt_dts;
- de->de_rec_state = DE_RS_COMMERCIAL;
+ dvr_rec_set_state(de, DVR_RS_COMMERCIAL, 0);
break;
}
outputpacket:
break;
- case DE_RS_COMMERCIAL:
+ case DVR_RS_COMMERCIAL:
if(pkt->pkt_commercial != COMMERCIAL_YES &&
st->codec->codec->type == CODEC_TYPE_VIDEO &&
/* Switch out of commercial mode */
de->de_ts_com_offset += (pkt->pkt_dts - de->de_ts_com_start);
- de->de_rec_state = DE_RS_RUNNING;
+ dvr_rec_set_state(de, DVR_RS_RUNNING, 0);
tvhlog(LOG_INFO, "dvr",
"%s - Skipped %" PRId64 " seconds of commercials",
fmap['C'] = de->de_creator; /* user who created this recording */
fmap['t'] = de->de_title; /* program title */
fmap['d'] = de->de_desc; /* program description */
- fmap['e'] = de->de_error; /* error message, empty if no error (FIXME:?) */
+ /* error message, empty if no error (FIXME:?) */
+ fmap['e'] = tvh_strdupa(streaming_code2txt(de->de_last_error));
fmap['S'] = start; /* start time, unix epoch */
fmap['E'] = stop; /* stop time, unix epoch */
break;
case DVR_COMPLETED:
s = "completed";
- if(de->de_error)
- error = de->de_error;
+ if(de->de_last_error)
+ error = streaming_code2txt(de->de_last_error);
break;
case DVR_NOSTATE:
s = "invalid";
case SM_CODE_NO_SOURCE:
return "No source available";
+ case SM_CODE_ABORTED:
+ return "Aborted by user";
+
+ case SM_CODE_NO_DESCRAMBLER:
+ return "No descrambler";
+
+ case SM_CODE_NO_ACCESS:
+ return "No access";
+
+
default:
return "Unknown reason";
}
streaming_message_type_t smt);
const char *streaming_code2txt(int code);
-
+
#endif /* STREAMING_H_ */
th_transport_t *t, *skip;
streaming_message_t *sm;
char buf[128];
- int errorcode;
+ int error;
+
lock_assert(&global_lock);
gtimer_arm(&subscription_reschedule_timer,
- subscription_reschedule_cb, NULL, 60);
+ subscription_reschedule_cb, NULL, 2);
LIST_FOREACH(s, &subscriptions, ths_global_link) {
if(s->ths_channel == NULL)
if(s->ths_state != SUBSCRIPTION_BAD_TRANSPORT)
continue; /* And it seems to work ok, so we're happy */
skip = s->ths_transport;
- transport_remove_subscriber(s->ths_transport, s, SM_CODE_BAD_SOURCE);
+ error = s->ths_testing_error;
+ transport_remove_subscriber(s->ths_transport, s, s->ths_testing_error);
} else {
+ error = 0;
skip = NULL;
}
snprintf(buf, sizeof(buf), "Subscription \"%s\"", s->ths_title);
- t = transport_find(s->ths_channel, s->ths_weight, buf, &errorcode, skip);
+ t = transport_find(s->ths_channel, s->ths_weight, buf, &error, skip);
if(t == NULL) {
/* No transport available */
- sm = streaming_msg_create_code(SMT_NOSTART, errorcode);
+ sm = streaming_msg_create_code(SMT_NOSTART, error);
streaming_target_deliver(s->ths_output, sm);
continue;
}
sm->sm_code & (TSS_GRACEPERIOD | TSS_ERRORS)) {
// No, mark our subscription as bad_transport
// the scheduler will take care of things
+ s->ths_testing_error = tss2errcode(sm->sm_code);
s->ths_state = SUBSCRIPTION_BAD_TRANSPORT;
streaming_msg_free(sm);
return;
SUBSCRIPTION_BAD_TRANSPORT,
} ths_state;
+ int ths_testing_error;
+
LIST_ENTRY(th_subscription) ths_channel_link;
struct channel *ths_channel; /* May be NULL if channel has been
destroyed during the
{
th_transport_t *t, **vec;
int cnt = 0, i, r, off;
- int error = 0;
lock_assert(&global_lock);
LIST_FOREACH(t, &ch->ch_transports, tht_ch_link) {
if(!t->tht_enabled) {
- error = SM_CODE_SVC_NOT_ENABLED;
if(loginfo != NULL) {
tvhlog(LOG_NOTICE, "Transport", "%s: Skipping \"%s\" -- not enabled",
loginfo, transport_nicename(t));
}
if(t->tht_quality_index(t) < 10) {
- error = SM_CODE_BAD_SIGNAL;
if(loginfo != NULL) {
tvhlog(LOG_NOTICE, "Transport",
"%s: Skipping \"%s\" -- Quality below 10%",
off = 0;
}
- error = SM_CODE_NO_SOURCE;
-
/* First, try all transports without stealing */
for(i = off; i < cnt; i++) {
t = vec[i];
return t;
if((r = transport_start(t, 0, 0)) == 0)
return t;
- tvhlog(LOG_DEBUG, "Transport", "%s: Unable to use \"%s\" -- %s",
+ if(loginfo != NULL)
+ tvhlog(LOG_DEBUG, "Transport", "%s: Unable to use \"%s\" -- %s",
loginfo, transport_nicename(t), streaming_code2txt(r));
}
t = vec[i];
if((r = transport_start(t, weight, 0)) == 0)
return t;
- error = r;
- if(loginfo != NULL)
- tvhlog(LOG_NOTICE, "Transport",
- "%s: Skipping \"%s\" -- %s",
- loginfo, transport_nicename(t), streaming_code2txt(r));
+ *errorp = r;
}
- if(errorp != NULL)
- *errorp = error;
return NULL;
}
return "No status";
}
+
+/**
+ *
+ */
+int
+tss2errcode(int tss)
+{
+ if(tss & TSS_NO_ACCESS)
+ return SM_CODE_NO_ACCESS;
+
+ if(tss & TSS_NO_DESCRAMBLER)
+ return SM_CODE_NO_DESCRAMBLER;
+
+ if(tss & TSS_GRACEPERIOD)
+ return SM_CODE_NO_INPUT;
+
+ return SM_CODE_OK;
+}
+
+
/**
*
*/
void transport_refresh_channel(th_transport_t *t);
+int tss2errcode(int tss);
+
#endif /* TRANSPORTS_H */
#define SM_CODE_OK 0
+#define SM_CODE_UNDEFINED_ERROR 1
+
#define SM_CODE_SOURCE_RECONFIGURED 100
#define SM_CODE_BAD_SOURCE 101
#define SM_CODE_SOURCE_DELETED 102
#define SM_CODE_BAD_SIGNAL 205
#define SM_CODE_NO_SOURCE 206
+#define SM_CODE_ABORTED 300
+
+#define SM_CODE_NO_DESCRAMBLER 400
+#define SM_CODE_NO_ACCESS 401
+#define SM_CODE_NO_INPUT 402
+
/**
* Streaming messages are sent from the pad to its receivers
*/
dvr_query_result_t dqr;
dvr_entry_t *de;
int start = 0, end, limit, i;
- const char *s, *t = NULL;
+ const char *s;
off_t fsize;
if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL)
htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri));
- switch(de->de_sched_state) {
- case DVR_SCHEDULED:
- s = "Scheduled for recording";
- t = "sched";
- break;
- case DVR_RECORDING:
- s = "Recording";
- t = "rec";
- break;
- case DVR_COMPLETED:
- s = de->de_error ?: "Completed OK";
- t = "done";
- break;
- default:
- s = "Invalid";
- break;
- }
- htsmsg_add_str(m, "status", s);
- if(t != NULL) htsmsg_add_str(m, "schedstate", t);
+ htsmsg_add_str(m, "status", dvr_entry_status(de));
+ htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de));
if(de->de_sched_state == DVR_COMPLETED) {
});
switch(entry.schedstate) {
- case 'sched':
+ case 'scheduled':
win.addButton({
handler: cancelEvent,
text: "Remove from schedule"
});
break;
- case 'rec':
+ case 'recording':
+ case 'recordingError':
win.addButton({
handler: cancelEvent,
text: "Abort recording"
*/
tvheadend.dvrschedule = function() {
+ var actions = new Ext.ux.grid.RowActions({
+ header:'',
+ dataIndex: 'actions',
+ width: 45,
+ actions: [
+ {
+ iconIndex:'schedstate'
+ }
+ ]
+ });
+
function renderDate(value){
var dt = new Date(value);
return dt.format('l H:i');
}
var dvrCm = new Ext.grid.ColumnModel([
+ actions,
{
width: 250,
id:'title',
iconCls: 'clock',
store: tvheadend.dvrStore,
cm: dvrCm,
+ plugins: [actions],
viewConfig: {forceFit:true},
tbar: [
{
});
tvheadend.comet.on('dvrdb', function(m) {
+
+ console.log(m);
+
if(m.reload != null)
tvheadend.dvrStore.reload();
+
+ if(m.updateEntry != null) {
+ r = tvheadend.dvrStore.getById(m.id)
+ if(typeof r === 'undefined') {
+ tvheadend.dvrStore.reload();
+ return;
+ }
+
+ console.log(r);
+
+ r.data.status = m.status;
+ r.data.schedstate = m.schedstate;
+
+ console.log(r);
+
+ tvheadend.dvrStore.afterEdit(r);
+ tvheadend.dvrStore.fireEvent('updated',
+ tvheadend.dvrStore, r,
+ Ext.data.Record.COMMIT);
+ }
});
background-image:url(../icons/layers.png) !important;
}
+.scheduled {
+ background-image:url(../icons/clock.png) !important;
+}
+
+.recordingError {
+ background-image:url(../icons/exclamation.png) !important;
+}
+
+.completed {
+ background-image:url(../icons/tick.png) !important;
+}
+
+.completedError {
+ background-image:url(../icons/exclamation.png) !important;
+}
+
+.recording {
+ background-image:url(../icons/rec.png) !important;
+}
+
.x-smallhdr {