From 84c1c05d73232d4d0e34bbd587892bb019f0abb5 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Fri, 10 May 2013 15:10:54 +0100 Subject: [PATCH 01/10] dvb: fix mistake in NIT processing. This was stopping some DVB-C networks from being properly scanned. --- src/dvb/dvb_tables.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dvb/dvb_tables.c b/src/dvb/dvb_tables.c index 2cce820b..037fbbbb 100644 --- a/src/dvb/dvb_tables.c +++ b/src/dvb/dvb_tables.c @@ -894,13 +894,13 @@ dvb_nit_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, tvhtrace("nit", "tableid 0x%02x", tableid); tvhlog_hexdump("nit", ptr, len); - /* Ignore other network */ - if(tableid != 0x40) return -1; - - /* Check NID */ - if(tdmi->tdmi_adapter->tda_nitoid && - tdmi->tdmi_adapter->tda_nitoid != network_id) - return -1; + /* Specific NID requested */ + if(tdmi->tdmi_adapter->tda_nitoid) { + if (tdmi->tdmi_adapter->tda_nitoid != network_id) + return -1; + } else if (tableid != 0x40) { + return -1; + } /* Ignore non-current */ if((ptr[2] & 1) == 0) From 60bdb16c9d3a84ac2eed29aef3ae38000447775c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20T=C3=B6rblom?= Date: Thu, 9 May 2013 21:51:27 +0200 Subject: [PATCH 02/10] Added initial support for transcoding. --- Makefile | 3 +- configure | 4 + src/htsp_server.c | 69 +- src/libav.c | 49 ++ src/libav.h | 2 +- src/main.c | 4 + src/plumbing/transcoding.c | 1336 ++++++++++++++++++++++++++++++++++++ src/plumbing/transcoding.h | 42 ++ src/psi.c | 12 +- src/tvheadend.h | 3 +- src/webui/webui.c | 59 ++ 11 files changed, 1578 insertions(+), 5 deletions(-) create mode 100644 src/plumbing/transcoding.c create mode 100644 src/plumbing/transcoding.h diff --git a/Makefile b/Makefile index 00ee4147..b3a6a7ae 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,8 @@ SRCS-$(CONFIG_AVAHI) += src/avahi.c # libav SRCS-$(CONFIG_LIBAV) += src/libav.c \ - src/muxer/muxer_libav.c + src/muxer/muxer_libav.c \ + src/plumbing/transcoding.c \ # CWC SRCS-${CONFIG_CWC} += src/cwc.c \ diff --git a/configure b/configure index 198e4d1b..fcec9de4 100755 --- a/configure +++ b/configure @@ -137,6 +137,10 @@ if enabled_or_auto libav; then has_libav=false fi + if $has_libav && ! check_pkg libswscale ">=0.13.0"; then + has_libav=false + fi + if $has_libav; then enable libav elif enabled libav; then diff --git a/src/htsp_server.c b/src/htsp_server.c index f68d016d..2a670a69 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -46,7 +46,9 @@ #if ENABLE_TIMESHIFT #include "timeshift.h" #endif - +#if ENABLE_LIBAV +#include "plumbing/transcoding.h" +#endif #include #include "settings.h" #include @@ -177,6 +179,10 @@ typedef struct htsp_subscription { streaming_target_t *hs_tshift; #endif +#if ENABLE_LIBAV +streaming_target_t *hs_transcoder; +#endif + htsp_msg_q_t hs_q; time_t hs_last_report; /* Last queue status report sent */ @@ -282,13 +288,23 @@ htsp_subscription_destroy(htsp_connection_t *htsp, htsp_subscription_t *hs) { LIST_REMOVE(hs, hs_link); subscription_unsubscribe(hs->hs_s); + if(hs->hs_tsfix != NULL) tsfix_destroy(hs->hs_tsfix); + +#if ENABLE_LIBAV + if(hs->hs_transcoder != NULL) + transcoder_destroy(hs->hs_transcoder); +#endif + htsp_flush_queue(htsp, &hs->hs_q); + #if ENABLE_TIMESHIFT if(hs->hs_tshift) timeshift_destroy(hs->hs_tshift); #endif + + free(hs); } @@ -1328,6 +1344,32 @@ htsp_method_subscribe(htsp_connection_t *htsp, htsmsg_t *in) normts = 1; } #endif + +#if ENABLE_LIBAV + if (transcoding_enabled) { + transcoder_props_t props; + + props.tp_vcodec = streaming_component_txt2type(htsmsg_get_str(in, "videoCodec")); + props.tp_acodec = streaming_component_txt2type(htsmsg_get_str(in, "audioCodec")); + props.tp_scodec = streaming_component_txt2type(htsmsg_get_str(in, "subtitleCodec")); + + props.tp_resolution = htsmsg_get_u32_or_default(in, "maxResolution", 0); + props.tp_channels = htsmsg_get_u32_or_default(in, "channels", 0); + props.tp_bandwidth = htsmsg_get_u32_or_default(in, "bandwidth", 0); + + if ((str = htsmsg_get_str(in, "language"))) + strncpy(props.tp_language, str, 3); + + if(props.tp_vcodec != SCT_UNKNOWN || + props.tp_acodec != SCT_UNKNOWN || + props.tp_scodec != SCT_UNKNOWN) { + st = hs->hs_transcoder = transcoder_create(st); + transcoder_set_properties(st, &props); + normts = 1; + } + } +#endif + if(normts) st = hs->hs_tsfix = tsfix_create(st); @@ -1640,6 +1682,28 @@ htsp_method_file_seek(htsp_connection_t *htsp, htsmsg_t *in) return rep; } + +#if ENABLE_LIBAV +/** + * + */ +static htsmsg_t * +htsp_method_getCodecs(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsmsg_t *out, *l; + + l = htsmsg_create_list(); + transcoder_get_capabilities(l); + + out = htsmsg_create_map(); + + htsmsg_add_msg(out, "encoders", l); + + return out; +} +#endif + + /** * HTSP methods */ @@ -1669,6 +1733,9 @@ struct { { "subscriptionSkip", htsp_method_skip, ACCESS_STREAMING}, { "subscriptionSpeed", htsp_method_speed, ACCESS_STREAMING}, { "subscriptionLive", htsp_method_live, ACCESS_STREAMING}, +#if ENABLE_LIBAV + { "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}, diff --git a/src/libav.c b/src/libav.c index e867eea4..7934c52e 100644 --- a/src/libav.c +++ b/src/libav.c @@ -91,6 +91,55 @@ streaming_component_type2codec_id(streaming_component_type_t type) return codec_id; } + +/** + * Translate a libavcodec id to a component type + */ +streaming_component_type_t +codec_id2streaming_component_type(enum CodecID id) +{ + streaming_component_type_t type = CODEC_ID_NONE; + + switch(id) { + case CODEC_ID_H264: + type = SCT_H264; + break; + case CODEC_ID_MPEG2VIDEO: + type = SCT_MPEG2VIDEO; + break; + case CODEC_ID_AC3: + type = SCT_AC3; + break; + case CODEC_ID_EAC3: + type = SCT_EAC3; + break; + case CODEC_ID_AAC: + type = SCT_AAC; + break; + case CODEC_ID_MP2: + type = SCT_MPEG2AUDIO; + break; + case CODEC_ID_DVB_SUBTITLE: + type = SCT_DVBSUB; + break; + case CODEC_ID_TEXT: + type = SCT_TEXTSUB; + break; + case CODEC_ID_DVB_TELETEXT: + type = SCT_TELETEXT; + break; + case CODEC_ID_NONE: + type = SCT_NONE; + break; + default: + type = SCT_UNKNOWN; + break; + } + + return type; +} + + /** * */ diff --git a/src/libav.h b/src/libav.h index c8a6ed7a..98856f90 100644 --- a/src/libav.h +++ b/src/libav.h @@ -24,7 +24,7 @@ #include "tvheadend.h" enum CodecID streaming_component_type2codec_id(streaming_component_type_t type); - +streaming_component_type_t codec_id2streaming_component_type(enum CodecID id); void libav_init(void); #endif diff --git a/src/main.c b/src/main.c index 8e2d7ec8..8a2323f8 100644 --- a/src/main.c +++ b/src/main.c @@ -64,6 +64,7 @@ #include "timeshift.h" #if ENABLE_LIBAV #include "libav.h" +#include "plumbing/transcoding.h" #endif /* Command line option struct */ @@ -126,6 +127,9 @@ const tvh_caps_t tvheadend_capabilities[] = { #if ENABLE_LINUXDVB { "linuxdvb", NULL }, #endif +#if ENABLE_LIBAV + { "transcoding", &transcoding_enabled }, +#endif #if ENABLE_IMAGECACHE { "imagecache", &imagecache_enabled }, #endif diff --git a/src/plumbing/transcoding.c b/src/plumbing/transcoding.c new file mode 100644 index 00000000..4cb48f67 --- /dev/null +++ b/src/plumbing/transcoding.c @@ -0,0 +1,1336 @@ +/** + * Transcoding + * Copyright (C) 2013 John Törnblom + * + * 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 "tvheadend.h" +#include "streaming.h" +#include "service.h" +#include "packet.h" +#include "transcoding.h" +#include "libav.h" + +LIST_HEAD(transcoder_stream_list, transcoder_stream); + +typedef struct transcoder_stream { + int ts_index; + streaming_component_type_t ts_type; + streaming_target_t *ts_target; + LIST_ENTRY(transcoder_stream) ts_link; + + void (*ts_handle_pkt) (struct transcoder_stream *, th_pkt_t *); + void (*ts_destroy) (struct transcoder_stream *); +} transcoder_stream_t; + + +typedef struct audio_stream { + transcoder_stream_t; + + AVCodecContext *aud_ictx; + AVCodec *aud_icodec; + + AVCodecContext *aud_octx; + AVCodec *aud_ocodec; + + uint8_t *aud_dec_sample; + uint32_t aud_dec_size; + uint32_t aud_dec_offset; + + uint8_t *aud_enc_sample; + uint32_t aud_enc_size; + + uint64_t aud_dec_pts; + uint64_t aud_enc_pts; + + int8_t aud_channels; + int32_t aud_bitrate; +} audio_stream_t; + + +typedef struct video_stream { + transcoder_stream_t; + + AVCodecContext *vid_ictx; + AVCodec *vid_icodec; + + AVCodecContext *vid_octx; + AVCodec *vid_ocodec; + + AVFrame *vid_dec_frame; + struct SwsContext *vid_scaler; + AVFrame *vid_enc_frame; + + int16_t vid_width; + int16_t vid_height; +} video_stream_t; + + +typedef struct subtitle_stream { + transcoder_stream_t; + + AVCodecContext *sub_ictx; + AVCodec *sub_icodec; + + AVCodecContext *sub_octx; + AVCodec *sub_ocodec; +} subtitle_stream_t; + + + +typedef struct transcoder { + streaming_target_t t_input; // must be first + streaming_target_t *t_output; + + transcoder_props_t t_props; + struct transcoder_stream_list t_stream_list; +} transcoder_t; + + + +#define WORKING_ENCODER(x) (x == CODEC_ID_H264 || x == CODEC_ID_MPEG2VIDEO || \ + x == CODEC_ID_AAC || x == CODEC_ID_MP2) + + +uint32_t transcoding_enabled = 0; + +/** + * + */ +static AVCodec * +transcoder_get_decoder(streaming_component_type_t ty) +{ + enum CodecID codec_id; + AVCodec *codec; + + codec_id = streaming_component_type2codec_id(ty); + if (codec_id == CODEC_ID_NONE) { + tvhlog(LOG_ERR, "transcode", "Unsupported input codec %s", + streaming_component_type2txt(ty)); + return NULL; + } + + codec = avcodec_find_decoder(codec_id); + if (!codec) { + tvhlog(LOG_ERR, "transcode", "Unable to find %s decoder", + streaming_component_type2txt(ty)); + return NULL; + } + + tvhlog(LOG_DEBUG, "transcode", "Using decoder %s", codec->name); + + return codec; +} + + +/** + * + */ +static AVCodec * +transcoder_get_encoder(streaming_component_type_t ty) +{ + enum CodecID codec_id; + AVCodec *codec; + + codec_id = streaming_component_type2codec_id(ty); + if (codec_id == CODEC_ID_NONE) { + tvhlog(LOG_ERR, "transcode", "Unable to find %s codec", + streaming_component_type2txt(ty)); + return NULL; + } + + if (!WORKING_ENCODER(codec_id)) { + tvhlog(LOG_WARNING, "transcode", "Unsupported output codec %s", + streaming_component_type2txt(ty)); + return NULL; + } + + codec = avcodec_find_encoder(codec_id); + if (!codec) { + tvhlog(LOG_ERR, "transcode", "Unable to find %s encoder", + streaming_component_type2txt(ty)); + return NULL; + } + + tvhlog(LOG_DEBUG, "transcode", "Using encoder %s", codec->name); + + return codec; +} + + +/** + * + */ +static void +transcoder_stream_packet(transcoder_stream_t *ts, th_pkt_t *pkt) +{ + streaming_message_t *sm; + + sm = streaming_msg_create_pkt(pkt); + streaming_target_deliver2(ts->ts_target, sm); +} + + +/** + * + */ +static void +transcoder_stream_subtitle(transcoder_stream_t *ts, th_pkt_t *pkt) +{ + //streaming_message_t *sm; + AVCodec *icodec; + AVCodecContext *ictx; + AVPacket packet; + AVSubtitle sub; + int length, got_subtitle; + + subtitle_stream_t *ss = (subtitle_stream_t*)ts; + + ictx = ss->sub_ictx; + //octx = ss->sub_octx; + + icodec = ss->sub_icodec; + //ocodec = ss->sub_ocodec; + + if (ictx->codec_id == CODEC_ID_NONE) { + ictx->codec_id = icodec->id; + + if (avcodec_open(ictx, icodec) < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to open %s decoder", icodec->name); + ts->ts_index = 0; + goto cleanup; + } + } + + av_init_packet(&packet); + packet.data = pktbuf_ptr(pkt->pkt_payload); + packet.size = pktbuf_len(pkt->pkt_payload); + packet.pts = pkt->pkt_pts; + packet.dts = pkt->pkt_dts; + packet.duration = pkt->pkt_duration; + + length = avcodec_decode_subtitle2(ictx, &sub, &got_subtitle, &packet); + if (length <= 0) { + tvhlog(LOG_ERR, "transcode", "Unable to decode subtitle (%d)", length); + ts->ts_index = 0; + goto cleanup; + } + + if (!got_subtitle) + goto cleanup; + + //TODO: encoding + + cleanup: + av_free_packet(&packet); + avsubtitle_free(&sub); +} + + +/** + * + */ +static void +transcoder_stream_audio(transcoder_stream_t *ts, th_pkt_t *pkt) +{ + AVCodec *icodec, *ocodec; + AVCodecContext *ictx, *octx; + AVPacket packet; + int length, len, i; + uint32_t frame_bytes; + short *samples; + streaming_message_t *sm; + th_pkt_t *n; + audio_stream_t *as = (audio_stream_t*)ts; + + ictx = as->aud_ictx; + octx = as->aud_octx; + + icodec = as->aud_icodec; + ocodec = as->aud_ocodec; + + if (ictx->codec_id == CODEC_ID_NONE) { + ictx->codec_id = icodec->id; + + if (avcodec_open(ictx, icodec) < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to open %s decoder", icodec->name); + ts->ts_index = 0; + goto cleanup; + } + + as->aud_dec_pts = pkt->pkt_pts; + } + + if (pkt->pkt_pts > as->aud_dec_pts) { + tvhlog(LOG_WARNING, "transcode", "Detected framedrop in audio"); + as->aud_enc_pts += (pkt->pkt_pts - as->aud_dec_pts); + } + + pkt = pkt_merge_header(pkt); + + av_init_packet(&packet); + packet.data = pktbuf_ptr(pkt->pkt_payload); + packet.size = pktbuf_len(pkt->pkt_payload); + packet.pts = pkt->pkt_pts; + packet.dts = pkt->pkt_dts; + packet.duration = pkt->pkt_duration; + + if ((len = as->aud_dec_size - as->aud_dec_offset) <= 0) { + tvhlog(LOG_ERR, "transcode", "Decoder buffer overflow"); + ts->ts_index = 0; + goto cleanup; + } + + samples = (short*)(as->aud_dec_sample + as->aud_dec_offset); + if ((length = avcodec_decode_audio3(ictx, samples, &len, &packet)) <= 0) { + tvhlog(LOG_ERR, "transcode", "Unable to decode audio (%d)", length); + ts->ts_index = 0; + goto cleanup; + } + + as->aud_dec_pts += pkt->pkt_duration; + as->aud_dec_offset += len; + + + octx->sample_rate = ictx->sample_rate; + octx->sample_fmt = ictx->sample_fmt; + + octx->time_base.den = 90000; + octx->time_base.num = 1; + + octx->channels = as->aud_channels ? as->aud_channels : ictx->channels; + octx->bit_rate = as->aud_bitrate ? as->aud_bitrate : ictx->bit_rate; + + octx->channels = MIN(octx->channels, ictx->channels); + octx->bit_rate = MIN(octx->bit_rate, ictx->bit_rate); + + switch (octx->channels) { + case 1: + octx->channel_layout = AV_CH_LAYOUT_MONO; + break; + + case 2: + octx->channel_layout = AV_CH_LAYOUT_STEREO; + break; + + case 3: + octx->channel_layout = AV_CH_LAYOUT_SURROUND; + break; + + case 4: + octx->channel_layout = AV_CH_LAYOUT_QUAD; + break; + + case 5: + octx->channel_layout = AV_CH_LAYOUT_5POINT0; + break; + + case 6: + octx->channel_layout = AV_CH_LAYOUT_5POINT1; + break; + + case 7: + octx->channel_layout = AV_CH_LAYOUT_6POINT1; + break; + + case 8: + octx->channel_layout = AV_CH_LAYOUT_7POINT1; + break; + + default: + break; + } + + switch (ts->ts_type) { + case SCT_MPEG2AUDIO: + octx->channels = MIN(octx->channels, 2); + if (octx->channels == 1) + octx->channel_layout = AV_CH_LAYOUT_MONO; + else + octx->channel_layout = AV_CH_LAYOUT_STEREO; + + break; + + case SCT_AAC: + octx->global_quality = 4*FF_QP2LAMBDA; + octx->flags |= CODEC_FLAG_QSCALE; + break; + + default: + break; + } + + if (octx->codec_id == CODEC_ID_NONE) { + octx->codec_id = ocodec->id; + + if (avcodec_open(octx, ocodec) < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to open %s encoder", ocodec->name); + ts->ts_index = 0; + goto cleanup; + } + } + + frame_bytes = av_get_bytes_per_sample(octx->sample_fmt) * + octx->frame_size * + octx->channels; + + len = as->aud_dec_offset; + + for (i = 0; i <= (len - frame_bytes); i += frame_bytes) { + length = avcodec_encode_audio(octx, + as->aud_enc_sample, + as->aud_enc_size, + (short *)(as->aud_dec_sample + i)); + if (length < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to encode audio (%d)", length); + ts->ts_index = 0; + goto cleanup; + + } else if (length) { + n = pkt_alloc(as->aud_enc_sample, length, as->aud_enc_pts, as->aud_enc_pts); + n->pkt_componentindex = ts->ts_index; + n->pkt_frametype = pkt->pkt_frametype; + n->pkt_channels = octx->channels; + n->pkt_sri = pkt->pkt_sri; + + if (octx->coded_frame && octx->coded_frame->pts != AV_NOPTS_VALUE) + n->pkt_duration = octx->coded_frame->pts - as->aud_enc_pts; + else + n->pkt_duration = frame_bytes*90000 / (2 * octx->channels * octx->sample_rate); + + as->aud_enc_pts += n->pkt_duration; + + if (octx->extradata_size) + n->pkt_header = pktbuf_alloc(octx->extradata, octx->extradata_size); + + sm = streaming_msg_create_pkt(n); + streaming_target_deliver2(ts->ts_target, sm); + pkt_ref_dec(n); + } + + as->aud_dec_offset -= frame_bytes; + } + + if (as->aud_dec_offset) + memmove(as->aud_dec_sample, as->aud_dec_sample + len - as->aud_dec_offset, + as->aud_dec_offset); + + cleanup: + av_free_packet(&packet); +} + + +/** + * + */ +static void +transcoder_stream_video(transcoder_stream_t *ts, th_pkt_t *pkt) +{ + AVCodec *icodec, *ocodec; + AVCodecContext *ictx, *octx; + AVDictionary *opts; + AVPacket packet; + AVPicture deint_pic; + uint8_t *buf, *out, *deint; + int length, len, got_picture; + streaming_message_t *sm; + th_pkt_t *n; + video_stream_t *vs = (video_stream_t*)ts; + + ictx = vs->vid_ictx; + octx = vs->vid_octx; + + icodec = vs->vid_icodec; + ocodec = vs->vid_ocodec; + + buf = out = deint = NULL; + opts = NULL; + + if (ictx->codec_id == CODEC_ID_NONE) { + ictx->codec_id = icodec->id; + + if (avcodec_open(ictx, icodec) < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to open %s decoder", icodec->name); + ts->ts_index = 0; + goto cleanup; + } + } + + pkt = pkt_merge_header(pkt); + + av_init_packet(&packet); + packet.data = pktbuf_ptr(pkt->pkt_payload); + packet.size = pktbuf_len(pkt->pkt_payload); + packet.pts = pkt->pkt_pts; + packet.dts = pkt->pkt_dts; + packet.duration = pkt->pkt_duration; + + vs->vid_enc_frame->pts = packet.pts; + vs->vid_enc_frame->pkt_dts = packet.dts; + vs->vid_enc_frame->pkt_pts = packet.pts; + + vs->vid_dec_frame->pts = packet.pts; + vs->vid_dec_frame->pkt_dts = packet.dts; + vs->vid_dec_frame->pkt_pts = packet.pts; + + ictx->reordered_opaque = packet.pts; + + length = avcodec_decode_video2(ictx, vs->vid_dec_frame, &got_picture, &packet); + if (length <= 0) { + tvhlog(LOG_ERR, "transcode", "Unable to decode video (%d)", length); + ts->ts_index = 0; + goto cleanup; + } + + if (!got_picture) + goto cleanup; + + octx->sample_aspect_ratio.num = ictx->sample_aspect_ratio.num; + octx->sample_aspect_ratio.den = ictx->sample_aspect_ratio.den; + + vs->vid_enc_frame->sample_aspect_ratio.num = vs->vid_dec_frame->sample_aspect_ratio.num; + vs->vid_enc_frame->sample_aspect_ratio.den = vs->vid_dec_frame->sample_aspect_ratio.den; + + if(octx->codec_id == CODEC_ID_NONE) { + // Common settings + octx->width = vs->vid_width ? vs->vid_width : ictx->width; + octx->height = vs->vid_height ? vs->vid_height : ictx->height; + octx->gop_size = 25; + octx->time_base.den = 25; + octx->time_base.num = 1; + octx->has_b_frames = ictx->has_b_frames; + + switch (ts->ts_type) { + case SCT_MPEG2VIDEO: + octx->codec_id = CODEC_ID_MPEG2VIDEO; + octx->pix_fmt = PIX_FMT_YUV420P; + octx->flags |= CODEC_FLAG_GLOBAL_HEADER; + + octx->qmin = 1; + octx->qmax = FF_LAMBDA_MAX; + + octx->bit_rate = 2 * octx->width * octx->height; + octx->rc_max_rate = 4 * octx->bit_rate; + octx->rc_buffer_size = 2 * octx->rc_max_rate; + break; + + case SCT_H264: + octx->codec_id = CODEC_ID_H264; + octx->pix_fmt = PIX_FMT_YUV420P; + octx->flags |= CODEC_FLAG_GLOBAL_HEADER; + + // Qscale difference between I-frames and P-frames. + // Note: -i_qfactor is handled a little differently than --ipratio. + // Recommended: -i_qfactor 0.71 + octx->i_quant_factor = 0.71; + + // QP curve compression: 0.0 => CBR, 1.0 => CQP. + // Recommended default: -qcomp 0.60 + octx->qcompress = 0.6; + + // Minimum quantizer. Doesn't need to be changed. + // Recommended default: -qmin 10 + octx->qmin = 10; + + // Maximum quantizer. Doesn't need to be changed. + // Recommended default: -qmax 51 + octx->qmax = 30; + + av_dict_set(&opts, "preset", "medium", 0); + av_dict_set(&opts, "profile", "baseline", 0); + + octx->bit_rate = 2 * octx->width * octx->height; + octx->rc_buffer_size = 8 * 1024 * 224; + octx->rc_max_rate = 2 * octx->rc_buffer_size; + break; + + default: + break; + } + + octx->codec_id = ocodec->id; + + if (avcodec_open2(octx, ocodec, &opts) < 0) { + tvhlog(LOG_ERR, "transcode", "Unable to open %s encoder", ocodec->name); + ts->ts_index = 0; + goto cleanup; + } + } + + len = avpicture_get_size(ictx->pix_fmt, ictx->width, ictx->height); + deint = av_malloc(len); + + avpicture_fill(&deint_pic, + deint, + ictx->pix_fmt, + ictx->width, + ictx->height); + + if (avpicture_deinterlace(&deint_pic, + (AVPicture *)vs->vid_dec_frame, + ictx->pix_fmt, + ictx->width, + ictx->height) < 0) { + tvhlog(LOG_ERR, "transcode", "Cannot deinterlace frame"); + ts->ts_index = 0; + goto cleanup; + } + + len = avpicture_get_size(octx->pix_fmt, octx->width, octx->height); + buf = av_malloc(len + FF_INPUT_BUFFER_PADDING_SIZE); + memset(buf, 0, len); + + avpicture_fill((AVPicture *)vs->vid_enc_frame, + buf, + octx->pix_fmt, + octx->width, + octx->height); + + vs->vid_scaler = sws_getCachedContext(vs->vid_scaler, + ictx->width, + ictx->height, + ictx->pix_fmt, + octx->width, + octx->height, + octx->pix_fmt, + 1, + NULL, + NULL, + NULL); + + if (sws_scale(vs->vid_scaler, + (const uint8_t * const*)deint_pic.data, + deint_pic.linesize, + 0, + ictx->height, + vs->vid_enc_frame->data, + vs->vid_enc_frame->linesize) < 0) { + tvhlog(LOG_ERR, "transcode", "Cannot scale frame"); + ts->ts_index = 0; + goto cleanup; + } + + + len = avpicture_get_size(octx->pix_fmt, ictx->width, ictx->height); + out = av_malloc(len + FF_INPUT_BUFFER_PADDING_SIZE); + memset(out, 0, len); + + vs->vid_enc_frame->pkt_pts = vs->vid_dec_frame->pkt_pts; + vs->vid_enc_frame->pkt_dts = vs->vid_dec_frame->pkt_dts; + + if (vs->vid_dec_frame->reordered_opaque != AV_NOPTS_VALUE) + vs->vid_enc_frame->pts = vs->vid_dec_frame->reordered_opaque; + + else if (ictx->coded_frame && ictx->coded_frame->pts != AV_NOPTS_VALUE) + vs->vid_enc_frame->pts = vs->vid_dec_frame->pts; + + length = avcodec_encode_video(octx, out, len, vs->vid_enc_frame); + if (length <= 0) { + if (length) { + tvhlog(LOG_ERR, "transcode", "Unable to encode video (%d)", length); + ts->ts_index = 0; + } + + goto cleanup; + } + + if (!octx->coded_frame) + goto cleanup; + + n = pkt_alloc(out, length, octx->coded_frame->pkt_pts, octx->coded_frame->pkt_dts); + + switch (octx->coded_frame->pict_type) { + case AV_PICTURE_TYPE_I: + n->pkt_frametype = PKT_I_FRAME; + break; + + case AV_PICTURE_TYPE_P: + n->pkt_frametype = PKT_P_FRAME; + break; + + case AV_PICTURE_TYPE_B: + n->pkt_frametype = PKT_B_FRAME; + break; + + default: + break; + } + + n->pkt_duration = pkt->pkt_duration; + n->pkt_commercial = pkt->pkt_commercial; + n->pkt_componentindex = pkt->pkt_componentindex; + n->pkt_field = pkt->pkt_field; + n->pkt_aspect_num = pkt->pkt_aspect_num; + n->pkt_aspect_den = pkt->pkt_aspect_den; + + if(octx->coded_frame && octx->coded_frame->pts != AV_NOPTS_VALUE) { + if(n->pkt_dts != PTS_UNSET) + n->pkt_dts -= n->pkt_pts; + + n->pkt_pts = octx->coded_frame->pts; + + if(n->pkt_dts != PTS_UNSET) + n->pkt_dts += n->pkt_pts; + } + + if (octx->extradata_size) + n->pkt_header = pktbuf_alloc(octx->extradata, octx->extradata_size); + + sm = streaming_msg_create_pkt(n); + streaming_target_deliver2(ts->ts_target, sm); + pkt_ref_dec(n); + + cleanup: + av_free_packet(&packet); + + if(buf) + av_free(buf); + + if(out) + av_free(out); + + if(deint) + av_free(deint); + + if(opts) + av_dict_free(&opts); +} + + +/** + * + */ +static void +transcoder_packet(transcoder_t *t, th_pkt_t *pkt) +{ + transcoder_stream_t *ts; + + LIST_FOREACH(ts, &t->t_stream_list, ts_link) { + if (pkt->pkt_componentindex != ts->ts_index) + continue; + + ts->ts_handle_pkt(ts, pkt); + break; + } +} + + +/** + * + */ +static void +transcoder_destroy_stream(transcoder_stream_t *ts) +{ + free(ts); +} + + +/** + * + */ +static int +transcoder_init_stream(transcoder_t *t, streaming_start_component_t *ssc) +{ + transcoder_stream_t *ts = calloc(1, sizeof(transcoder_stream_t)); + + ts->ts_index = ssc->ssc_index; + ts->ts_type = ssc->ssc_type; + ts->ts_target = t->t_output; + ts->ts_handle_pkt = transcoder_stream_packet; + ts->ts_destroy = transcoder_destroy_stream; + + LIST_INSERT_HEAD(&t->t_stream_list, ts, ts_link); + + if(ssc->ssc_gh) + pktbuf_ref_inc(ssc->ssc_gh); + + tvhlog(LOG_INFO, "transcode", "%d:%s ==> Passthrough", + ssc->ssc_index, + streaming_component_type2txt(ssc->ssc_type)); + + return 1; +} + + +/** + * + */ +static void +transcoder_destroy_subtitle(transcoder_stream_t *ts) +{ + subtitle_stream_t *ss = (subtitle_stream_t*)ts; + + if(ss->sub_ictx) { + avcodec_close(ss->sub_ictx); + av_free(ss->sub_ictx); + } + + if(ss->sub_octx) { + avcodec_close(ss->sub_octx); + av_free(ss->sub_octx); + } + + free(ts); +} + + +/** + * + */ +static int +transcoder_init_subtitle(transcoder_t *t, streaming_start_component_t *ssc) +{ + subtitle_stream_t *ss; + AVCodec *icodec, *ocodec; + transcoder_props_t *tp = &t->t_props; + + if (tp->tp_scodec == SCT_NONE) + return 0; + + else if (tp->tp_scodec == SCT_UNKNOWN) + return transcoder_init_stream(t, ssc); + + else if (!(icodec = transcoder_get_decoder(ssc->ssc_type))) + return transcoder_init_stream(t, ssc); + + else if (!(ocodec = transcoder_get_encoder(tp->tp_scodec))) + return transcoder_init_stream(t, ssc); + + if (tp->tp_scodec == ssc->ssc_type) + return transcoder_init_stream(t, ssc); + + ss = calloc(1, sizeof(subtitle_stream_t)); + + ss->ts_index = ssc->ssc_index; + ss->ts_type = tp->tp_scodec; + ss->ts_target = t->t_output; + ss->ts_handle_pkt = transcoder_stream_subtitle; + ss->ts_destroy = transcoder_destroy_subtitle; + + ss->sub_icodec = icodec; + ss->sub_ocodec = ocodec; + + ss->sub_ictx = avcodec_alloc_context(); + ss->sub_octx = avcodec_alloc_context(); + + ss->sub_ictx->codec_type = AVMEDIA_TYPE_SUBTITLE; + ss->sub_octx->codec_type = AVMEDIA_TYPE_SUBTITLE; + + avcodec_get_context_defaults3(ss->sub_ictx, icodec); + avcodec_get_context_defaults3(ss->sub_octx, ocodec); + + LIST_INSERT_HEAD(&t->t_stream_list, (transcoder_stream_t*)ss, ts_link); + + tvhlog(LOG_INFO, "transcode", "%d:%s ==> %s", + ssc->ssc_index, + streaming_component_type2txt(ssc->ssc_type), + streaming_component_type2txt(ss->ts_type)); + + ssc->ssc_type = tp->tp_scodec; + ssc->ssc_gh = NULL; + + return 1; +} + + +/** + * + */ +static void +transcoder_destroy_audio(transcoder_stream_t *ts) +{ + audio_stream_t *as = (audio_stream_t*)ts; + + if(as->aud_ictx) { + avcodec_close(as->aud_ictx); + av_free(as->aud_ictx); + } + + if(as->aud_octx) { + avcodec_close(as->aud_octx); + av_free(as->aud_octx); + } + + if(as->aud_dec_sample) + av_free(as->aud_dec_sample); + + if(as->aud_enc_sample) + av_free(as->aud_enc_sample); + + free(ts); +} + + +/** + * + */ +static int +transcoder_init_audio(transcoder_t *t, streaming_start_component_t *ssc) +{ + audio_stream_t *as; + transcoder_stream_t *ts; + AVCodec *icodec, *ocodec; + transcoder_props_t *tp = &t->t_props; + + if (tp->tp_acodec == SCT_NONE) + return 0; + + else if (tp->tp_acodec == SCT_UNKNOWN) + return transcoder_init_stream(t, ssc); + + else if (!(icodec = transcoder_get_decoder(ssc->ssc_type))) + return transcoder_init_stream(t, ssc); + + else if (!(ocodec = transcoder_get_encoder(tp->tp_acodec))) + return transcoder_init_stream(t, ssc); + + LIST_FOREACH(ts, &t->t_stream_list, ts_link) + if (SCT_ISAUDIO(ts->ts_type)) + return 0; + + if (tp->tp_acodec == ssc->ssc_type) + return transcoder_init_stream(t, ssc); + + as = calloc(1, sizeof(audio_stream_t)); + + as->ts_index = ssc->ssc_index; + as->ts_type = tp->tp_acodec; + as->ts_target = t->t_output; + as->ts_handle_pkt = transcoder_stream_audio; + as->ts_destroy = transcoder_destroy_audio; + + as->aud_icodec = icodec; + as->aud_ocodec = ocodec; + + as->aud_ictx = avcodec_alloc_context(); + as->aud_octx = avcodec_alloc_context(); + + as->aud_ictx->codec_type = AVMEDIA_TYPE_AUDIO; + as->aud_octx->codec_type = AVMEDIA_TYPE_AUDIO; + + as->aud_ictx->thread_count = sysconf(_SC_NPROCESSORS_ONLN); + as->aud_octx->thread_count = sysconf(_SC_NPROCESSORS_ONLN); + + avcodec_get_context_defaults3(as->aud_ictx, icodec); + avcodec_get_context_defaults3(as->aud_octx, ocodec); + + as->aud_ictx->codec_type = AVMEDIA_TYPE_AUDIO; + as->aud_octx->codec_type = AVMEDIA_TYPE_AUDIO; + + as->aud_dec_size = AVCODEC_MAX_AUDIO_FRAME_SIZE*2; + as->aud_enc_size = AVCODEC_MAX_AUDIO_FRAME_SIZE*2; + + as->aud_dec_sample = av_malloc(as->aud_dec_size + FF_INPUT_BUFFER_PADDING_SIZE); + as->aud_enc_sample = av_malloc(as->aud_enc_size + FF_INPUT_BUFFER_PADDING_SIZE); + + memset(as->aud_dec_sample, 0, as->aud_dec_size + FF_INPUT_BUFFER_PADDING_SIZE); + memset(as->aud_enc_sample, 0, as->aud_enc_size + FF_INPUT_BUFFER_PADDING_SIZE); + + LIST_INSERT_HEAD(&t->t_stream_list, (transcoder_stream_t*)as, ts_link); + + tvhlog(LOG_INFO, "transcode", "%d:%s ==> %s", + ssc->ssc_index, + streaming_component_type2txt(ssc->ssc_type), + streaming_component_type2txt(as->ts_type)); + + ssc->ssc_type = tp->tp_acodec; + ssc->ssc_gh = NULL; + + // resampling not implemented yet + if(tp->tp_channels > 0) + as->aud_channels = 0; //tp->tp_channels; + else + as->aud_channels = 0; + + as->aud_bitrate = as->aud_channels * 64000; + return 1; +} + + +/** + * + */ +static void +transcoder_destroy_video(transcoder_stream_t *ts) +{ + video_stream_t *vs = (video_stream_t*)ts; + + if(vs->vid_ictx) { + avcodec_close(vs->vid_ictx); + av_free(vs->vid_ictx); + } + + if(vs->vid_octx) { + avcodec_close(vs->vid_octx); + av_free(vs->vid_octx); + } + + if(vs->vid_dec_frame) + av_free(vs->vid_dec_frame); + + if(vs->vid_scaler) + sws_freeContext(vs->vid_scaler); + + if(vs->vid_enc_frame) + av_free(vs->vid_enc_frame); + + free(ts); +} + + +/** + * + */ +static int +transcoder_init_video(transcoder_t *t, streaming_start_component_t *ssc) +{ + video_stream_t *vs; + AVCodec *icodec, *ocodec; + double aspect; + transcoder_props_t *tp = &t->t_props; + + if (tp->tp_vcodec == SCT_NONE) + return 0; + + else if (tp->tp_vcodec == SCT_UNKNOWN) + return transcoder_init_stream(t, ssc); + + else if (!(icodec = transcoder_get_decoder(ssc->ssc_type))) + return transcoder_init_stream(t, ssc); + + else if (!(ocodec = transcoder_get_encoder(tp->tp_vcodec))) + return transcoder_init_stream(t, ssc); + + vs = calloc(1, sizeof(video_stream_t)); + + vs->ts_index = ssc->ssc_index; + vs->ts_type = tp->tp_vcodec; + vs->ts_target = t->t_output; + vs->ts_handle_pkt = transcoder_stream_video; + vs->ts_destroy = transcoder_destroy_video; + + vs->vid_icodec = icodec; + vs->vid_ocodec = ocodec; + + vs->vid_ictx = avcodec_alloc_context(); + vs->vid_octx = avcodec_alloc_context(); + + vs->vid_ictx->thread_count = sysconf(_SC_NPROCESSORS_ONLN); + vs->vid_octx->thread_count = sysconf(_SC_NPROCESSORS_ONLN); + + avcodec_get_context_defaults3(vs->vid_ictx, icodec); + avcodec_get_context_defaults3(vs->vid_octx, ocodec); + + vs->vid_dec_frame = avcodec_alloc_frame(); + vs->vid_enc_frame = avcodec_alloc_frame(); + + avcodec_get_frame_defaults(vs->vid_dec_frame); + avcodec_get_frame_defaults(vs->vid_enc_frame); + + vs->vid_ictx->codec_type = AVMEDIA_TYPE_VIDEO; + vs->vid_octx->codec_type = AVMEDIA_TYPE_VIDEO; + + LIST_INSERT_HEAD(&t->t_stream_list, (transcoder_stream_t*)vs, ts_link); + + aspect = (double)ssc->ssc_width / ssc->ssc_height; + + vs->vid_height = MIN(tp->tp_resolution, ssc->ssc_height); + if (vs->vid_height&1) // Must be even + vs->vid_height++; + + vs->vid_width = vs->vid_height * aspect; + if (vs->vid_width&1) // Must be even + vs->vid_width++; + + tvhlog(LOG_INFO, "transcode", "%d:%s %dx%d ==> %s %dx%d", + ssc->ssc_index, + streaming_component_type2txt(ssc->ssc_type), + ssc->ssc_width, + ssc->ssc_height, + streaming_component_type2txt(vs->ts_type), + vs->vid_width, + vs->vid_height); + + ssc->ssc_type = tp->tp_vcodec; + ssc->ssc_width = vs->vid_width; + ssc->ssc_height = vs->vid_height; + ssc->ssc_gh = NULL; + + return 1; +} + + +/** + * Figure out how many streams we will use. + */ +static int +transcoder_calc_stream_count(transcoder_t *t, streaming_start_t *ss) { + int i = 0; + int video = 0; + int audio = 0; + int subtitle = 0; + streaming_start_component_t *ssc = NULL; + + for (i = 0; i < ss->ss_num_components; i++) { + ssc = &ss->ss_components[i]; + + if (ssc->ssc_disabled) + continue; + + if (SCT_ISVIDEO(ssc->ssc_type)) { + if (t->t_props.tp_vcodec == SCT_NONE) + video = 0; + else if (t->t_props.tp_vcodec == SCT_UNKNOWN) + video++; + else + video = 1; + + } else if (SCT_ISAUDIO(ssc->ssc_type)) { + if (t->t_props.tp_acodec == SCT_NONE) + audio = 0; + else if (t->t_props.tp_acodec == SCT_UNKNOWN) + audio++; + else + audio = 1; + + } else if (SCT_ISSUBTITLE(ssc->ssc_type)) { + if (t->t_props.tp_scodec == SCT_NONE) + subtitle = 0; + else if (t->t_props.tp_scodec == SCT_UNKNOWN) + subtitle++; + else + subtitle = 1; + } + } + + return (video + audio + subtitle); +} + + +/** + * + */ +static streaming_start_t * +transcoder_start(transcoder_t *t, streaming_start_t *src) +{ + int i, j, n, rc; + streaming_start_t *ss; + + + n = transcoder_calc_stream_count(t, src); + ss = calloc(1, (sizeof(streaming_start_t) + + sizeof(streaming_start_component_t) * n)); + + ss->ss_refcount = 1; + ss->ss_num_components = n; + ss->ss_pcr_pid = src->ss_pcr_pid; + ss->ss_pmt_pid = src->ss_pmt_pid; + service_source_info_copy(&ss->ss_si, &src->ss_si); + + + for (i = j = 0; i < src->ss_num_components && j < n; i++) { + streaming_start_component_t *ssc_src = &src->ss_components[i]; + streaming_start_component_t *ssc = &ss->ss_components[j]; + + if (ssc_src->ssc_disabled) + continue; + + ssc->ssc_index = ssc_src->ssc_index; + ssc->ssc_type = ssc_src->ssc_type; + ssc->ssc_composition_id = ssc_src->ssc_composition_id; + ssc->ssc_ancillary_id = ssc_src->ssc_ancillary_id; + ssc->ssc_pid = ssc_src->ssc_pid; + ssc->ssc_width = ssc_src->ssc_width; + ssc->ssc_height = ssc_src->ssc_height; + ssc->ssc_aspect_num = ssc_src->ssc_aspect_num; + ssc->ssc_aspect_den = ssc_src->ssc_aspect_den; + ssc->ssc_sri = ssc_src->ssc_sri; + ssc->ssc_channels = ssc_src->ssc_channels; + ssc->ssc_disabled = ssc_src->ssc_disabled; + ssc->ssc_frameduration = ssc_src->ssc_frameduration; + ssc->ssc_gh = ssc_src->ssc_gh; + + memcpy(ssc->ssc_lang, ssc_src->ssc_lang, 4); + + if (SCT_ISVIDEO(ssc->ssc_type)) + rc = transcoder_init_video(t, ssc); + + else if (SCT_ISAUDIO(ssc->ssc_type)) + rc = transcoder_init_audio(t, ssc); + + else if (SCT_ISSUBTITLE(ssc->ssc_type)) + rc = transcoder_init_subtitle(t, ssc); + else + rc = 0; + + if(!rc) + tvhlog(LOG_INFO, "transcode", "%d:%s ==> Filtered", + ssc->ssc_index, + streaming_component_type2txt(ssc->ssc_type)); + else + j++; + } + + return ss; +} + + +/** + * + */ +static void +transcoder_stop(transcoder_t *t) +{ + transcoder_stream_t *ts; + + while ((ts = LIST_FIRST(&t->t_stream_list))) { + LIST_REMOVE(ts, ts_link); + + if (ts->ts_destroy) + ts->ts_destroy(ts); + } +} + + +/** + * + */ +static void +transcoder_input(void *opaque, streaming_message_t *sm) +{ + transcoder_t *t; + streaming_start_t *ss; + th_pkt_t *pkt; + + t = opaque; + + switch (sm->sm_type) { + case SMT_PACKET: + pkt = sm->sm_data; + transcoder_packet(t, pkt); + pkt_ref_dec(pkt); + break; + + case SMT_START: + ss = transcoder_start(t, sm->sm_data); + streaming_start_unref(sm->sm_data); + sm->sm_data = ss; + + streaming_target_deliver2(t->t_output, sm); + break; + + case SMT_STOP: + transcoder_stop(t); + // Fallthrough + + case SMT_SPEED: + case SMT_SKIP: + case SMT_TIMESHIFT_STATUS: + case SMT_EXIT: + case SMT_SERVICE_STATUS: + case SMT_SIGNAL_STATUS: + case SMT_NOSTART: + case SMT_MPEGTS: + streaming_target_deliver2(t->t_output, sm); + break; + } +} + + +/** + * + */ +streaming_target_t * +transcoder_create(streaming_target_t *output) +{ + transcoder_t *t = calloc(1, sizeof(transcoder_t)); + + t->t_output = output; + + streaming_target_init(&t->t_input, transcoder_input, t, 0); + + return &t->t_input; +} + + +/** + * + */ +void +transcoder_set_properties(streaming_target_t *st, + transcoder_props_t *props) +{ + transcoder_t *t = (transcoder_t *)st; + transcoder_props_t *tp = &t->t_props; + + tp->tp_vcodec = props->tp_vcodec; + tp->tp_acodec = props->tp_acodec; + tp->tp_scodec = props->tp_scodec; + tp->tp_channels = props->tp_channels; + tp->tp_bandwidth = props->tp_bandwidth; + tp->tp_resolution = props->tp_resolution; + + memcpy(tp->tp_language, props->tp_language, 4); +} + + +/** + * + */ +void +transcoder_destroy(streaming_target_t *st) +{ + transcoder_t *t = (transcoder_t *)st; + + transcoder_stop(t); + free(t); +} + + +/** + * + */ +void +transcoder_get_capabilities(htsmsg_t *array) +{ + AVCodec *p = NULL; + const char *name; + streaming_component_type_t sct; + + while ((p = av_codec_next(p))) { + + if (!p->encode && !p->encode2) + continue; + + if (!WORKING_ENCODER(p->id)) + continue; + + sct = codec_id2streaming_component_type(p->id); + if (sct == SCT_NONE) + continue; + + name = streaming_component_type2txt(sct); + htsmsg_add_str(array, NULL, name); + } +} + + + diff --git a/src/plumbing/transcoding.h b/src/plumbing/transcoding.h new file mode 100644 index 00000000..a08bea01 --- /dev/null +++ b/src/plumbing/transcoding.h @@ -0,0 +1,42 @@ +/** + * Transcoding + * Copyright (C) 2013 John Törnblom + * + * 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 . + */ +#pragma once + +#include "tvheadend.h" +#include "htsmsg.h" + +typedef struct transcoder_prop { + streaming_component_type_t tp_vcodec; + streaming_component_type_t tp_acodec; + streaming_component_type_t tp_scodec; + + int8_t tp_channels; + int32_t tp_bandwidth; + char tp_language[4]; + int32_t tp_resolution; +} transcoder_props_t; + +extern uint32_t transcoding_enabled; + +streaming_target_t *transcoder_create (streaming_target_t *output); +void transcoder_destroy(streaming_target_t *tr); + +void transcoder_get_capabilities(htsmsg_t *array); +void transcoder_set_properties (streaming_target_t *tr, + transcoder_props_t *prop); + diff --git a/src/psi.c b/src/psi.c index b191037b..d86519c8 100644 --- a/src/psi.c +++ b/src/psi.c @@ -908,6 +908,8 @@ psi_caid2name(uint16_t caid) * */ static struct strtab streamtypetab[] = { + { "NONE", SCT_NONE }, + { "UNKNOWN", SCT_UNKNOWN }, { "MPEG2VIDEO", SCT_MPEG2VIDEO }, { "MPEG2AUDIO", SCT_MPEG2AUDIO }, { "H264", SCT_H264 }, @@ -920,7 +922,7 @@ static struct strtab streamtypetab[] = { { "MPEGTS", SCT_MPEGTS }, { "TEXTSUB", SCT_TEXTSUB }, { "EAC3", SCT_EAC3 }, - { "AAC", SCT_MP4A }, + { "AAC", SCT_MP4A }, }; @@ -933,6 +935,14 @@ streaming_component_type2txt(streaming_component_type_t s) return val2str(s, streamtypetab) ?: "INVALID"; } +/** + * + */ +streaming_component_type_t +streaming_component_txt2type(const char *str) +{ + return str ? str2val(str, streamtypetab) : SCT_UNKNOWN; +} /** * Store service settings into message diff --git a/src/tvheadend.h b/src/tvheadend.h index 55cdd517..4bc951dc 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -184,6 +184,7 @@ int get_device_connection(const char *dev); * Stream component types */ typedef enum { + SCT_NONE = -1, SCT_UNKNOWN = 0, SCT_MPEG2VIDEO = 1, SCT_MPEG2AUDIO, @@ -423,7 +424,7 @@ typedef struct sbuf { } sbuf_t; - +streaming_component_type_t streaming_component_txt2type(const char *str); const char *streaming_component_type2txt(streaming_component_type_t s); static inline unsigned int tvh_strhash(const char *s, unsigned int mod) diff --git a/src/webui/webui.c b/src/webui/webui.c index bc7d5989..997b8829 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -40,6 +40,7 @@ #include "psi.h" #include "plumbing/tsfix.h" #include "plumbing/globalheaders.h" +#include "plumbing/transcoding.h" #include "epg.h" #include "muxer.h" #include "dvb/dvb.h" @@ -548,6 +549,47 @@ page_http_playlist(http_connection_t *hc, const char *remain, void *opaque) } +#if ENABLE_LIBAV +static int +http_get_transcoder_properties(struct http_arg_list *args, + transcoder_props_t *props) +{ + int transcode; + const char *s; + + memset(props, 0, sizeof(transcoder_props_t)); + + if ((s = http_arg_get(args, "transcode"))) + transcode = atoi(s); + else + transcode = 0; + + if ((s = http_arg_get(args, "resolution"))) + props->tp_resolution = atoi(s); + + if ((s = http_arg_get(args, "channels"))) + props->tp_channels = atoi(s); + + if ((s = http_arg_get(args, "bandwidth"))) + props->tp_bandwidth = atoi(s); + + if ((s = http_arg_get(args, "language"))) + strncpy(props->tp_language, s, 3); + + if ((s = http_arg_get(args, "vcodec"))) + props->tp_vcodec = streaming_component_txt2type(s); + + if ((s = http_arg_get(args, "acodec"))) + props->tp_acodec = streaming_component_txt2type(s); + + if ((s = http_arg_get(args, "scodec"))) + props->tp_scodec = streaming_component_txt2type(s); + + return transcode && transcoding_enabled; +} +#endif + + /** * Subscribes to a service and starts the streaming loop */ @@ -660,6 +702,9 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) streaming_target_t *gh; streaming_target_t *tsfix; streaming_target_t *st; +#if ENABLE_LIBAV + streaming_target_t *tr = NULL; +#endif dvr_config_t *cfg; int priority = 100; int flags; @@ -689,6 +734,14 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) } else { streaming_queue_init2(&sq, 0, qsize); gh = globalheaders_create(&sq.sq_st); +#if ENABLE_LIBAV + transcoder_props_t props; + if(http_get_transcoder_properties(&hc->hc_req_args, &props)) { + tr = transcoder_create(gh); + transcoder_set_properties(tr, &props); + tsfix = tsfix_create(tr); + } else +#endif tsfix = tsfix_create(gh); st = tsfix; flags = 0; @@ -710,6 +763,12 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) if(gh) globalheaders_destroy(gh); + +#if ENABLE_LIBAV + if(tr) + transcoder_destroy(tr); +#endif + if(tsfix) tsfix_destroy(tsfix); From 4b86d2fd200fc2a65dab0da6381f1337b892ac63 Mon Sep 17 00:00:00 2001 From: Dave Chapman Date: Mon, 13 May 2013 09:27:01 +0100 Subject: [PATCH 03/10] Parse the audio_type field of the iso639_language_descriptor - this indicates if the audio stream contains audio description for the hard of hearing. Use this value when regenerating the PMT for the passthrough muxer, and also include it in the HTSP stream description messages. --- src/htsp_server.c | 1 + src/psi.c | 30 +++++++++++++++++++----------- src/service.c | 1 + src/service.h | 2 ++ src/streaming.h | 1 + 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/htsp_server.c b/src/htsp_server.c index 2a670a69..eeb15ace 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -2421,6 +2421,7 @@ htsp_subscription_start(htsp_subscription_t *hs, const streaming_start_t *ss) if (SCT_ISAUDIO(ssc->ssc_type)) { + htsmsg_add_u32(c, "audio_type", ssc->ssc_audio_type); if (ssc->ssc_channels) htsmsg_add_u32(c, "channels", ssc->ssc_channels); if (ssc->ssc_sri) diff --git a/src/psi.c b/src/psi.c index d86519c8..dc8ec979 100644 --- a/src/psi.c +++ b/src/psi.c @@ -172,16 +172,17 @@ psi_build_pat(service_t *t, uint8_t *buf, int maxlen, int pmtpid) #define PMT_UPDATE_PCR 0x1 #define PMT_UPDATE_NEW_STREAM 0x2 #define PMT_UPDATE_LANGUAGE 0x4 -#define PMT_UPDATE_FRAME_DURATION 0x8 -#define PMT_UPDATE_COMPOSITION_ID 0x10 -#define PMT_UPDATE_ANCILLARY_ID 0x20 -#define PMT_UPDATE_STREAM_DELETED 0x40 -#define PMT_UPDATE_NEW_CA_STREAM 0x80 -#define PMT_UPDATE_NEW_CAID 0x100 -#define PMT_UPDATE_CA_PROVIDER_CHANGE 0x200 -#define PMT_UPDATE_PARENT_PID 0x400 -#define PMT_UPDATE_CAID_DELETED 0x800 -#define PMT_REORDERED 0x1000 +#define PMT_UPDATE_AUDIO_TYPE 0x8 +#define PMT_UPDATE_FRAME_DURATION 0x10 +#define PMT_UPDATE_COMPOSITION_ID 0x20 +#define PMT_UPDATE_ANCILLARY_ID 0x40 +#define PMT_UPDATE_STREAM_DELETED 0x80 +#define PMT_UPDATE_NEW_CA_STREAM 0x100 +#define PMT_UPDATE_NEW_CAID 0x200 +#define PMT_UPDATE_CA_PROVIDER_CHANGE 0x400 +#define PMT_UPDATE_PARENT_PID 0x800 +#define PMT_UPDATE_CAID_DELETED 0x1000 +#define PMT_REORDERED 0x2000 /** * Add a CA descriptor @@ -397,6 +398,7 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, int position = 0; int tt_position = 1000; const char *lang = NULL; + uint8_t audio_type = 0; caid_t *c, *cn; @@ -529,6 +531,7 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, case DVB_DESC_LANGUAGE: lang = lang_code_get2((const char*)ptr, 3); + audio_type = ptr[3]; break; case DVB_DESC_TELETEXT: @@ -601,6 +604,11 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, memcpy(st->es_lang, lang, 4); } + if(st->es_audio_type != audio_type) { + update |= PMT_UPDATE_AUDIO_TYPE; + st->es_audio_type = audio_type; + } + if(composition_id != -1 && st->es_composition_id != composition_id) { st->es_composition_id = composition_id; update |= PMT_UPDATE_COMPOSITION_ID; @@ -756,7 +764,7 @@ psi_build_pmt(const streaming_start_t *ss, uint8_t *buf0, int maxlen, buf[0] = DVB_DESC_LANGUAGE; buf[1] = 4; memcpy(&buf[2],ssc->ssc_lang,3); - buf[5] = 0; /* Main audio */ + buf[5] = ssc->ssc_audio_type; dlen = 6; break; case SCT_DVBSUB: diff --git a/src/service.c b/src/service.c index 7851f5c2..c6666b5f 100644 --- a/src/service.c +++ b/src/service.c @@ -908,6 +908,7 @@ service_build_stream_start(service_t *t) ssc->ssc_type = st->es_type; memcpy(ssc->ssc_lang, st->es_lang, 4); + ssc->ssc_audio_type = st->es_audio_type; ssc->ssc_composition_id = st->es_composition_id; ssc->ssc_ancillary_id = st->es_ancillary_id; ssc->ssc_pid = st->es_pid; diff --git a/src/service.h b/src/service.h index d2f8b03b..8b96156e 100644 --- a/src/service.h +++ b/src/service.h @@ -82,6 +82,8 @@ typedef struct elementary_stream { uint16_t es_aspect_den; char es_lang[4]; /* ISO 639 2B 3-letter language code */ + uint8_t es_audio_type; /* Audio type */ + uint16_t es_composition_id; uint16_t es_ancillary_id; diff --git a/src/streaming.h b/src/streaming.h index 75dc7857..855be286 100644 --- a/src/streaming.h +++ b/src/streaming.h @@ -27,6 +27,7 @@ typedef struct streaming_start_component { int ssc_index; int ssc_type; char ssc_lang[4]; + uint8_t ssc_audio_type; uint16_t ssc_composition_id; uint16_t ssc_ancillary_id; uint16_t ssc_pid; From 477fa0a846780b4c6f594bc93354ed7fe2878ba9 Mon Sep 17 00:00:00 2001 From: Dave Chapman Date: Mon, 13 May 2013 10:39:35 +0100 Subject: [PATCH 04/10] Add saving/loading of audio_type, and also display it in the webui --- src/psi.c | 22 ++++++++++++++++++++++ src/psi.h | 1 + src/webui/extjs.c | 8 +++++++- src/webui/static/app/tvadapters.js | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/psi.c b/src/psi.c index dc8ec979..378cc4c4 100644 --- a/src/psi.c +++ b/src/psi.c @@ -912,6 +912,20 @@ psi_caid2name(uint16_t caid) return buf; } +const char * +psi_audio_type2desc(uint8_t audio_type) +{ + /* From ISO 13818-1 - ISO 639 language descriptor */ + switch(audio_type) { + case 0: return ""; /* "Undefined" in the standard, but used for normal audio */ + case 1: return "Clean effects"; + case 2: return "Hearing impaired"; + case 3: return "Visually impaired commentary"; + } + + return "Reserved"; +} + /** * */ @@ -977,6 +991,9 @@ psi_save_service_settings(htsmsg_t *m, service_t *t) if(st->es_lang[0]) htsmsg_add_str(sub, "language", st->es_lang); + if (SCT_ISAUDIO(st->es_type)) + htsmsg_add_u32(sub, "audio_type", st->es_audio_type); + if(st->es_type == SCT_CA) { caid_t *c; @@ -1127,6 +1144,11 @@ psi_load_service_settings(htsmsg_t *m, service_t *t) if((v = htsmsg_get_str(c, "language")) != NULL) strncpy(st->es_lang, lang_code_get(v), 3); + if (SCT_ISAUDIO(type)) { + if(!htsmsg_get_u32(c, "audio_type", &u32)) + st->es_audio_type = u32; + } + if(!htsmsg_get_u32(c, "position", &u32)) st->es_position = u32; diff --git a/src/psi.h b/src/psi.h index 34c9736b..5dfc98b5 100644 --- a/src/psi.h +++ b/src/psi.h @@ -46,6 +46,7 @@ int psi_build_pmt(const streaming_start_t *ss, uint8_t *buf, int maxlen, int version, int pcrpid); const char *psi_caid2name(uint16_t caid); +const char *psi_audio_type2desc(uint8_t audio_type); void psi_load_service_settings(htsmsg_t *m, struct service *t); void psi_save_service_settings(htsmsg_t *m, struct service *t); diff --git a/src/webui/extjs.c b/src/webui/extjs.c index c7a6dfcb..dd2b07d1 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -1634,7 +1634,13 @@ extjs_servicedetails(http_connection_t *hc, case SCT_MP4A: case SCT_AAC: case SCT_MPEG2AUDIO: - htsmsg_add_str(c, "details", st->es_lang); + if (st->es_audio_type) { + snprintf(buf, sizeof(buf), "%s (%s)", st->es_lang, + psi_audio_type2desc(st->es_audio_type)); + htsmsg_add_str(c, "details", buf); + } else { + htsmsg_add_str(c, "details", st->es_lang); + } break; case SCT_DVBSUB: diff --git a/src/webui/static/app/tvadapters.js b/src/webui/static/app/tvadapters.js index 3661fabd..8da843f1 100644 --- a/src/webui/static/app/tvadapters.js +++ b/src/webui/static/app/tvadapters.js @@ -99,7 +99,7 @@ tvheadend.showTransportDetails = function(data) { win = new Ext.Window({ title : 'Service details for ' + data.title, layout : 'fit', - width : 400, + width : 450, height : 400, plain : true, bodyStyle : 'padding: 5px', From 29f715f685b502d332359dccd4047a222fc679e4 Mon Sep 17 00:00:00 2001 From: Dave Chapman Date: Mon, 13 May 2013 11:23:26 +0100 Subject: [PATCH 05/10] Bump HTSP protocol version to 11 for the recent transcoding changes and the addition of the audio_type field --- src/htsp_server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/htsp_server.c b/src/htsp_server.c index eeb15ace..88fe3bdd 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -59,7 +59,7 @@ static void *htsp_server, *htsp_server_2; -#define HTSP_PROTO_VERSION 10 +#define HTSP_PROTO_VERSION 11 #define HTSP_ASYNC_OFF 0x00 #define HTSP_ASYNC_ON 0x01 From 535fc25fccbe84baa4d9a3077a5a67897eb4b000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96man?= Date: Wed, 22 May 2013 09:47:25 +0200 Subject: [PATCH 06/10] HTSP: Add subscriptionFilterStream command --- src/htsp_server.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/htsp_server.c b/src/htsp_server.c index 88fe3bdd..88de45cf 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -193,6 +193,10 @@ streaming_target_t *hs_transcoder; int hs_queue_depth; +#define NUM_FILTERED_STREAMS (32*16) + + uint32_t hs_filtered_streams[16]; // one bit per stream + } htsp_subscription_t; @@ -214,6 +218,31 @@ typedef struct htsp_file { * Support routines * *************************************************************************/ +static void +htsp_disable_stream(htsp_subscription_t *hs, unsigned int id) +{ + if(id < NUM_FILTERED_STREAMS) + hs->hs_filtered_streams[id / 32] |= 1 << (id & 31); +} + + +static void +htsp_enable_stream(htsp_subscription_t *hs, unsigned int id) +{ + if(id < NUM_FILTERED_STREAMS) + hs->hs_filtered_streams[id / 32] &= ~(1 << (id & 31)); +} + + +static inline int +htsp_is_stream_enabled(htsp_subscription_t *hs, unsigned int id) +{ + if(id < NUM_FILTERED_STREAMS) + return !(hs->hs_filtered_streams[id / 32] & (1 << (id & 31))); + return 1; +} + + /** * */ @@ -664,6 +693,8 @@ htsp_build_event htsmsg_add_s64(out, "stop", e->stop); if ((str = epg_broadcast_get_title(e, lang))) htsmsg_add_str(out, "title", str); + if ((str = epg_broadcast_get_subtitle(e, lang))) + htsmsg_add_str(out, "subtitle", str); if ((str = epg_broadcast_get_description(e, lang))) { htsmsg_add_str(out, "description", str); if ((str = epg_broadcast_get_summary(e, lang))) @@ -1532,6 +1563,44 @@ htsp_method_live(htsp_connection_t *htsp, htsmsg_t *in) return NULL; } +/** + * Change filters for a subscription + */ +static htsmsg_t * +htsp_method_filter_stream(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_subscription_t *hs; + uint32_t sid; + htsmsg_t *l; + if(htsmsg_get_u32(in, "subscriptionId", &sid)) + return htsp_error("Missing argument 'subscriptionId'"); + + LIST_FOREACH(hs, &htsp->htsp_subscriptions, hs_link) + if(hs->hs_sid == sid) + break; + + if(hs == NULL) + return htsp_error("Requested subscription does not exist"); + + if((l = htsmsg_get_list(in, "enable")) != NULL) { + htsmsg_field_t *f; + HTSMSG_FOREACH(f, l) { + if(f->hmf_type == HMF_S64) + htsp_enable_stream(hs, f->hmf_s64); + } + } + + if((l = htsmsg_get_list(in, "disable")) != NULL) { + htsmsg_field_t *f; + HTSMSG_FOREACH(f, l) { + if(f->hmf_type == HMF_S64) + htsp_disable_stream(hs, f->hmf_s64); + } + } + return htsmsg_create_map(); +} + + /** * Open file */ @@ -1733,6 +1802,7 @@ struct { { "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}, #endif @@ -2280,6 +2350,11 @@ htsp_stream_deliver(htsp_subscription_t *hs, th_pkt_t *pkt) int64_t ts; int qlen = hs->hs_q.hmq_payload; + if(!htsp_is_stream_enabled(hs, pkt->pkt_componentindex)) { + pkt_ref_dec(pkt); + return; + } + if((qlen > hs->hs_queue_depth && pkt->pkt_frametype == PKT_B_FRAME) || (qlen > hs->hs_queue_depth * 2 && pkt->pkt_frametype == PKT_P_FRAME) || (qlen > hs->hs_queue_depth * 3)) { From a5a08aa1e594079d6c07334ede040324f9f5f28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96man?= Date: Wed, 22 May 2013 09:53:27 +0200 Subject: [PATCH 07/10] Remove accidental code --- src/htsp_server.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/htsp_server.c b/src/htsp_server.c index 88de45cf..8a6ce451 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -693,8 +693,6 @@ htsp_build_event htsmsg_add_s64(out, "stop", e->stop); if ((str = epg_broadcast_get_title(e, lang))) htsmsg_add_str(out, "title", str); - if ((str = epg_broadcast_get_subtitle(e, lang))) - htsmsg_add_str(out, "subtitle", str); if ((str = epg_broadcast_get_description(e, lang))) { htsmsg_add_str(out, "description", str); if ((str = epg_broadcast_get_summary(e, lang))) From 15fa7adc2254d8a171d36daa51bbd3d76b2f503c Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Wed, 22 May 2013 12:53:32 +0100 Subject: [PATCH 08/10] htsp: update the EPG/HTSP hookup to send channelUpdate on next event change. Previously this was only being output if the current event was updated. --- src/epg.c | 15 ++++++--------- src/htsp_server.c | 4 ++-- src/htsp_server.h | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/epg.c b/src/epg.c index e031ae07..280b8108 100644 --- a/src/epg.c +++ b/src/epg.c @@ -1396,12 +1396,16 @@ static void _epg_channel_timer_callback ( void *p ) break; } - /* Change */ - if (cur != ch->ch_epg_now || nxt != ch->ch_epg_next) + /* Change (update HTSP) */ + if (cur != ch->ch_epg_now || nxt != ch->ch_epg_next) { tvhlog(LOG_DEBUG, "epg", "now/next %u/%u set on %s", ch->ch_epg_now ? ch->ch_epg_now->id : 0, ch->ch_epg_next ? ch->ch_epg_next->id : 0, ch->ch_name); + tvhlog(LOG_DEBUG, "epg", "inform HTSP of now event change on %s", + ch->ch_name); + htsp_channel_update_nownext(ch); + } /* re-arm */ if ( next ) { @@ -1410,13 +1414,6 @@ static void _epg_channel_timer_callback ( void *p ) gtimer_arm_abs(&ch->ch_epg_timer, _epg_channel_timer_callback, ch, next); } - /* Update HTSP */ - if ( cur != ch->ch_epg_now ) { - tvhlog(LOG_DEBUG, "epg", "inform HTSP of now event change on %s", - ch->ch_name); - htsp_channel_update_current(ch); - } - /* Remove refs */ if (cur) cur->putref(cur); if (nxt) nxt->putref(nxt); diff --git a/src/htsp_server.c b/src/htsp_server.c index 8a6ce451..e5234e96 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -2161,13 +2161,13 @@ htsp_async_send(htsmsg_t *m, int mode) /** - * EPG subsystem calls this function when the current event + * EPG subsystem calls this function when the current/next event * changes for a channel, e may be NULL if there is no current event. * * global_lock is held */ void -htsp_channel_update_current(channel_t *ch) +htsp_channel_update_nownext(channel_t *ch) { epg_broadcast_t *now, *next; htsmsg_t *m; diff --git a/src/htsp_server.h b/src/htsp_server.h index 77002cac..f551ab89 100644 --- a/src/htsp_server.h +++ b/src/htsp_server.h @@ -24,7 +24,7 @@ void htsp_init(const char *bindaddr); -void htsp_channel_update_current(channel_t *ch); +void htsp_channel_update_nownext(channel_t *ch); void htsp_channel_add(channel_t *ch); void htsp_channel_update(channel_t *ch); From 833355d1018280552f48625903960c5beb210284 Mon Sep 17 00:00:00 2001 From: Dave Chapman Date: Thu, 23 May 2013 09:21:29 +0100 Subject: [PATCH 09/10] Add episodeUri and serieslinkUri to the HTSP eventAdd and eventUpdate messages. --- src/htsp_server.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/htsp_server.c b/src/htsp_server.c index e5234e96..b6f2d462 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -699,11 +699,16 @@ htsp_build_event htsmsg_add_str(out, "summary", str); } else if((str = epg_broadcast_get_summary(e, lang))) htsmsg_add_str(out, "description", str); - if (e->serieslink) + if (e->serieslink) { htsmsg_add_u32(out, "serieslinkId", e->serieslink->id); + if (e->serieslink->uri) + htsmsg_add_str(out, "serieslinkUri", e->serieslink->uri); + } if (ee) { htsmsg_add_u32(out, "episodeId", ee->id); + if (ee->uri) + htsmsg_add_str(out, "episodeUri", ee->uri); if (ee->brand) htsmsg_add_u32(out, "brandId", ee->brand->id); if (ee->season) From 8c5f9af36b59f91652c96dd86a35d878ae2a2266 Mon Sep 17 00:00:00 2001 From: Dave Chapman Date: Thu, 23 May 2013 10:10:34 +0100 Subject: [PATCH 10/10] Do not export the episode URI if it starts with tvh:// - these are just for internal use within tvh. --- src/htsp_server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/htsp_server.c b/src/htsp_server.c index b6f2d462..77dbb6c0 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -707,7 +707,7 @@ htsp_build_event if (ee) { htsmsg_add_u32(out, "episodeId", ee->id); - if (ee->uri) + if (ee->uri && strncasecmp(ee->uri,"tvh://",6)) /* tvh:// uris are internal */ htsmsg_add_str(out, "episodeUri", ee->uri); if (ee->brand) htsmsg_add_u32(out, "brandId", ee->brand->id);