/* * Services * Copyright (C) 2010 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 "tvheadend.h" #include "service.h" #include "subscriptions.h" #include "tsdemux.h" #include "streaming.h" #include "v4l.h" #include "psi.h" #include "packet.h" #include "channels.h" #include "cwc.h" #include "capmt.h" #include "notify.h" #include "serviceprobe.h" #include "atomic.h" #include "dvb/dvb.h" #include "htsp_server.h" #include "lang_codes.h" static void service_data_timeout(void *aux); static const char *service_channel_get(void *obj); static void service_channel_set(void *obj, const char *str); const idclass_t service_class = { .ic_class = "service", .ic_properties = (const property_t[]){ { "channel", "Channel", PT_STR, .str_get = service_channel_get, .str_set = service_channel_set, }, { "enabled", "Enabled", PT_BOOL, offsetof(service_t, s_enabled) }, { }} }; /** * */ static void stream_init(elementary_stream_t *st) { st->es_cc_valid = 0; st->es_startcond = 0xffffffff; st->es_curdts = PTS_UNSET; st->es_curpts = PTS_UNSET; st->es_prevdts = PTS_UNSET; st->es_pcr_real_last = PTS_UNSET; st->es_pcr_last = PTS_UNSET; st->es_pcr_drift = 0; st->es_pcr_recovery_fails = 0; st->es_blank = 0; } /** * */ static void stream_clean(elementary_stream_t *st) { if(st->es_demuxer_fd != -1) { // XXX: Should be in DVB-code perhaps close(st->es_demuxer_fd); st->es_demuxer_fd = -1; } free(st->es_priv); st->es_priv = NULL; /* Clear reassembly buffers */ st->es_startcode = 0; sbuf_free(&st->es_buf); sbuf_free(&st->es_buf_ps); sbuf_free(&st->es_buf_a); if(st->es_curpkt != NULL) { pkt_ref_dec(st->es_curpkt); st->es_curpkt = NULL; } free(st->es_global_data); st->es_global_data = NULL; st->es_global_data_len = 0; } /** * */ void service_stream_destroy(service_t *t, elementary_stream_t *es) { if(t->s_status == SERVICE_RUNNING) stream_clean(es); avgstat_flush(&es->es_rate); avgstat_flush(&es->es_cc_errors); TAILQ_REMOVE(&t->s_components, es, es_link); free(es->es_nicename); free(es); } /** * Service lock must be held */ static void service_stop(service_t *t) { th_descrambler_t *td; elementary_stream_t *st; gtimer_disarm(&t->s_receive_timer); t->s_stop_feed(t); pthread_mutex_lock(&t->s_stream_mutex); while((td = LIST_FIRST(&t->s_descramblers)) != NULL) td->td_stop(td); t->s_tt_commercial_advice = COMMERCIAL_UNKNOWN; assert(LIST_FIRST(&t->s_streaming_pad.sp_targets) == NULL); assert(LIST_FIRST(&t->s_subscriptions) == NULL); /** * Clean up each stream */ TAILQ_FOREACH(st, &t->s_components, es_link) stream_clean(st); sbuf_free(&t->s_tsbuf); t->s_status = SERVICE_IDLE; pthread_mutex_unlock(&t->s_stream_mutex); } /** * Remove the given subscriber from the service * * if s == NULL all subscribers will be removed * * Global lock must be held */ void service_remove_subscriber(service_t *t, th_subscription_t *s, int reason) { lock_assert(&global_lock); if(s == NULL) { while((s = LIST_FIRST(&t->s_subscriptions)) != NULL) { subscription_unlink_service(s, reason); } } else { subscription_unlink_service(s, reason); } if(LIST_FIRST(&t->s_subscriptions) == NULL) service_stop(t); } /** * */ int service_start(service_t *t, int instance) { elementary_stream_t *st; int r, timeout = 2; lock_assert(&global_lock); assert(t->s_status != SERVICE_RUNNING); t->s_streaming_status = 0; t->s_pcr_drift = 0; if((r = t->s_start_feed(t, instance))) return r; cwc_service_start(t); capmt_service_start(t); pthread_mutex_lock(&t->s_stream_mutex); t->s_status = SERVICE_RUNNING; t->s_current_pts = PTS_UNSET; /** * Initialize stream */ TAILQ_FOREACH(st, &t->s_components, es_link) stream_init(st); pthread_mutex_unlock(&t->s_stream_mutex); if(t->s_grace_period != NULL) timeout = t->s_grace_period(t); gtimer_arm(&t->s_receive_timer, service_data_timeout, t, timeout); return 0; } /** * Main entry point for starting a service based on a channel */ service_instance_t * service_find_instance(channel_t *ch, struct service_instance_list *sil, int *error, int weight) { service_t *s; service_instance_t *si, *next; lock_assert(&global_lock); // First, update list of candidates LIST_FOREACH(si, sil, si_link) si->si_mark = 1; LIST_FOREACH(s, &ch->ch_services, s_ch_link) s->s_enlist(s, sil); for(si = LIST_FIRST(sil); si != NULL; si = next) { next = LIST_NEXT(si, si_link); if(si->si_mark) service_instance_destroy(si); } // Check if any service is already running, if so, use that LIST_FOREACH(si, sil, si_link) if(si->si_s->s_status == SERVICE_RUNNING) return si; // Check if any is idle LIST_FOREACH(si, sil, si_link) if(si->si_weight == 0 && si->si_error == 0) break; // Check if to kick someone out if(si == NULL) { LIST_FOREACH(si, sil, si_link) { if(si->si_weight < weight && si->si_error == 0) break; } } if(si == NULL) { *error = SM_CODE_NO_FREE_ADAPTER; return NULL; } service_start(si->si_s, si->si_instance); return NULL; } #if 0 /** * */ service_t * service_enlist(channel_t *ch) { service_t *t, **vec; int cnt = 0, i, r, off; int err = 0; lock_assert(&global_lock); /* First, sort all services in order */ LIST_FOREACH(t, &ch->ch_services, s_ch_link) cnt++; vec = alloca(cnt * sizeof(service_t *)); cnt = 0; LIST_FOREACH(t, &ch->ch_services, s_ch_link) { if(!t->s_enabled) { if(loginfo != NULL) { tvhlog(LOG_NOTICE, "Service", "%s: Skipping \"%s\" -- not enabled", loginfo, service_nicename(t)); err = SM_CODE_SVC_NOT_ENABLED; } continue; } vec[cnt++] = t; tvhlog(LOG_DEBUG, "Service", "%s: Adding adapter \"%s\" for service \"%s\"", loginfo, service_adapter_nicename(t), service_nicename(t)); } /* Sort services, lower priority should come come earlier in the vector (i.e. it will be more favoured when selecting a service */ // qsort(vec, cnt, sizeof(service_t *), servicecmp); // Skip up to the service that the caller didn't want // If the sorting above is not stable that might mess up things // temporary. But it should resolve itself eventually if(skip != NULL) { for(i = 0; i < cnt; i++) { if(skip == t) break; } off = i + 1; } else { off = 0; } /* First, try all services without stealing */ for(i = off; i < cnt; i++) { t = vec[i]; if(t->s_status == SERVICE_RUNNING) return t; tvhlog(LOG_DEBUG, "Service", "%s: Probing adapter \"%s\" without stealing for service \"%s\"", loginfo, service_adapter_nicename(t), service_nicename(t)); if((r = service_start(t, 0, 0)) == 0) return t; if(loginfo != NULL) tvhlog(LOG_DEBUG, "Service", "%s: Unable to use \"%s\" -- %s", loginfo, service_nicename(t), streaming_code2txt(r)); } /* Ok, nothing, try again, but supply our weight and thus, try to steal transponders */ for(i = off; i < cnt; i++) { t = vec[i]; tvhlog(LOG_DEBUG, "Service", "%s: Probing adapter \"%s\" with weight %d for service \"%s\"", loginfo, service_adapter_nicename(t), weight, service_nicename(t)); if((r = service_start(t, weight, 0)) == 0) return t; *errorp = r; } if(err) *errorp = err; else if(*errorp == 0) *errorp = SM_CODE_NO_SERVICE; return NULL; } #endif /** * */ void service_unref(service_t *t) { if((atomic_add(&t->s_refcount, -1)) == 1) free(t); } /** * */ void service_ref(service_t *t) { atomic_add(&t->s_refcount, 1); } /** * Destroy a service */ void service_destroy(service_t *t) { elementary_stream_t *st; th_subscription_t *s; channel_t *ch = t->s_ch; if(t->s_dtor != NULL) t->s_dtor(t); lock_assert(&global_lock); serviceprobe_delete(t); while((s = LIST_FIRST(&t->s_subscriptions)) != NULL) { subscription_unlink_service(s, SM_CODE_SOURCE_DELETED); } if(t->s_ch != NULL) { t->s_ch = NULL; LIST_REMOVE(t, s_ch_link); } LIST_REMOVE(t, s_group_link); idnode_unlink(&t->s_id); if(t->s_status != SERVICE_IDLE) service_stop(t); t->s_status = SERVICE_ZOMBIE; free(t->s_svcname); free(t->s_provider); free(t->s_dvb_charset); while((st = TAILQ_FIRST(&t->s_components)) != NULL) service_stream_destroy(t, st); free(t->s_pat_section); free(t->s_pmt_section); sbuf_free(&t->s_tsbuf); avgstat_flush(&t->s_cc_errors); avgstat_flush(&t->s_rate); service_unref(t); if(ch != NULL) { if(LIST_FIRST(&ch->ch_services) == NULL) channel_delete(ch); } } /** * Create and initialize a new service struct */ service_t * service_create(const char *uuid, int source_type, const idclass_t *idc) { service_t *t = calloc(1, sizeof(service_t)); lock_assert(&global_lock); pthread_mutex_init(&t->s_stream_mutex, NULL); pthread_cond_init(&t->s_tss_cond, NULL); t->s_source_type = source_type; t->s_refcount = 1; t->s_enabled = 1; t->s_pcr_last = PTS_UNSET; t->s_dvb_charset = NULL; t->s_dvb_eit_enable = 1; TAILQ_INIT(&t->s_components); sbuf_init(&t->s_tsbuf); streaming_pad_init(&t->s_streaming_pad); idnode_insert(&t->s_id, uuid, idc); return t; } /** * Find a service based on the given identifier */ service_t * service_find_by_identifier(const char *identifier) { return idnode_find(identifier, &service_class); } /** * */ static void service_stream_make_nicename(service_t *t, elementary_stream_t *st) { char buf[200]; if(st->es_pid != -1) snprintf(buf, sizeof(buf), "%s: %s @ #%d", service_nicename(t), streaming_component_type2txt(st->es_type), st->es_pid); else snprintf(buf, sizeof(buf), "%s: %s", service_nicename(t), streaming_component_type2txt(st->es_type)); free(st->es_nicename); st->es_nicename = strdup(buf); } /** * */ void service_make_nicename(service_t *t) { char buf[200]; source_info_t si; elementary_stream_t *st; lock_assert(&t->s_stream_mutex); t->s_setsourceinfo(t, &si); snprintf(buf, sizeof(buf), "%s%s%s%s%s", si.si_adapter ?: "", si.si_adapter && si.si_mux ? "/" : "", si.si_mux ?: "", si.si_mux && si.si_service ? "/" : "", si.si_service ?: ""); service_source_info_free(&si); free(t->s_nicename); t->s_nicename = strdup(buf); TAILQ_FOREACH(st, &t->s_components, es_link) service_stream_make_nicename(t, st); } /** * Add a new stream to a service */ elementary_stream_t * service_stream_create(service_t *t, int pid, streaming_component_type_t type) { elementary_stream_t *st; int i = 0; int idx = 0; lock_assert(&t->s_stream_mutex); TAILQ_FOREACH(st, &t->s_components, es_link) { if(st->es_index > idx) idx = st->es_index; i++; if(pid != -1 && st->es_pid == pid) return st; } st = calloc(1, sizeof(elementary_stream_t)); st->es_index = idx + 1; st->es_type = type; TAILQ_INSERT_TAIL(&t->s_components, st, es_link); st->es_service = t; st->es_pid = pid; st->es_demuxer_fd = -1; avgstat_init(&st->es_rate, 10); avgstat_init(&st->es_cc_errors, 10); service_stream_make_nicename(t, st); if(t->s_flags & S_DEBUG) tvhlog(LOG_DEBUG, "service", "Add stream %s", st->es_nicename); if(t->s_status == SERVICE_RUNNING) stream_init(st); return st; } /** * Add a new stream to a service */ elementary_stream_t * service_stream_find(service_t *t, int pid) { elementary_stream_t *st; lock_assert(&t->s_stream_mutex); TAILQ_FOREACH(st, &t->s_components, es_link) { if(st->es_pid == pid) return st; } return NULL; } /** * */ void service_map_channel(service_t *t, channel_t *ch, int save) { lock_assert(&global_lock); if(t->s_ch != NULL) { LIST_REMOVE(t, s_ch_link); htsp_channel_update(t->s_ch); t->s_ch = NULL; } if(ch != NULL) { avgstat_init(&t->s_cc_errors, 3600); avgstat_init(&t->s_rate, 10); t->s_ch = ch; LIST_INSERT_HEAD(&ch->ch_services, t, s_ch_link); htsp_channel_update(t->s_ch); } if(save) t->s_config_save(t); } /** * */ static const char *service_channel_get(void *obj) { service_t *s = obj; return s->s_ch ? s->s_ch->ch_name : NULL; } /** * */ static void service_channel_set(void *obj, const char *str) { service_map_channel(obj, str ? channel_find_by_name(str, 1, 0) : NULL, 1); } /** * */ void service_set_dvb_charset(service_t *t, const char *dvb_charset) { lock_assert(&global_lock); if(t->s_dvb_charset != NULL && !strcmp(t->s_dvb_charset, dvb_charset)) return; free(t->s_dvb_charset); t->s_dvb_charset = strdup(dvb_charset); t->s_config_save(t); } /** * */ void service_set_dvb_eit_enable(service_t *t, int dvb_eit_enable) { if(t->s_dvb_eit_enable == dvb_eit_enable) return; t->s_dvb_eit_enable = dvb_eit_enable; t->s_config_save(t); } /** * */ static void service_data_timeout(void *aux) { service_t *t = aux; pthread_mutex_lock(&t->s_stream_mutex); if(!(t->s_streaming_status & TSS_PACKETS)) service_set_streaming_status_flags(t, TSS_GRACEPERIOD); pthread_mutex_unlock(&t->s_stream_mutex); } /** * */ static struct strtab stypetab[] = { { "SDTV", ST_SDTV }, { "Radio", ST_RADIO }, { "HDTV", ST_HDTV }, { "HDTV", ST_EX_HDTV }, { "SDTV", ST_EX_SDTV }, { "HDTV", ST_EP_HDTV }, { "HDTV", ST_ET_HDTV }, { "SDTV", ST_DN_SDTV }, { "HDTV", ST_DN_HDTV }, { "SDTV", ST_SK_SDTV }, { "SDTV", ST_NE_SDTV }, { "SDTV-AC", ST_AC_SDTV }, { "HDTV-AC", ST_AC_HDTV }, }; const char * service_servicetype_txt(service_t *t) { return val2str(t->s_servicetype, stypetab) ?: "Other"; } /** * */ int service_is_tv(service_t *t) { return t->s_servicetype == ST_SDTV || t->s_servicetype == ST_HDTV || t->s_servicetype == ST_EX_HDTV || t->s_servicetype == ST_EX_SDTV || t->s_servicetype == ST_EP_HDTV || t->s_servicetype == ST_ET_HDTV || t->s_servicetype == ST_DN_SDTV || t->s_servicetype == ST_DN_HDTV || t->s_servicetype == ST_SK_SDTV || t->s_servicetype == ST_NE_SDTV || t->s_servicetype == ST_AC_SDTV || t->s_servicetype == ST_AC_HDTV; } /** * */ int service_is_radio(service_t *t) { return t->s_servicetype == ST_RADIO; } /** * */ void service_set_streaming_status_flags(service_t *t, int set) { int n; streaming_message_t *sm; lock_assert(&t->s_stream_mutex); n = t->s_streaming_status; n |= set; if(n == t->s_streaming_status) return; // Already set t->s_streaming_status = n; tvhlog(LOG_DEBUG, "Service", "%s: Status changed to %s%s%s%s%s%s%s", service_nicename(t), n & TSS_INPUT_HARDWARE ? "[Hardware input] " : "", n & TSS_INPUT_SERVICE ? "[Input on service] " : "", n & TSS_MUX_PACKETS ? "[Demuxed packets] " : "", n & TSS_PACKETS ? "[Reassembled packets] " : "", n & TSS_NO_DESCRAMBLER ? "[No available descrambler] " : "", n & TSS_NO_ACCESS ? "[No access] " : "", n & TSS_GRACEPERIOD ? "[Graceperiod expired] " : ""); sm = streaming_msg_create_code(SMT_SERVICE_STATUS, t->s_streaming_status); streaming_pad_deliver(&t->s_streaming_pad, sm); streaming_msg_free(sm); pthread_cond_broadcast(&t->s_tss_cond); } /** * Restart output on a service. * Happens if the stream composition changes. * (i.e. an AC3 stream disappears, etc) */ void service_restart(service_t *t, int had_components) { streaming_message_t *sm; lock_assert(&t->s_stream_mutex); if(had_components) { sm = streaming_msg_create_code(SMT_STOP, SM_CODE_SOURCE_RECONFIGURED); streaming_pad_deliver(&t->s_streaming_pad, sm); streaming_msg_free(sm); } if(t->s_refresh_feed != NULL) t->s_refresh_feed(t); if(TAILQ_FIRST(&t->s_components) != NULL) { sm = streaming_msg_create_data(SMT_START, service_build_stream_start(t)); streaming_pad_deliver(&t->s_streaming_pad, sm); streaming_msg_free(sm); } } /** * Generate a message containing info about all components */ streaming_start_t * service_build_stream_start(service_t *t) { elementary_stream_t *st; int n = 0; streaming_start_t *ss; lock_assert(&t->s_stream_mutex); TAILQ_FOREACH(st, &t->s_components, es_link) n++; ss = calloc(1, sizeof(streaming_start_t) + sizeof(streaming_start_component_t) * n); ss->ss_num_components = n; n = 0; TAILQ_FOREACH(st, &t->s_components, es_link) { streaming_start_component_t *ssc = &ss->ss_components[n++]; ssc->ssc_index = st->es_index; ssc->ssc_type = st->es_type; memcpy(ssc->ssc_lang, st->es_lang, 4); ssc->ssc_composition_id = st->es_composition_id; ssc->ssc_ancillary_id = st->es_ancillary_id; ssc->ssc_pid = st->es_pid; ssc->ssc_width = st->es_width; ssc->ssc_height = st->es_height; ssc->ssc_frameduration = st->es_frame_duration; } t->s_setsourceinfo(t, &ss->ss_si); ss->ss_refcount = 1; ss->ss_pcr_pid = t->s_pcr_pid; ss->ss_pmt_pid = t->s_pmt_pid; return ss; } /** * */ void service_set_enable(service_t *t, int enabled) { if(t->s_enabled == enabled) return; t->s_enabled = enabled; t->s_config_save(t); subscription_reschedule(); } static pthread_mutex_t pending_save_mutex; static pthread_cond_t pending_save_cond; static struct service_queue pending_save_queue; /** * */ void service_request_save(service_t *t, int restart) { pthread_mutex_lock(&pending_save_mutex); if(!t->s_ps_onqueue) { t->s_ps_onqueue = 1 + !!restart; TAILQ_INSERT_TAIL(&pending_save_queue, t, s_ps_link); service_ref(t); pthread_cond_signal(&pending_save_cond); } else if(restart) { t->s_ps_onqueue = 2; // upgrade to restart too } pthread_mutex_unlock(&pending_save_mutex); } /** * */ static void * service_saver(void *aux) { service_t *t; int restart; pthread_mutex_lock(&pending_save_mutex); while(1) { if((t = TAILQ_FIRST(&pending_save_queue)) == NULL) { pthread_cond_wait(&pending_save_cond, &pending_save_mutex); continue; } assert(t->s_ps_onqueue != 0); restart = t->s_ps_onqueue == 2; TAILQ_REMOVE(&pending_save_queue, t, s_ps_link); t->s_ps_onqueue = 0; pthread_mutex_unlock(&pending_save_mutex); pthread_mutex_lock(&global_lock); if(t->s_status != SERVICE_ZOMBIE) t->s_config_save(t); if(t->s_status == SERVICE_RUNNING && restart) { pthread_mutex_lock(&t->s_stream_mutex); service_restart(t, 1); pthread_mutex_unlock(&t->s_stream_mutex); } service_unref(t); pthread_mutex_unlock(&global_lock); pthread_mutex_lock(&pending_save_mutex); } return NULL; } /** * */ void service_init(void) { pthread_t tid; TAILQ_INIT(&pending_save_queue); pthread_mutex_init(&pending_save_mutex, NULL); pthread_cond_init(&pending_save_cond, NULL); pthread_create(&tid, NULL, service_saver, NULL); } /** * */ void service_source_info_free(struct source_info *si) { free(si->si_device); free(si->si_adapter); free(si->si_network); free(si->si_mux); free(si->si_provider); free(si->si_service); } void service_source_info_copy(source_info_t *dst, const source_info_t *src) { #define COPY(x) dst->si_##x = src->si_##x ? strdup(src->si_##x) : NULL COPY(device); COPY(adapter); COPY(network); COPY(mux); COPY(provider); COPY(service); #undef COPY } /** * */ const char * service_nicename(service_t *t) { return t->s_nicename; } const char * service_component_nicename(elementary_stream_t *st) { return st->es_nicename; } const char * service_adapter_nicename(service_t *t) { return "Adapter"; } const char * service_tss2text(int flags) { if(flags & TSS_NO_ACCESS) return "No access"; if(flags & TSS_NO_DESCRAMBLER) return "No descrambler"; if(flags & TSS_PACKETS) return "Got valid packets"; if(flags & TSS_MUX_PACKETS) return "Got multiplexed packets but could not decode further"; if(flags & TSS_INPUT_SERVICE) return "Got packets for this service but could not decode further"; if(flags & TSS_INPUT_HARDWARE) return "Sensed input from hardware but nothing for the service"; if(flags & TSS_GRACEPERIOD) return "No input detected"; 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 service_refresh_channel(service_t *t) { if(t->s_ch != NULL) htsp_channel_update(t->s_ch); } /** * */ static int si_cmp(const service_instance_t *a, const service_instance_t *b) { return a->si_prio - b->si_prio; } /** * */ service_instance_t * service_instance_add(struct service_instance_list *sil, struct service *s, int instance, int prio, int weight) { service_instance_t *si; LIST_FOREACH(si, sil, si_link) if(si->si_s == s && si->si_instance == instance) break; if(si == NULL) { si = calloc(1, sizeof(service_instance_t)); si->si_s = s; service_ref(s); si->si_instance = instance; si->si_weight = weight; } else { si->si_mark = 0; if(si->si_prio == prio && si->si_weight == weight) return si; LIST_REMOVE(si, si_link); } si->si_weight = weight; si->si_prio = prio; LIST_INSERT_SORTED(sil, si, si_link, si_cmp); return si; } /** * */ void service_instance_destroy(service_instance_t *si) { LIST_REMOVE(si, si_link); service_unref(si->si_s); free(si); } /** * */ void service_instance_list_clear(struct service_instance_list *sil) { lock_assert(&global_lock); service_instance_t *si; while((si = LIST_FIRST(sil)) != NULL) service_instance_destroy(si); } /** * Get the encryption CAID from a service * only the first CA stream in a service is returned */ uint16_t service_get_encryption(service_t *t) { elementary_stream_t *st; caid_t *c; TAILQ_FOREACH(st, &t->s_components, es_link) { switch(st->es_type) { case SCT_CA: LIST_FOREACH(c, &st->es_caids, link) if(c->caid != 0) return c->caid; break; default: break; } } return 0; } /* * Find the primary EPG service (to stop EPG trying to update * from multiple OTA sources) */ int service_is_primary_epg(service_t *svc) { service_t *ret = NULL, *t; if (!svc || !svc->s_ch) return 0; LIST_FOREACH(t, &svc->s_ch->ch_services, s_ch_link) { if (!t->s_enabled || !t->s_dvb_eit_enable) continue; if (!ret) ret = t; } return !ret ? 0 : (ret->s_dvb_service_id == svc->s_dvb_service_id); } /* * list of known service types */ htsmsg_t *servicetype_list ( void ) { htsmsg_t *ret, *e; int i; ret = htsmsg_create_list(); for (i = 0; i < sizeof(stypetab) / sizeof(stypetab[0]); i++ ) { e = htsmsg_create_map(); htsmsg_add_u32(e, "val", stypetab[i].val); htsmsg_add_str(e, "str", stypetab[i].str); htsmsg_add_msg(ret, NULL, e); } return ret; }