/* * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tvhead.h" #include "channels.h" #include "subscriptions.h" #include "htsclient.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(channel, 1, NULL); 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) { syslog(LOG_DEBUG, "pvr: File \"%s\" -- %s -- Using for recording", fullname, strerror(errno)); break; } syslog(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) { syslog(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); 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) { syslog(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) { syslog(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; syslog(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); syslog(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); }