diff --git a/.gitignore b/.gitignore index 2a4ca61c..d7e34f72 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,16 @@ build.* src/version.c +data/dvb-scan + .cproject .project .settings -data/dvb-scan *.pyc .*.sw[op] +debian/changelog debian/files debian/tvheadend debian/tvheadend-dbg diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/Makefile b/Makefile index f8a2a535..b3a6a7ae 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,8 @@ # Configuration # -include ${CURDIR}/.config.mk -PROG = ${BUILDDIR}/tvheadend +include $(dir $(lastword $(MAKEFILE_LIST))).config.mk +PROG := $(BUILDDIR)/tvheadend # # Common compiler flags @@ -31,8 +31,11 @@ CFLAGS += -Wall -Werror -Wwrite-strings -Wno-deprecated-declarations CFLAGS += -Wmissing-prototypes -fms-extensions CFLAGS += -g -funsigned-char -O2 CFLAGS += -D_FILE_OFFSET_BITS=64 -CFLAGS += -I${BUILDDIR} -I${CURDIR}/src -I${CURDIR} -LDFLAGS += -lrt -ldl -lpthread +CFLAGS += -I${BUILDDIR} -I${ROOTDIR}/src -I${ROOTDIR} +LDFLAGS += -lrt -ldl -lpthread -lm + +vpath %.c $(ROOTDIR) +vpath %.h $(ROOTDIR) # # Other config @@ -45,16 +48,16 @@ BUNDLE_FLAGS = ${BUNDLE_FLAGS-yes} # Binaries/Scripts # -MKBUNDLE = $(PYTHON) $(CURDIR)/support/mkbundle +MKBUNDLE = $(PYTHON) $(ROOTDIR)/support/mkbundle # # Debug/Output # ifndef V -ECHO = printf "$(1)\t\t%s\n" $(2) +ECHO = printf "%-16s%s\n" $(1) $(2) BRIEF = CC MKBUNDLE CXX -MSG = $@ +MSG = $(subst $(BUILDDIR)/,,$@) $(foreach VAR,$(BRIEF), \ $(eval $(VAR) = @$$(call ECHO,$(VAR),$$(MSG)); $($(VAR)))) endif @@ -62,10 +65,11 @@ endif # # Core # -SRCS = src/main.c \ +SRCS = src/version.c \ + src/main.c \ + src/tvhlog.c \ src/utils.c \ src/wrappers.c \ - src/version.c \ src/access.c \ src/dtable.c \ src/tcp.c \ @@ -88,12 +92,14 @@ SRCS = src/main.c \ src/parser_latm.c \ src/tsdemux.c \ src/bitstream.c \ - src/htsp.c \ + src/htsp_server.c \ src/serviceprobe.c \ src/htsmsg.c \ src/htsmsg_binary.c \ src/htsmsg_json.c \ src/htsmsg_xml.c \ + src/misc/dbl.c \ + src/misc/json.c \ src/settings.c \ src/htsbuf.c \ src/trap.c \ @@ -104,28 +110,28 @@ SRCS = src/main.c \ src/avc.c \ src/huffman.c \ src/filebundle.c \ - src/muxes.c \ src/config2.c \ src/lang_codes.c \ src/lang_str.c \ + src/imagecache.c \ + src/tvhtime.c SRCS += src/epggrab/module.c\ src/epggrab/channel.c\ - src/epggrab/otamux.c\ src/epggrab/module/pyepg.c\ src/epggrab/module/xmltv.c\ + +SRCS-$(CONFIG_LINUXDVB) += src/epggrab/otamux.c\ src/epggrab/module/eit.c \ src/epggrab/module/opentv.c \ src/epggrab/support/freesat_huffman.c \ SRCS += src/plumbing/tsfix.c \ - src/plumbing/globalheaders.c \ + src/plumbing/globalheaders.c SRCS += src/dvr/dvr_db.c \ src/dvr/dvr_rec.c \ src/dvr/dvr_autorec.c \ - src/dvr/ebml.c \ - src/dvr/mkmux.c \ SRCS += src/webui/webui.c \ src/webui/comet.c \ @@ -135,13 +141,22 @@ SRCS += src/webui/webui.c \ src/webui/html.c\ SRCS += src/muxer.c \ - src/muxer_pass.c \ - src/muxer_tvh.c \ + src/muxer/muxer_pass.c \ + src/muxer/muxer_tvh.c \ + src/muxer/tvh/ebml.c \ + src/muxer/tvh/mkmux.c \ # # Optional code # +# Timeshift +SRCS-${CONFIG_TIMESHIFT} += \ + src/timeshift.c \ + src/timeshift/timeshift_filemgr.c \ + src/timeshift/timeshift_writer.c \ + src/timeshift/timeshift_reader.c \ + # DVB SRCS-${CONFIG_LINUXDVB} += \ src/dvb/dvb.c \ @@ -152,30 +167,46 @@ SRCS-${CONFIG_LINUXDVB} += \ src/dvb/diseqc.c \ src/dvb/dvb_adapter.c \ src/dvb/dvb_multiplex.c \ - src/dvb/dvb_transport.c \ + src/dvb/dvb_service.c \ src/dvb/dvb_preconf.c \ src/dvb/dvb_satconf.c \ + src/dvb/dvb_input_filtered.c \ + src/dvb/dvb_input_raw.c \ src/webui/extjs_dvb.c \ + src/muxes.c \ + +# Inotify +SRCS-${CONFIG_INOTIFY} += \ + src/dvr/dvr_inotify.c \ # V4L SRCS-${CONFIG_V4L} += \ src/v4l.c \ src/webui/extjs_v4l.c \ -# CWC -SRCS-${CONFIG_CWC} += src/cwc.c \ - src/capmt.c \ - src/ffdecsa/ffdecsa_interface.c \ - src/ffdecsa/ffdecsa_int.c - # Avahi SRCS-$(CONFIG_AVAHI) += src/avahi.c -# Optimised code +# libav +SRCS-$(CONFIG_LIBAV) += src/libav.c \ + src/muxer/muxer_libav.c \ + src/plumbing/transcoding.c \ + +# CWC +SRCS-${CONFIG_CWC} += src/cwc.c \ + src/capmt.c + +# FFdecsa +ifneq ($(CONFIG_DVBCSA),yes) +SRCS-${CONFIG_CWC} += src/ffdecsa/ffdecsa_interface.c \ + src/ffdecsa/ffdecsa_int.c +ifeq ($(CONFIG_CWC),yes) SRCS-${CONFIG_MMX} += src/ffdecsa/ffdecsa_mmx.c SRCS-${CONFIG_SSE2} += src/ffdecsa/ffdecsa_sse2.c +endif ${BUILDDIR}/src/ffdecsa/ffdecsa_mmx.o : CFLAGS += -mmmx ${BUILDDIR}/src/ffdecsa/ffdecsa_sse2.o : CFLAGS += -msse2 +endif # File bundles SRCS-${CONFIG_BUNDLE} += bundle.c @@ -207,16 +238,26 @@ DEPS = ${OBJS:%.o=%.d} all: ${PROG} # Special -.PHONY: clean distclean +.PHONY: clean distclean check_config reconfigure + +# Check configure output is valid +check_config: + @test $(ROOTDIR)/.config.mk -nt $(ROOTDIR)/configure\ + || echo "./configure output is old, please re-run" + @test $(ROOTDIR)/.config.mk -nt $(ROOTDIR)/configure + +# Recreate configuration +reconfigure: + $(ROOTDIR)/configure $(CONFIGURE_ARGS) # Binary -${PROG}: $(OBJS) $(ALLDEPS) +${PROG}: check_config $(OBJS) $(ALLDEPS) $(CC) -o $@ $(OBJS) $(CFLAGS) $(LDFLAGS) # Object ${BUILDDIR}/%.o: %.c @mkdir -p $(dir $@) - $(CC) -MD -MP $(CFLAGS) -c -o $@ $(CURDIR)/$< + $(CC) -MD -MP $(CFLAGS) -c -o $@ $< # Add-on ${BUILDDIR}/%.so: ${SRCS_EXTRA} @@ -229,25 +270,26 @@ clean: find . -name "*~" | xargs rm -f distclean: clean - rm -rf ${CURDIR}/build.* - rm -f ${CURDIR}/.config.mk + rm -rf ${ROOTDIR}/build.* + rm -f ${ROOTDIR}/.config.mk -# Create buildversion.h -src/version.c: FORCE - @$(CURDIR)/support/version $@ > /dev/null +# Create version +$(BUILDDIR)/src/version.o: $(ROOTDIR)/src/version.c +$(ROOTDIR)/src/version.c: FORCE + @$(ROOTDIR)/support/version $@ > /dev/null FORCE: # Include dependency files if they exist. -include $(DEPS) # Include OS specific targets -include support/${OSENV}.mk +include ${ROOTDIR}/support/${OSENV}.mk # Bundle files $(BUILDDIR)/bundle.o: $(BUILDDIR)/bundle.c @mkdir -p $(dir $@) - $(CC) -I${CURDIR}/src -c -o $@ $< + $(CC) -I${ROOTDIR}/src -c -o $@ $< $(BUILDDIR)/bundle.c: @mkdir -p $(dir $@) - $(MKBUNDLE) -o $@ -d ${BUILDDIR}/bundle.d $(BUNDLE_FLAGS) $(BUNDLES) + $(MKBUNDLE) -o $@ -d ${BUILDDIR}/bundle.d $(BUNDLE_FLAGS) $(BUNDLES:%=$(ROOTDIR)/%) diff --git a/README b/README index 6edba018..dd1cc6ef 100644 --- a/README +++ b/README @@ -1,30 +1,33 @@ - Tvheadend TV streaming server - ============================= - - (c) 2006 - 2012 Andreas Öman, et al. - +Tvheadend (TV streaming server) v3.5 +==================================== +(c) 2006 - 2013 Andreas Öman, et al. How to build for Linux -====================== +---------------------- First you need to configure: -$ ./configure + $ ./configure If any dependencies are missing the configure script will complain or attempt to disable optional features. -$ make +Build the binary: + + $ make + +After build, the binary resides in `build.linux/`. -Build the binary, after build the binary resides in 'build.linux/'. Thus, to start it, just type: -$ ./build.linux/tvheadend + $ ./build.linux/tvheadend -Settings are stored in $HOME/.hts/tvheadend +Settings are stored in `$HOME/.hts/tvheadend`. Further information -=================== +------------------- -For more information about building, including generating packages please -visit https://www.lonelycoder.com/redmine/projects/tvheadend/wiki/Building +For more information about building, including generating packages please visit: +> https://tvheadend.org/projects/tvheadend/wiki/Building +> https://tvheadend.org/projects/tvheadend/wiki/Packaging +> https://tvheadend.org/projects/tvheadend/wiki/Git diff --git a/README.md b/README.md new file mode 100644 index 00000000..5f2c0675 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +Tvheadend (TV streaming server) +==================================== +(c) 2006 - 2013 Andreas Öman, et al. + +How to build for Linux +---------------------- + +First you need to configure: + + $ ./configure + +If any dependencies are missing the configure script will complain or attempt +to disable optional features. + +Build the binary: + + $ make + +After build, the binary resides in `build.linux/`. + +Thus, to start it, just type: + + $ ./build.linux/tvheadend + +Settings are stored in `$HOME/.hts/tvheadend`. + +Further information +------------------- + +For more information about building, including generating packages please visit: +> https://tvheadend.org/projects/tvheadend/wiki/Building +> https://tvheadend.org/projects/tvheadend/wiki/Packaging +> https://tvheadend.org/projects/tvheadend/wiki/Git diff --git a/configure b/configure index cd493750..fcec9de4 100755 --- a/configure +++ b/configure @@ -9,7 +9,7 @@ # Setup # ########################################################################### -ROOTDIR=$(dirname $0) +ROOTDIR=$(cd "$(dirname "$0")"; pwd) # # Options @@ -20,22 +20,30 @@ OPTIONS=( "v4l:yes" "linuxdvb:yes" "dvbscan:yes" + "timeshift:yes" + "trace:yes" + "imagecache:auto" "avahi:auto" "zlib:auto" + "libav:auto" + "inotify:auto" "bundle:no" + "dvbcsa:no" ) # # Begin # -. $ROOTDIR/support/configure.inc +. "$ROOTDIR/support/configure.inc" parse_args $* # ########################################################################### # Checks # ########################################################################### +echo "Checking support/features" + # # Compiler # @@ -47,6 +55,11 @@ check_cc_option sse2 check_cc_snippet getloadavg '#include void test() { getloadavg(NULL,0); }' +check_cc_snippet atomic64 '#include +uint64_t test(uint64_t *ptr){ +return __sync_fetch_and_add(ptr, 1); +}' + # # Python # @@ -98,22 +111,86 @@ if enabled_or_auto avahi; then fi fi +# +# libav +# +if enabled_or_auto libav; then + has_libav=true + + if $has_libav && ! check_pkg libavcodec "<=55.0.0"; then + has_libav=false + fi + + if $has_libav && ! check_pkg libavcodec ">=52.96.0"; then + has_libav=false + fi + + if $has_libav && ! check_pkg libavutil ">=50.43.0"; then + has_libav=false + fi + + if $has_libav && ! check_pkg libavformat "<=55.0.0"; then + has_libav=false + fi + + if $has_libav && ! check_pkg libavformat ">=53.10.0"; 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 + die "libav development support not found (use --disable-libav)" + fi +fi + +# +# Inotify +# +if enabled_or_auto inotify; then + if check_cc_header "sys/inotify" inotify_h; then + enable inotify + elif enabled inotify; then + die "Inotify support not found (use --disable-inotify)" + fi +fi + +# +# libdvbcsa +# +if enabled cwc && enabled dvbcsa; then + (check_cc_header "dvbcsa/dvbcsa" dvbcsa_h &&\ + check_cc_lib dvbcsa dvbcsa_l) ||\ + die "Failed to find dvbcsa support (use --disable-dvbcsa)" + LDFLAGS="$LDFLAGS -ldvbcsa" +fi + +# +# Icon caching +# +if enabled_or_auto imagecache; then + if check_pkg libcurl; then + enable imagecache + elif enabled imagecache; then + die "Libcurl support not found (use --disable-imagecache)" + fi +fi + # # DVB scan # if enabled linuxdvb && enabled dvbscan; then - if [ ! -d ${ROOTDIR}/data/dvb-scan ]; then - if ! enabled bin_bzip2; then - die "Bzip2 not found, fetch dvb-scan files (use --disable-dvbscan)" - fi - echo -n "Fetching dvb-scan files... " - if ${ROOTDIR}/support/getmuxlist &> /dev/null; then - echo "done" - else - echo - die "Failed to fetch dvb-scan files (use --disable-dvbscan to skip)" - fi + printf "${TAB}" "fetching dvb-scan files ..." + "${ROOTDIR}/support/getmuxlist" + if [ $? -ne 0 ]; then + echo "fail" + die "Failed to fetch dvb-scan data (use --disable-dvbscan)" fi + echo "ok" fi # ########################################################################### @@ -122,7 +199,7 @@ fi # Write config write_config -cat >> ${CONFIG_H} <> "${CONFIG_H}" </dev/null 2>&1 -} - - -case "$1" in - start) - rh_status_q && exit 0 - $1 - ;; - stop) - rh_status_q || exit 0 - $1 - ;; - restart) - $1 - ;; - reload) - rh_status_q || exit 7 - $1 - ;; - force-reload) - force_reload - ;; - status) - rh_status - ;; - condrestart|try-restart) - rh_status_q || exit 0 - restart - ;; - *) - echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}" - exit 2 -esac -exit $? - diff --git a/contrib/redhat/tvheadend.spec b/contrib/redhat/tvheadend.spec deleted file mode 100644 index dd2fa8c4..00000000 --- a/contrib/redhat/tvheadend.spec +++ /dev/null @@ -1,75 +0,0 @@ -Name: tvheadend -Summary: TV streaming server -Version: 2.12.cae47cf -Release: 1%{dist} -License: GPL -Group: Applications/Multimedia -URL: http://www.lonelycoder.com/tvheadend -Packager: Jouk Hettema -Source: tvheadend-%{version}.tar.bz2 -Prefix: /usr -BuildRequires: avahi-devel, openssl, glibc, zlib - -%description -Tvheadend is a TV streaming server for Linux supporting DVB-S, DVB-S2, -DVB-C, DVB-T, ATSC, IPTV, and Analog video (V4L) as input sources. - -%prep -%setup -q - -%build -%configure --release --prefix=%{prefix}/bin -%{__make} - -%install -%{__rm} -rf %{buildroot} - -mkdir -p $RPM_BUILD_ROOT/%{prefix} - -%{__install} -d -m0755 %{buildroot}%{prefix}/bin -%{__install} -d -m0755 %{buildroot}/etc/tvheadend -%{__install} -d -m0755 %{buildroot}/etc/sysconfig -%{__install} -d -m0755 %{buildroot}/etc/rc.d/init.d -%{__install} -d -m0755 %{buildroot}%{prefix}/shared/man/man1 - -%{__install} -m0755 build.Linux/tvheadend %{buildroot}%{prefix}/bin/ -%{__install} -m0755 man/tvheadend.1 %{buildroot}%{prefix}/shared/man/man1 -%{__install} -m0755 contrib/redhat/tvheadend %{buildroot}/etc/rc.d/init.d - -cat >> %{buildroot}/etc/sysconfig/tvheadend << EOF -OPTIONS="-f -u tvheadend -g tvheadend -c /etc/tvheadend -s -C" -EOF - -%pre -groupadd -f -r tvheadend >/dev/null 2>&! || : -useradd -s /sbin/nologin -c "Tvheadend" -M -r tvheadend -g tvheadend -G video >/dev/null 2>&! || : - -%preun -if [ $1 = 0 ]; then - /sbin/service tvheadend stop >/dev/null 2>&1 - /sbin/chkconfig --del tvheadend -fi - -%post -/sbin/chkconfig --add tvheadend - -if [ "$1" -ge "1" ]; then - /sbin/service tvheadend condrestart >/dev/null 2>&1 || : -fi - -%clean -%{__rm} -rf %{buildroot} - -%files -%defattr(-,root,root) -%doc debian/changelog LICENSE README -%dir %attr(-,tvheadend,root) %{prefix} -%{prefix}/bin/tvheadend -%{prefix}/shared/man/man1/tvheadend.1* -%{_sysconfdir}/rc.d/init.d/tvheadend -/etc/tvheadend -%config /etc/sysconfig/tvheadend - -%changelog -* Fri Feb 18 2011 Jouk Hettema - 2.12.cae47cf -- initial build for fedora 14 diff --git a/data/conf/charset b/data/conf/charset deleted file mode 100644 index 01fd4fcf..00000000 --- a/data/conf/charset +++ /dev/null @@ -1,408 +0,0 @@ -[ - { - "tsid": 1048, - "onid": 1, - "charset": "PL_AUTO", - "sid": 4310, - "description": "Astra 19.2E TV Trwam" - }, - { - "tsid": 1059, - "onid": 1, - "charset": "PL_AUTO", - "sid": 0, - "description": "Astra 19.2E TVP" - }, - { - "tsid": 1111, - "onid": 1, - "charset": "PL_AUTO", - "sid": 7269, - "description": "Astra 19.2E TV Trwam" - }, - { - "tsid": 8100, - "onid": 156, - "charset": "PL_AUTO", - "sid": 1500, - "description": "Eurobird 9.0E TV Polonia" - }, - { - "tsid": 8300, - "onid": 156, - "charset": "PL_AUTO", - "sid": 620, - "description": "Eurobird 9.0E TVN International" - }, - { - "tsid": 200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13834, - "description": "Hotbird 13.0 Eutelsat Eurosport PL" - }, - { - "tsid": 200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13864, - "description": "Hotbird 13.0 Eutelsat Eurosport 2 PL" - }, - { - "tsid": 200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13865, - "description": "Hotbird 13.0 Eutelsat Eurosport PL" - }, - { - "tsid": 200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13878, - "description": "Hotbird 13.0 Eutelsat Eurosport 2 NE PL" - }, - { - "tsid": 300, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 ITI/Nka HD PL" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13020, - "description": "Hotbird 13.0 Cyfra+ Canal+ HD Polska" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13022, - "description": "Hotbird 13.0 Cyfra+ Canal+ Sport HD Polska" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13023, - "description": "Hotbird 13.0 Cyfra+ National Geographic HD Polska" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13025, - "description": "Hotbird 13.0 Cyfra+ Filmbox HD" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13026, - "description": "Hotbird 13.0 Cyfra+ AXN Spin HD" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13027, - "description": "Hotbird 13.0 Cyfra+ TVN 7 HD" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13070, - "description": "Hotbird 13.0 Cyfra+ Eurosport HD PL" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13080, - "description": "Hotbird 13.0 Cyfra+ Eurosport HD PL" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13081, - "description": "Hotbird 13.0 Cyfra+ Eurosport HD PL" - }, - { - "tsid": 400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13082, - "description": "Hotbird 13.0 Cyfra+ Eurosport HD PL" - }, - { - "tsid": 1000, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 ITI/Nka TVN" - }, - { - "tsid": 1100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfra+" - }, - { - "tsid": 1300, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 ITI/Nka" - }, - { - "tsid": 1400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 30, - "description": "Hotbird 13.0 Nick Junior" - }, - { - "tsid": 1400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 31, - "description": "Hotbird 13.0 Nickelodeon HD" - }, - { - "tsid": 1500, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfra+" - }, - { - "tsid": 1600, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 ITI/Nka EskaTV, TVN" - }, - { - "tsid": 1800, - "onid": 200, - "charset": "PL_AUTO", - "sid": 3623, - "description": "Hotbird 13.0 Polo TV" - }, - { - "tsid": 7400, - "onid": 113, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfrowy Polsat" - }, - { - "tsid": 7700, - "onid": 318, - "charset": "PL_AUTO", - "sid": 117, - "description": "Hotbird 13.0 Hot TV PL" - }, - { - "tsid": 7800, - "onid": 113, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfrowy Polsat" - }, - { - "tsid": 7900, - "onid": 113, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfrowy Polsat" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14900, - "description": "Hotbird 13.0 Eutelsat Sport Klub PL" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14901, - "description": "Hotbird 13.0 Eutelsat Universal CE" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14902, - "description": "Hotbird 13.0 Eutelsat Sci-Fi CE" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14910, - "description": "Hotbird 13.0 Eutelsat Sport Klub PL" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14911, - "description": "Hotbird 13.0 Eutelsat Universal CE" - }, - { - "tsid": 8100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 14912, - "description": "Hotbird 13.0 Eutelsat Sci-Fi CE" - }, - { - "tsid": 11000, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfra+" - }, - { - "tsid": 11100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 4661, - "description": "Hotbird 13.0 Eutelsat Wedding TV" - }, - { - "tsid": 11200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Polsat Cyfrowy PPV" - }, - { - "tsid": 11400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfra+" - }, - { - "tsid": 11600, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 ITI/Nka" - }, - { - "tsid": 11900, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfra+" - }, - { - "tsid": 12000, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Polsat Cyfrowy" - }, - { - "tsid": 12200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7457, - "description": "Hotbird 13.0 Globecast Cartoon Network PL / TCM CE" - }, - { - "tsid": 12200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7466, - "description": "Hotbird 13.0 Globecast Disney Channel PL" - }, - { - "tsid": 12200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7467, - "description": "Hotbird 13.0 Globecast Cartoon Network CE" - }, - { - "tsid": 12200, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7468, - "description": "Hotbird 13.0 Globecast TCM CE" - }, - { - "tsid": 12800, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Viacom MTV / VH1 Polska" - }, - { - "tsid": 13000, - "onid": 318, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Globecast Orange PL" - }, - { - "tsid": 13100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7322, - "description": "Hotbird 13.0 TV5 Monde Europe" - }, - { - "tsid": 13100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7324, - "description": "Hotbird 13.0 Crime & Investigation" - }, - { - "tsid": 13100, - "onid": 318, - "charset": "PL_AUTO", - "sid": 7325, - "description": "Hotbird 13.0 Crime & Investigation" - }, - { - "tsid": 13200, - "onid": 113, - "charset": "PL_AUTO", - "sid": 0, - "description": "Hotbird 13.0 Cyfrowy Polsat" - }, - { - "tsid": 13400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 4754, - "description": "Hotbird 13.0 TVR" - }, - { - "tsid": 15400, - "onid": 318, - "charset": "PL_AUTO", - "sid": 13511, - "description": "Hotbird 13.0 Rebel TV" - } - { - "tsid": 15700, - "onid": 318, - "charset": "PL_AUTO", - "sid": 10626, - "description": "Hotbird 13.0 Disco TV" - } -] diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 78fa0626..00000000 --- a/debian/changelog +++ /dev/null @@ -1,6 +0,0 @@ -tvheadend (3.3) unstable; urgency=low - - * The full changelog can be found at - http://www.lonelycoder.com/tvheadend/download - - -- Andreas Öman Tue, 18 Sep 2012 12:45:49 +0100 diff --git a/debian/control b/debian/control index 060e7757..29435c2a 100644 --- a/debian/control +++ b/debian/control @@ -1,8 +1,8 @@ Source: tvheadend Section: video Priority: extra -Maintainer: Andreas Öman -Build-Depends: debhelper (>= 7.0.50), pkg-config, libavahi-client-dev, libssl-dev, zlib1g-dev, wget, bzip2 +Maintainer: Andreas Öman +Build-Depends: debhelper (>= 7.0.50), pkg-config, libavahi-client-dev, libssl-dev, zlib1g-dev, wget, bzip2, libcurl4-gnutls-dev, git-core Standards-Version: 3.7.3 Package: tvheadend @@ -11,7 +11,7 @@ Depends: ${shlibs:Depends}, libavahi-client3, zlib1g Recommends: xmltv-util Enhances: showtime Replaces: hts-tvheadend -Homepage: http://www.lonelycoder.com/tvheadend +Homepage: https://tvheadend.org Description: Tvheadend Tvheadend is a TV streaming server for Linux supporting DVB, ATSC, IPTV, and Analog video (V4L) as input sources. Can be used as a backend to Showtime, XBMC and various other clients. diff --git a/debian/rules b/debian/rules index 9b6d1783..20a70098 100755 --- a/debian/rules +++ b/debian/rules @@ -5,7 +5,7 @@ export DH_VERBOSE=1 dh $@ override_dh_auto_configure: - dh_auto_configure -- ${JOBSARGS} + dh_auto_configure -- ${AUTOBUILD_CONFIGURE_EXTRA} ${JOBSARGS} override_dh_auto_build: make ${JARGS} diff --git a/debian/tvheadend.default b/debian/tvheadend.default index a8d7c48a..29b6dbef 100644 --- a/debian/tvheadend.default +++ b/debian/tvheadend.default @@ -25,10 +25,20 @@ TVH_CONF_DIR="" # set as "0,1" TVH_ADAPTERS="" +# TVH_IPV6 +# if set to 1 will enable IPv6 support +TVH_IPV6=0 + # TVH_HTTP_PORT # if set to "" will use binary default TVH_HTTP_PORT="" +# TVH_HTTP_ROOT +# if set to "" will use binary default +# else will change the webui root context, useful for proxied +# servers +TVH_HTTP_ROOT="" + # TVH_HTSP_PORT # if set to "" will use binary default TVH_HTSP_PORT="" @@ -37,6 +47,10 @@ TVH_HTSP_PORT="" # if set to 1 will output debug to syslog TVH_DEBUG=0 +# TVH_DELAY +# if set startup will be delayed N seconds to allow hardware init +TVH_DELAY="" + # TVH_ARGS # add any other arguments TVH_ARGS="" diff --git a/debian/tvheadend.init b/debian/tvheadend.init index 8a24bdd8..b4aea9cb 100644 --- a/debian/tvheadend.init +++ b/debian/tvheadend.init @@ -33,8 +33,10 @@ ARGS="-f" [ -z "$TVH_GROUP" ] || ARGS="$ARGS -g $TVH_GROUP" [ -z "$TVH_CONF_DIR" ] || ARGS="$ARGS -c $TVH_CONF_DIR" [ -z "$TVH_ADAPTERS" ] || ARGS="$ARGS -a $TVH_ADAPTERS" -[ -z "$TVH_HTTP_PORT" ] || ARGS="$ARGS -w $TVH_HTTP_PORT" -[ -z "$TVH_HTSP_PORT" ] || ARGS="$ARGS -e $TVH_HTSP_PORT" +[ "$TVH_IPV6" = "1" ] && ARGS="$ARGS -6" +[ -z "$TVH_HTTP_PORT" ] || ARGS="$ARGS --http_port $TVH_HTTP_PORT" +[ -z "$TVH_HTTP_ROOT" ] || ARGS="$ARGS --http_root $TVH_HTTP_ROOT" +[ -z "$TVH_HTSP_PORT" ] || ARGS="$ARGS --htsp_port $TVH_HTSP_PORT" [ "$TVH_DEBUG" = "1" ] && ARGS="$ARGS -s" # Load the VERBOSE setting and other rcS variables @@ -83,6 +85,7 @@ do_stop() case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + [ ! -z "$TVH_DELAY" ] && sleep $TVH_DELAY do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; diff --git a/debian/tvheadend.upstart b/debian/tvheadend.upstart index 37b76b85..a5d70404 100644 --- a/debian/tvheadend.upstart +++ b/debian/tvheadend.upstart @@ -22,9 +22,13 @@ script [ -z "$TVH_GROUP" ] || ARGS="$ARGS -g $TVH_GROUP" [ -z "$TVH_CONF_DIR" ] || ARGS="$ARGS -c $TVH_CONF_DIR" [ -z "$TVH_ADAPTERS" ] || ARGS="$ARGS -a $TVH_ADAPTERS" - [ -z "$TVH_HTTP_PORT" ] || ARGS="$ARGS -w $TVH_HTTP_PORT" - [ -z "$TVH_HTSP_PORT" ] || ARGS="$ARGS -e $TVH_HTSP_PORT" + [ "$TVH_IPV6" = "1" ] && ARGS="$ARGS -6" + [ -z "$TVH_HTTP_PORT" ] || ARGS="$ARGS --http_port $TVH_HTTP_PORT" + [ -z "$TVH_HTTP_ROOT" ] || ARGS="$ARGS --http_root $TVH_HTTP_ROOT" + [ -z "$TVH_HTSP_PORT" ] || ARGS="$ARGS --htsp_port $TVH_HTSP_PORT" [ "$TVH_DEBUG" = "1" ] && ARGS="$ARGS -s" + [ ! -z "$TVH_DELAY" ] && sleep $TVH_DELAY + exec tvheadend $ARGS $TVH_ARGS end script diff --git a/docs/docresources/configcapmt.png b/docs/docresources/configcapmt.png index 5b11d8b4..75894092 100644 Binary files a/docs/docresources/configcapmt.png and b/docs/docresources/configcapmt.png differ diff --git a/docs/html/config_access.html b/docs/html/config_access.html index 0eddf860..ef6c5fcb 100644 --- a/docs/html/config_access.html +++ b/docs/html/config_access.html @@ -6,7 +6,7 @@ wide open.

-When Tvheadend verifies access is scan thru all the enabled access control entries. +When Tvheadend verifies access is scan through all the enabled access control entries. The permission flags are combined for all matching access entries. An access entry is said to match if the username / password matches and the IP source address of the requesting peer is within the prefix. @@ -42,7 +42,7 @@ The columns have the following functions:

Username
Name of user, if no username is needed for match it should contain a - single asterix (*). + single asterisk (*).
Password
diff --git a/docs/html/config_capmt.html b/docs/html/config_capmt.html index 23c5e8c1..26e04a48 100644 --- a/docs/html/config_capmt.html +++ b/docs/html/config_capmt.html @@ -49,6 +49,15 @@ This module will communicate the received control-words back to Tvheadend via Port 9000 +
OSCam mode +
If selected, connection will be made directly to oscam without using LD_PRELOAD hack.
+ Port 9000 will be used automatically.
+ The following lines are required in [dvbapi] section of oscam.conf file: +
+
boxtype = pc
+ pmt_mode = 4 +
+
Comment
Allows the administrator to set a comment only visible in this editor. It does not serve any active purpose. diff --git a/docs/html/config_channels.html b/docs/html/config_channels.html index 022dbe59..5062c39e 100644 --- a/docs/html/config_channels.html +++ b/docs/html/config_channels.html @@ -3,7 +3,7 @@
- The channels are listed / edited in a grid. + The channels are listed / edited in a grid.
  • To edit a cell, double-click on it. After a cell is changed it @@ -37,7 +37,7 @@ player.
    EPG Grab Source -
    Name of the Internet based EPG provider (typically XMLTV) channel +
    Name of the Internet-based EPG provider (typically XMLTV) channel that should be used to update this channels EPG info. By default Tvheadend tries to match the name itself, but sometimes it might not match correctly in which case you can do the mapping diff --git a/docs/html/config_dvb.html b/docs/html/config_dvb.html index 5ef40f06..1f713325 100644 --- a/docs/html/config_dvb.html +++ b/docs/html/config_dvb.html @@ -53,7 +53,7 @@ This will attempt to close all available device handles. This can be necessary to allow some devices to go into low power states.
    - However this option has been known to cause problems with some multi tuner + However, this option has been known to cause problems with some multi-tuner DVB cards. If you have signal problems, try disabling this option.
    Note: this option has no effect if idle scanning is enabled. @@ -110,8 +110,8 @@
    Turn off LNB when idle
    This option can be enabled to disable the power to the LNB when the adapter - is not in use. This can reduce power consumption, however for poorly shieled - multi tuner setups you may some inteference when the LNB is re-enabled. + is not in use. This can reduce power consumption, however for poorly shielded + multi-tuner setups you may have some interference when the LNB is re-enabled. @@ -143,25 +143,25 @@ temporary broken
    Network -
    Network name as given in the DVB stream. Can not be changed +
    Network name as given in the DVB stream. Cannot be changed
    Frequency -
    Center frequency for the mux. Can not be changed +
    Center frequency for the mux. Cannot be changed
    Modulation -
    Information about the modulation used on the mux. Can not be changed +
    Information about the modulation used on the mux. Cannot be changed
    Polarisation -
    Information about the polarisation used on the mux. Can not be changed +
    Information about the polarisation used on the mux. Cannot be changed
    Satellite config (DVB-S only) -
    The satellite configuration in use on this mux. Can not be changed. +
    The satellite configuration in use on this mux. Cannot be changed.
    Frontend status -
    The status of the frontend signal last time the mux was tuned. Can not be changed +
    The status of the frontend signal last time the mux was tuned. Cannot be changed
    Mux id -
    Unique ID for this mux in the dvb network. Can not be changed +
    Unique ID for this mux in the dvb network. Cannot be changed
    Quality
    Tvheadend's estimated quality for the mux. @@ -180,7 +180,7 @@ 'Save changes' button. In order to change a Checkbox cell you only have to click once in it. -
  • Service can not be deleted since they are directly inherited / +
  • Service cannot be deleted since they are directly inherited / discovered from a mux they will reappear in just a few seconds should one delete them.
@@ -194,7 +194,7 @@ temporary broken
Service name -
Service name as given in the DVB stream. Can not be changed +
Service name as given in the DVB stream. Cannot be changed
Play
Open the VLC plugin window to play this service. @@ -210,13 +210,13 @@ charset to use when none is specified by the broadcaster.
EPG -
Uncheck this if EPG data should not be retreived for this service. +
Uncheck this if EPG data should not be retrieved for this service.
Type -
Type of service. Can not be changed +
Type of service. Cannot be changed
Provider -
Provider as given in the DVB stream. Can not be changed +
Provider as given in the DVB stream. Cannot be changed
Network
Network name for the mux this service resides on @@ -257,6 +257,10 @@
Switchport
Port number to select for this configuration (numbering begins at 0). +
In DiSEqC 1.0/2.0 mode, ports 0-3 are valid. +
In DiSEqC 1.1/2.1 mode, ports 0-63 are valid. +
Use numbers 0-3 for LNBs behind first input on the uncommitted switch, + then 4-7 and so on to support up to 64 ports using DiSEqC 1.1/2.1.
LNB type
Select the LNB type from the list of supported LNBs. If your LNB diff --git a/docs/html/config_dvr.html b/docs/html/config_dvr.html index 05d6b8cc..2b72e8a0 100644 --- a/docs/html/config_dvr.html +++ b/docs/html/config_dvr.html @@ -74,6 +74,10 @@
If checked, media containers that support metadata will be tagged with the metadata associated with the event being recorded. +
Skip commercials +
If checked, commercials will be dropped from the recordings. At the + moment, commercial detection only works for the swedish channel TV4. +
Post-processor command
Command to run after finishing a recording. The command will be run in background and is executed even if a recording is aborted diff --git a/docs/html/config_epggrab.html b/docs/html/config_epggrab.html index 222e4ce6..7fbb33c2 100644 --- a/docs/html/config_epggrab.html +++ b/docs/html/config_epggrab.html @@ -1,28 +1,51 @@

- This tab is used to configure EPG grabbing capabilities. TVheadend supports - a variety of different EPG grabbing mechanisms. These fall into 3 broad - categories, within which there are a variety of specific grabber - implementations. + This tab is used to configure the Electronic Program Guide (EPG) grabbing + capabilities. Tvheadend supports a variety of different EPG grabbing + mechanisms. These fall into 3 broad categories, within which there are a + variety of specific grabber implementations.

Grabber Types

    -
  • Over-the-Air (OTA) - These grabbers receive EPG data directly from the DVB network. This is often the easiest way to get up and running and does provide timely updates should scheduling change. However the information isn't always as rich as some of the other grabbers. -
  • Internal - These are grabbers which can be internally initiated from within TVheadend using a very simple scheduler. These are typically Internet based services. This can be a quick way to get richer EPG data where you don't have decent OTA support. -
  • External - These provide the option to run grabber scripts externally and to send data into TVheadend via Unix domain sockets. It provides the ability to run more complex configurations using things like cronjob's, script chains, etc. +
  • Over-the-Air (OTA) - These grabbers receive EPG data directly from the + DVB network. This is often the easiest way to get up and running and does + provide timely updates should scheduling change. However, the information + isn't always as rich as some of the other grabbers. +
  • Internal - These are grabbers which can be internally initiated from + within Tvheadend using a very simple scheduler. These are typically + Internet-based services. This can be a quick way to get richer EPG data + where you don't have decent OTA support. +
  • External - These provide the option to run grabber scripts externally and + to send data into Tvheadend via Unix domain sockets. It provides the ability + to run more complex configurations using things like cronjob's, script + chains, etc.

Grabber Modules

    -
  • EIT - This is a DVB standards compatible EIT grabber. Typically it will - retrieve now/next information, though on some networks there may be more +
  • EIT - This is a DVB standards compatible EIT grabber. Typically it + will retrieve now/next information, though on some networks there may be more extensive data published. -
  • Freesat/view - This is an extended version of EIT that is used by the Free-to-air DVB providers in the UK. It includes additional information such as series links and episode identifiers. -
  • OpenTV - This is a proprietary OTA EPG grabber. Its known to be used on the SKY networks, but others may use it. You need two configuration files to define settings for your particular network, if you don't see yours listed please visit IRC #hts for help. -
  • XMLTV - This is am Internet based suite of scripts, for more information about XMLTV please visit http://www.xmltv.org. To make use of the internal XMLTV grabber you typically require the xmltv-utils package to be installed. If you install new grabbers you will need to restart TVheadend to pick these up as they're loaded at startup. If you see no XMLTV grabbers listed then most probably XMLTV is not properly installed and in the PATH. -
  • PyEPG - This is another Internet based scraper. It currently only supports the Atlas UK system (for which you need a key), but it does provide a very rich EPG data set. For more information see http://github.com/adamsutton/PyEPG.
  • +
  • Freesat/view - This is an extended version of EIT that is used by the + Free-to-air DVB providers in the UK. It includes additional information such + as series links and episode identifiers. +
  • OpenTV - This is a proprietary OTA EPG grabber. It's known to be used on + the SKY networks, but others may use it. You need two configuration files to + define settings for your particular network, if you don't see yours listed + please visit IRC #hts for help. +
  • XMLTV - This is am Internet-based suite of scripts, for more information + about XMLTV please visit http://www.xmltv.org. + To make use of the internal XMLTV grabber you typically require the xmltv-utils + package to be installed. If you install new grabbers you will need to + restart Tvheadend to pick these up as they're loaded at startup. If you see + no XMLTV grabbers listed then most probably XMLTV is not properly installed + and in the PATH. +
  • PyEPG - This is another Internet-based scraper. It currently only + supports the Atlas UK system (for which you need a key), but it does provide + a very rich EPG data set. For more information see + http://github.com/adamsutton/PyEPG.

Configuration options

@@ -38,6 +61,12 @@
Update channel name
Automatically update channel icons using information provided by the enabled EPG providers. +
Periodic save EPG to disk Interval +
Writes the current in-memory EPG database to disk every x Hours + (user defined), so should a crash/unexpected shutdown occur EPG + data is saved periodically to the database (Re-read on + next startup) + Set to 0 to disable.

Internal Grabber

@@ -46,7 +75,7 @@
Select which internal grabber to use.
Grab interval -
Time period between grabs. Value and unit are indepdently set. +
Time period between grabs. Value and unit are independently set.

Over-the-air Grabbers

diff --git a/docs/html/config_misc.html b/docs/html/config_misc.html index 00e00b07..da07d690 100644 --- a/docs/html/config_misc.html +++ b/docs/html/config_misc.html @@ -19,6 +19,60 @@ Select the path to use for DVB scan configuration files. Typically dvb-apps stores these in /usr/share/dvb/. Leave blank to use TVH's internal file set. + + +

+ Icon caching - this will cache any channel icons or other images (such as + EPG metadata). These will then be served from the local webserver, this + can be useful for multi-client systems and generally to reduce hits on + upstream providers. +

+ +
+ +
Enabled +
+ Select whether or not to enable caching. Note: even with this disabled + you can still specify local (file://) icons and these will be served by + the built-in webserver. + +
Re-fetch period (hours) +
+ How frequently the upstream provider is checked for changes. + +
Re-try period (hours) +
+ How frequently it will re-try fetching an image that has failed to be + fetched. + +
Ignore invalid SSL certificates +
Ignore invalid/unverifiable (expired, self-certified, etc.) certificates + +
+ +

+ Time Update - TVH now has a built-in capability to update the systme time. + However you should bare in mind that DVB time is not highly accurate and is + prone to both jitter and variation between different transponders. +
+ Where possible its probably still better to use an internet based NTP source + to synchronise the system clock. +

+ +
+
Update time +
Enable system time updates, this will only work if the user + running TVH has rights to update the system clock (normally only root). + +
Enable NTP driver +
This will create an NTP driver (using shmem interface) that you can feed + into ntpd. This can be run without root priviledges, but generally the + performance is not that great. + +
Update tolerance (milliseconds) +
Only update the system clock (doesn't affect NTP driver) if the delta + between the system clock and DVB time is greater than this. This can help + stop horrible oscillations on the system clock.
diff --git a/docs/html/config_timeshift.html b/docs/html/config_timeshift.html new file mode 100644 index 00000000..c664a986 --- /dev/null +++ b/docs/html/config_timeshift.html @@ -0,0 +1,49 @@ +
+ +

+ This tab is used to configure timeshift properties. + +

+ Configuration options: +

+
Enabled +
Turn on and off timeshift. + +
On-Demand +
Turn this on to start timeshift buffer on pause. In this mode + you cannot rewind the buffer (it always begins on the currently playing + frame). + + Without this option there will be a permanent, circular, buffer up to + the limits defined below. + +
Storage Path: +
Where the timeshift data will be stored. If nothing is specified this + will default to CONF_DIR/timeshift/buffer + +
Max. Period (mins): +
Specify the maximum time period that will be buffered for any given + (client) subscription. + +
Unlimited: +
If checked, this allows the timeshift buffer to grow unbounded until + your storage media runs out of space (WARNING: this could be dangerous!). + +
Max. Size (MegaBytes) +
Specifies the maximum combined size of all timeshift buffers. If you + specify an unlimited period its highly recommended you specifying a value + here. + +
Unlimited: +
If checked, this allows the combined size of all timeshift buffers to + potentially grow unbounded until your storage media runs out of space + (WARNING: this could be dangerous!). + +
+ Changes to any of these settings must be confirmed by pressing the + 'Save configuration' button before taking effect. + + NOTE: These settings represent server side maximums, however the clients can + request smaller buffers or even not to create a buffer at all (for example + should they not support timeshifting). +
diff --git a/docs/html/dvrlog.html b/docs/html/dvrlog.html index 1b2de7c3..d29b53aa 100644 --- a/docs/html/dvrlog.html +++ b/docs/html/dvrlog.html @@ -1,6 +1,12 @@
-The DVR log displays a paged grid containing all scheduled, current -and completed recordings. The list is sorted based on start time. +The DVR log is split into a series of paged grids: + +
    +
  • Upcoming Recordings - stuff scheduled to be recorded in the future +
  • Finished Recordings - stuff that has succesfully finished recording +
  • Failed Recordings - stuff that failed to record +
+

Use the bottom toolbar (not displayed in this manual) to navigate between pages in the grid. diff --git a/docs/html/epg.html b/docs/html/epg.html index c11bc718..13118c8f 100644 --- a/docs/html/epg.html +++ b/docs/html/epg.html @@ -62,7 +62,7 @@ sorted based on start time. [Record series] button that will record all entries in the series.

For events without any series link information, a [Autorec] button will be - provided to create a pseudo series link using hte Autorec feature. + provided to create a pseudo series link using the Autorec feature.

diff --git a/docs/html/features.html b/docs/html/features.html index b1760939..7e7dc8ff 100644 --- a/docs/html/features.html +++ b/docs/html/features.html @@ -16,7 +16,7 @@

Multicasted IPTV.
Both raw transport streams in UDP and transport stream in RTP in UDP - is supported. The precense of RTP is autodetected. + is supported. The presence of RTP is autodetected.
Analog TV
diff --git a/docs/html/install.html b/docs/html/install.html index a6fc34af..569ee74b 100644 --- a/docs/html/install.html +++ b/docs/html/install.html @@ -38,7 +38,7 @@ Parts of this documentation is also available in the Tvheadend man page. access to all features / settings in the web user interface. If this is the first time you setup Tvheadend you are most encouraged to enter the web user interface, selected the 'Configuration' + 'Access Control' - tab and make reasonable changes. Futher help / documentation can be obtained + tab and make reasonable changes. Further help / documentation can be obtained inside the web interface.
Settings storage @@ -47,7 +47,7 @@ Parts of this documentation is also available in the Tvheadend man page.
Logging
- All activity inside tvheadend is logged to syslog using log facility. + All activity inside Tvheadend is logged to syslog using log facility. Also, if logged in to the web interface you will receive the same log in the bottom tab (System log). diff --git a/docs/html/overview.html b/docs/html/overview.html index 437dab8c..36de92be 100644 --- a/docs/html/overview.html +++ b/docs/html/overview.html @@ -1,16 +1,8 @@

- -

-

HTS Tvheadend 3.2

-© 2006 - 2012, Andreas Öman, et al.

+

Tvheadend

+© 2006 - 2013, Andreas Öman, et al.

- -Tvheadend is part of the HTS project hosted at -http://www.lonelycoder.com/hts -

-It functions primarily as a TV streaming back end for the Showtime Mediaplayer but -can be used standalone for other purposes, such as a Digital Video Recorder.

diff --git a/docs/html/sysreq.html b/docs/html/sysreq.html index 0f908e07..6aab8579 100644 --- a/docs/html/sysreq.html +++ b/docs/html/sysreq.html @@ -1,16 +1,9 @@

-If you want to build tvheadend from source, please visit -this page. -Please notice that wiki development site only reflects the work -in HEAD (-current). It should not deviate much should you want to +If you want to build Tvheadend from source, please visit +this page. +Please note: that wiki development site only reflects the work +in master. It should not deviate much should you want to build a released version from source, but you never know.

- -Tvheadend is part of the HTS project hosted at -http://hts.lonelycoder.com/. - -

-It functions primarily as a TV backend for the Showtime Media player but -can be used standalone for other purposes.

diff --git a/lib/py/tvh/htsmsg.py b/lib/py/tvh/htsmsg.py index 2c291790..dc7d5088 100644 --- a/lib/py/tvh/htsmsg.py +++ b/lib/py/tvh/htsmsg.py @@ -129,8 +129,12 @@ def serialize ( msg ): return int2bin(cnt) + binary_write(msg) # Deserialize an htsmsg -def deserialize0 ( data ): - msg = {} +def deserialize0 ( data, typ = HMF_MAP ): + islist = False + msg = {} + if (typ == HMF_LIST): + islist = True + msg = [] while len(data) > 5: typ = ord(data[0]) nlen = ord(data[1]) @@ -152,10 +156,13 @@ def deserialize0 ( data ): item = (item << 8) | ord(data[i]) i = i - 1 elif typ in [ HMF_LIST, HMF_MAP ]: - item = deserialize0(data[:dlen]) + item = deserialize0(data[:dlen], typ) else: raise Exception('invalid data type %d' % typ) - msg[name] = item + if islist: + msg.append(item) + else: + msg[name] = item data = data[dlen:] return msg diff --git a/man/tvheadend.1 b/man/tvheadend.1 index 47fb3c5a..c8648bf7 100644 --- a/man/tvheadend.1 +++ b/man/tvheadend.1 @@ -13,21 +13,82 @@ Media player. .SH OPTIONS All arguments are optional. .TP +\fB\-v\fR, \fB\-\-version\fR +Show version information. +.TP +\fB\-h\fR, \fB\-\-help\fR +Show built-in help information (may be more up to date). +.TP +\fB\-c\fR, \fB\-\-config\fR +Specify an alternate config path; the default is \fI${HOME}/.hts\fR +.TP \fB\-f Fork and become a background process (deamon). Default no. .TP -\fB\-u \fR\fIusername\fR +\fB\-u\fR \fIusername\fR, \fB\-\-user\fR \fIusername\fR Run as user \fIusername\fR. Only applicable if daemonizing. Default is to use the uid of 1 (daemon on most systems). .TP -\fB\-g \fR\fIgroupname\fR +\fB\-g\fR \fIgroupname\fR, \fB\-\-group \fR\fIgroupname\fR Run as group \fR\fIgroupname\fR. Only applicable if daemonizing. Default is to use the 'video' group. If the 'video' group does not exist, gid 1 (daemon) will be used. .TP -\fB\-C +\fB\-p\fR \fIpidpath\fR, \fB\-\-pid \fR\fIpidpath\fR +Specify alternative PID path file (default /var/run/tvheadend.pid). +.TP +\fB\-C\fR, \fB\-\-firstrun\fR If no useraccount exist then create one with no username and no password. Use with care as it will allow world-wide administrative access to your Tvheadend installation until you edit the access-control from within the Tvheadend UI. +.TP +\fB\-a\fR, \fB\-\-adapters\fR +Only use specified DVB adapters (comma separated). +.TP +\fB\-6\fR, \fB\-\-ipv6\fR +Listen on IPv6. +.TP +\fB\-b\fR \fIaddress\fR, \fB\-\-bindaddr\fR \fIaddress\fR +Specify an interface IP address on which incoming HTTP and HTSP connections +will be accepted. By default, connections are accepted on all interfaces. +.TP +\fB\-\-http_port +Specify alternative http port (default 9881). +.TP +\fB\-\-http_root +Specify alternative http webroot. +.TP +\fB\-\-htsp_port +Specify alternative htsp port (default 9882). +.TP +\fB\-\-htsp_port2 +Specify extra htsp port. +.TP +\fB\-d\fR, \fB\-\-debug\fR +Enable all debug. +.TP +\fB\-s\fR, \fB\-\-syslog\fR +Enable debug to syslog. +.TP +\fB\-\-uidebug +Enable web UI debug. +.TP +\fB\-l\fR, \fB\-\-log\fR +Log to file. +.TP +\fB\-A\fR, \fB\-\-abort\fR +Immediately abort on startup (for debug). +.TP +\fB\-\-noacl +Do not perform any access control checking. +.TP +\fB\-R\fR, \fB\-\-dvbraw\fR +Use rawts file to create virtual adapter. +.TP +\fB\-r\fR, \fB\-\-rawts\fR +Use rawts file to generate virtual services. +.TP +\fB\-j\fR, \fB\-\-join\fR +Subscribe to a service permanently. .SH "LOGGING" All activity inside tvheadend is logged to syslog using log facility \fBLOG_DAEMON\fR. @@ -71,8 +132,8 @@ If daemonizing, tvheadend will writes it pid in /var/run/tvheadend.pid .B Tvheadend and this man page is maintained by Andreas Oeman .PP -(andreas a lonelycoder d com) +(andreas a tvheadend d org) .PP You may also visit #hts at irc.freenode.net .SH "SEE ALSO" -.BR http://www.lonelycoder.com/hts/ +.BR https://tvheadend.org diff --git a/src/access.c b/src/access.c index 7814025e..25ee2948 100644 --- a/src/access.c +++ b/src/access.c @@ -43,6 +43,8 @@ struct access_ticket_queue access_tickets; const char *superuser_username; const char *superuser_password; +static int access_noacl; + /** * */ @@ -149,6 +151,76 @@ access_ticket_verify(const char *id, const char *resource) return 0; } +/** + * + */ +static int +netmask_verify(access_entry_t *ae, struct sockaddr *src) +{ + access_ipmask_t *ai; + int isv4v6 = 0; + uint32_t v4v6 = 0; + + if(src->sa_family == AF_INET6) + { + struct in6_addr *in6 = &(((struct sockaddr_in6 *)src)->sin6_addr); + uint32_t *a32 = (uint32_t*)in6->s6_addr; + if(a32[0] == 0 && a32[1] == 0 && ntohl(a32[2]) == 0x0000FFFFu) + { + isv4v6 = 1; + v4v6 = ntohl(a32[3]); + } + } + + TAILQ_FOREACH(ai, &ae->ae_ipmasks, ai_link) + { + if(ai->ai_ipv6 == 0 && src->sa_family == AF_INET) + { + struct sockaddr_in *in4 = (struct sockaddr_in *)src; + uint32_t b = ntohl(in4->sin_addr.s_addr); + if((b & ai->ai_netmask) == ai->ai_network) + return 1; + } + else if(ai->ai_ipv6 == 0 && isv4v6) + { + if((v4v6 & ai->ai_netmask) == ai->ai_network) + return 1; + } + else if(ai->ai_ipv6 && isv4v6) + { + continue; + } + else if(ai->ai_ipv6 && src->sa_family == AF_INET6) + { + struct in6_addr *in6 = &(((struct sockaddr_in6 *)src)->sin6_addr); + uint8_t *a8 = (uint8_t*)in6->s6_addr; + uint8_t *m8 = (uint8_t*)ai->ai_ip6.s6_addr; + int slen = ai->ai_prefixlen; + uint32_t apos = 0; + uint8_t lastMask = (0xFFu << (8 - (slen % 8))); + + if(slen < 0 || slen > 128) + continue; + + while(slen >= 8) + { + if(a8[apos] != m8[apos]) + break; + + apos += 1; + slen -= 8; + } + if(slen >= 8) + continue; + + if(slen == 0 || (a8[apos] & lastMask) == (m8[apos] & lastMask)) + return 1; + } + } + + return 0; +} + /** * */ @@ -157,10 +229,11 @@ access_verify(const char *username, const char *password, struct sockaddr *src, uint32_t mask) { uint32_t bits = 0; - struct sockaddr_in *si = (struct sockaddr_in *)src; - uint32_t b = ntohl(si->sin_addr.s_addr); access_entry_t *ae; + if (access_noacl) + return 0; + if(username != NULL && superuser_username != NULL && password != NULL && superuser_password != NULL && !strcmp(username, superuser_username) && @@ -182,7 +255,7 @@ access_verify(const char *username, const char *password, continue; /* username/password mismatch */ } - if((b & ae->ae_netmask) != ae->ae_network) + if(!netmask_verify(ae, src)) continue; /* IP based access mismatches */ bits |= ae->ae_rights; @@ -200,14 +273,15 @@ access_get_hashed(const char *username, const uint8_t digest[20], const uint8_t *challenge, struct sockaddr *src, int *entrymatch) { - struct sockaddr_in *si = (struct sockaddr_in *)src; - uint32_t b = ntohl(si->sin_addr.s_addr); access_entry_t *ae; SHA_CTX shactx; uint8_t d[20]; uint32_t r = 0; int match = 0; + if(access_noacl) + return 0xffffffff; + if(superuser_username != NULL && superuser_password != NULL) { SHA1_Init(&shactx); @@ -226,7 +300,7 @@ access_get_hashed(const char *username, const uint8_t digest[20], if(!ae->ae_enabled) continue; - if((b & ae->ae_netmask) != ae->ae_network) + if(!netmask_verify(ae, src)) continue; /* IP based access mismatches */ SHA1_Init(&shactx); @@ -253,17 +327,18 @@ access_get_hashed(const char *username, const uint8_t digest[20], uint32_t access_get_by_addr(struct sockaddr *src) { - struct sockaddr_in *si = (struct sockaddr_in *)src; - uint32_t b = ntohl(si->sin_addr.s_addr); access_entry_t *ae; uint32_t r = 0; TAILQ_FOREACH(ae, &access_entries, ae_link) { + if(!ae->ae_enabled) + continue; + if(ae->ae_username[0] != '*') continue; - if((b & ae->ae_netmask) != ae->ae_network) + if(!netmask_verify(ae, src)) continue; /* IP based access mismatches */ r |= ae->ae_rights; @@ -293,28 +368,90 @@ static void access_set_prefix(access_entry_t *ae, const char *prefix) { char buf[100]; + char tokbuf[4096]; int prefixlen; - char *p; + char *p, *tok, *saveptr; + access_ipmask_t *ai; - if(strlen(prefix) > 90) - return; - - strcpy(buf, prefix); - p = strchr(buf, '/'); - if(p) { - *p++ = 0; - prefixlen = atoi(p); - if(prefixlen > 32) - return; - } else { - prefixlen = 32; + while((ai = TAILQ_FIRST(&ae->ae_ipmasks)) != NULL) + { + TAILQ_REMOVE(&ae->ae_ipmasks, ai, ai_link); + free(ai); } - ae->ae_ip.s_addr = inet_addr(buf); - ae->ae_prefixlen = prefixlen; + strncpy(tokbuf, prefix, 4095); + tokbuf[4095] = 0; + tok = strtok_r(tokbuf, ",;| ", &saveptr); - ae->ae_netmask = prefixlen ? 0xffffffff << (32 - prefixlen) : 0; - ae->ae_network = ntohl(ae->ae_ip.s_addr) & ae->ae_netmask; + while(tok != NULL) + { + ai = calloc(1, sizeof(access_ipmask_t)); + + if(strlen(tok) > 90 || strlen(tok) == 0) + { + free(ai); + tok = strtok_r(NULL, ",;| ", &saveptr); + continue; + } + + strcpy(buf, tok); + + if(strchr(buf, ':') != NULL) + ai->ai_ipv6 = 1; + else + ai->ai_ipv6 = 0; + + if(ai->ai_ipv6) + { + p = strchr(buf, '/'); + if(p) + { + *p++ = 0; + prefixlen = atoi(p); + if(prefixlen > 128) + { + free(ai); + tok = strtok_r(NULL, ",;| ", &saveptr); + continue; + } + } else { + prefixlen = 128; + } + + ai->ai_prefixlen = prefixlen; + inet_pton(AF_INET6, buf, &ai->ai_ip6); + + ai->ai_netmask = 0xffffffff; + ai->ai_network = 0x00000000; + } + else + { + p = strchr(buf, '/'); + if(p) + { + *p++ = 0; + prefixlen = atoi(p); + if(prefixlen > 32) + { + free(ai); + tok = strtok_r(NULL, ",;| ", &saveptr); + continue; + } + } else { + prefixlen = 32; + } + + ai->ai_ip.s_addr = inet_addr(buf); + ai->ai_prefixlen = prefixlen; + + ai->ai_netmask = prefixlen ? 0xffffffff << (32 - prefixlen) : 0; + ai->ai_network = ntohl(ai->ai_ip.s_addr) & ai->ai_netmask; + } + + TAILQ_INSERT_TAIL(&ae->ae_ipmasks, ai, ai_link); + + tok = strtok_r(NULL, ",;| ", &saveptr); + } } @@ -325,6 +462,7 @@ access_set_prefix(access_entry_t *ae, const char *prefix) static access_entry_t * access_entry_find(const char *id, int create) { + access_ipmask_t *ai; access_entry_t *ae; char buf[20]; static int tally; @@ -351,6 +489,13 @@ access_entry_find(const char *id, int create) ae->ae_username = strdup("*"); ae->ae_password = strdup("*"); ae->ae_comment = strdup("New entry"); + TAILQ_INIT(&ae->ae_ipmasks); + ai = calloc(1, sizeof(access_ipmask_t)); + ai->ai_ipv6 = 1; + TAILQ_INSERT_HEAD(&ae->ae_ipmasks, ai, ai_link); + ai = calloc(1, sizeof(access_ipmask_t)); + ai->ai_ipv6 = 0; + TAILQ_INSERT_HEAD(&ae->ae_ipmasks, ai, ai_link); TAILQ_INSERT_TAIL(&access_entries, ae, ae_link); return ae; } @@ -363,6 +508,14 @@ access_entry_find(const char *id, int create) static void access_entry_destroy(access_entry_t *ae) { + access_ipmask_t *ai; + + while((ai = TAILQ_FIRST(&ae->ae_ipmasks)) != NULL) + { + TAILQ_REMOVE(&ae->ae_ipmasks, ai, ai_link); + free(ai); + } + free(ae->ae_id); free(ae->ae_username); free(ae->ae_password); @@ -378,7 +531,12 @@ static htsmsg_t * access_record_build(access_entry_t *ae) { htsmsg_t *e = htsmsg_create_map(); - char buf[100]; + access_ipmask_t *ai; + char addrbuf[50]; + char buf[4096]; + int pos = 0; + + memset(buf, 0, 4096); htsmsg_add_u32(e, "enabled", !!ae->ae_enabled); @@ -386,8 +544,22 @@ access_record_build(access_entry_t *ae) htsmsg_add_str(e, "password", ae->ae_password); htsmsg_add_str(e, "comment", ae->ae_comment); - snprintf(buf, sizeof(buf), "%s/%d", inet_ntoa(ae->ae_ip), ae->ae_prefixlen); - htsmsg_add_str(e, "prefix", buf); + TAILQ_FOREACH(ai, &ae->ae_ipmasks, ai_link) + { + if(sizeof(buf)-pos <= 0) + break; + + if(ai->ai_ipv6) + { + inet_ntop(AF_INET6, &ai->ai_ip6, addrbuf, 50); + pos += snprintf(buf+pos, sizeof(buf)-pos, ",%s/%d", addrbuf, ai->ai_prefixlen); + } + else + { + pos += snprintf(buf+pos, sizeof(buf)-pos, ",%s/%d", inet_ntoa(ai->ai_ip), ai->ai_prefixlen); + } + } + htsmsg_add_str(e, "prefix", buf + 1); htsmsg_add_u32(e, "streaming", ae->ae_rights & ACCESS_STREAMING ? 1 : 0); htsmsg_add_u32(e, "dvr" , ae->ae_rights & ACCESS_RECORDER ? 1 : 0); @@ -532,11 +704,12 @@ static const dtable_class_t access_dtc = { * */ void -access_init(int createdefault) +access_init(int createdefault, int noacl) { dtable_t *dt; htsmsg_t *r, *m; access_entry_t *ae; + access_ipmask_t *ai; const char *s; static struct { @@ -544,6 +717,10 @@ access_init(int createdefault) struct timeval tv; } randseed; + access_noacl = noacl; + if (noacl) + tvhlog(LOG_WARNING, "access", "Access control checking disabled"); + randseed.pid = getpid(); gettimeofday(&randseed.tv, NULL); RAND_seed(&randseed, sizeof(randseed)); @@ -563,11 +740,21 @@ access_init(int createdefault) ae->ae_enabled = 1; ae->ae_rights = 0xffffffff; + TAILQ_INIT(&ae->ae_ipmasks); + + ai = calloc(1, sizeof(access_ipmask_t)); + ai->ai_ipv6 = 1; + TAILQ_INSERT_HEAD(&ae->ae_ipmasks, ai, ai_link); + + ai = calloc(1, sizeof(access_ipmask_t)); + ai->ai_ipv6 = 0; + TAILQ_INSERT_HEAD(&ae->ae_ipmasks, ai, ai_link); + r = access_record_build(ae); dtable_record_store(dt, ae->ae_id, r); htsmsg_destroy(r); - tvhlog(LOG_WARNING, "accesscontrol", + tvhlog(LOG_WARNING, "access", "Created default wide open access controle entry"); } diff --git a/src/access.h b/src/access.h index 35bb1d83..2edb0ab9 100644 --- a/src/access.h +++ b/src/access.h @@ -20,6 +20,20 @@ #define ACCESS_H_ +typedef struct access_ipmask { + TAILQ_ENTRY(access_ipmask) ai_link; + + int ai_ipv6; + + struct in_addr ai_ip; + struct in6_addr ai_ip6; + + int ai_prefixlen; + + uint32_t ai_network; + uint32_t ai_netmask; +} access_ipmask_t; + TAILQ_HEAD(access_entry_queue, access_entry); extern struct access_entry_queue access_entries; @@ -31,15 +45,12 @@ typedef struct access_entry { char *ae_username; char *ae_password; char *ae_comment; - struct in_addr ae_ip; - int ae_prefixlen; int ae_enabled; uint32_t ae_rights; - uint32_t ae_network; /* derived from ae_ip */ - uint32_t ae_netmask; /* derived from ae_prefixlen */ + TAILQ_HEAD(, access_ipmask) ae_ipmasks; } access_entry_t; TAILQ_HEAD(access_ticket_queue, access_ticket); @@ -99,6 +110,6 @@ uint32_t access_get_by_addr(struct sockaddr *src); /** * */ -void access_init(int createdefault); +void access_init(int createdefault, int noacl); #endif /* ACCESS_H_ */ diff --git a/src/atomic.h b/src/atomic.h index a9e1485e..e9ef2648 100644 --- a/src/atomic.h +++ b/src/atomic.h @@ -16,16 +16,7 @@ * along with this program. If not, see . */ -#ifndef HTSATOMIC_H__ -#define HTSATOMIC_H__ - -/** - * Atomically add 'incr' to *ptr and return the previous value - */ - - - -#if defined(linux) && __GNUC__ >= 4 && __GNUC_MINOR__ >=3 +#pragma once static inline int atomic_add(volatile int *ptr, int incr) @@ -33,80 +24,38 @@ atomic_add(volatile int *ptr, int incr) return __sync_fetch_and_add(ptr, incr); } -#elif defined(__i386__) || defined(__x86_64__) static inline int -atomic_add(volatile int *ptr, int incr) +atomic_exchange(volatile int *ptr, int new) { - int r; - asm volatile("lock; xaddl %0, %1" : - "=r"(r), "=m"(*ptr) : "0" (incr), "m" (*ptr) : "memory"); - return r; -} -#elif defined(WII) - -#include - -static inline int -atomic_add(volatile int *ptr, int incr) -{ - int r, level; - - /* - * Last time i checked libogc's context switcher did not do the - * necessary operations to clear locks held by lwarx/stwcx. - * Thus we need to resort to other means - */ - - _CPU_ISR_Disable(level); - - r = *ptr; - *ptr = *ptr + incr; - - _CPU_ISR_Restore(level); - - return r; -} -#elif defined(__ppc__) || defined(__PPC__) - -/* somewhat based on code from darwin gcc */ -static inline int -atomic_add (volatile int *ptr, int incr) -{ - int tmp, res; - asm volatile("0:\n" - "lwarx %1,0,%2\n" - "add%I3 %0,%1,%3\n" - "stwcx. %0,0,%2\n" - "bne- 0b\n" - : "=&r"(tmp), "=&b"(res) - : "r" (ptr), "Ir"(incr) - : "cr0", "memory"); - - return res; + return __sync_lock_test_and_set(ptr, new); } -#elif defined(__arm__) - -static inline int -atomic_add(volatile int *ptr, int val) +static inline uint64_t +atomic_add_u64(volatile uint64_t *ptr, uint64_t incr) { - int a, b, c; - - asm volatile( "0:\n\t" - "ldr %0, [%3]\n\t" - "add %1, %0, %4\n\t" - "swp %2, %1, [%3]\n\t" - "cmp %0, %2\n\t" - "swpne %1, %2, [%3]\n\t" - "bne 0b" - : "=&r" (a), "=&r" (b), "=&r" (c) - : "r" (ptr), "r" (val) - : "cc", "memory"); - return a; -} - +#if ENABLE_ATOMIC64 + return __sync_fetch_and_add(ptr, incr); #else -#error Missing atomic ops + uint64_t ret; + pthread_mutex_lock(&atomic_lock); + ret = *ptr; + *ptr += incr; + pthread_mutex_unlock(&atomic_lock); + return ret; #endif +} -#endif /* HTSATOMIC_H__ */ +static inline uint64_t +atomic_pre_add_u64(volatile uint64_t *ptr, uint64_t incr) +{ +#if ENABLE_ATOMIC64 + return __sync_add_and_fetch(ptr, incr); +#else + uint64_t ret; + pthread_mutex_lock(&atomic_lock); + *ptr += incr; + ret = *ptr; + pthread_mutex_unlock(&atomic_lock); + return ret; +#endif +} diff --git a/src/avahi.c b/src/avahi.c index af66db26..76ec45c8 100644 --- a/src/avahi.c +++ b/src/avahi.c @@ -133,7 +133,7 @@ create_services(AvahiClient *c) /* Add the service for HTSP */ if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, name, - "_htsp._tcp", NULL, NULL,htsp_port, + "_htsp._tcp", NULL, NULL,tvheadend_htsp_port, NULL)) < 0) { if (ret == AVAHI_ERR_COLLISION) @@ -149,7 +149,7 @@ create_services(AvahiClient *c) /* Add the service for HTTP */ if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, name, - "_http._tcp", NULL, NULL, webui_port, + "_http._tcp", NULL, NULL, tvheadend_webui_port, "path=/", NULL)) < 0) { diff --git a/src/avc.c b/src/avc.c index afad9e4a..0a473f15 100644 --- a/src/avc.c +++ b/src/avc.c @@ -186,8 +186,6 @@ avc_convert_pkt(th_pkt_t *src) pkt->pkt_header = NULL; pkt->pkt_payload = NULL; - pkt->pkt_payload = malloc(sizeof(pktbuf_t)); - pkt->pkt_payload->pb_refcount=1; if (src->pkt_header) { sbuf_t headers; sbuf_init(&headers); diff --git a/src/capmt.c b/src/capmt.c index 18cdf2da..341abce5 100644 --- a/src/capmt.c +++ b/src/capmt.c @@ -33,18 +33,26 @@ #include #include #include +#include +#include +#include #include "tvheadend.h" #include "dvb/dvb.h" #include "tcp.h" #include "psi.h" #include "tsdemux.h" -#include "ffdecsa/FFdecsa.h" #include "capmt.h" #include "notify.h" #include "subscriptions.h" #include "dtable.h" +#if ENABLE_DVBCSA +#include +#else +#include "ffdecsa/FFdecsa.h" +#endif + // ca_pmt_list_management values: #define CAPMT_LIST_MORE 0x00 // append a 'MORE' CAPMT object the list and start receiving the next object #define CAPMT_LIST_FIRST 0x01 // clear the list when a 'FIRST' CAPMT object is received, and start receiving the next object @@ -67,6 +75,22 @@ #define CW_DUMP(buf, len, format, ...) \ printf(format, __VA_ARGS__); int j; for (j = 0; j < len; ++j) printf("%02X ", buf[j]); printf("\n"); +#ifdef __GNUC__ +#include +#if __GNUC_PREREQ(4, 3) +#pragma GCC diagnostic ignored "-Warray-bounds" +#endif +#endif + +#define MAX_CA 4 +#define MAX_INDEX 64 +#define KEY_SIZE 8 +#define INFO_SIZE (2+KEY_SIZE+KEY_SIZE) +#define EVEN_OFF (2) +#define ODD_OFF (2+KEY_SIZE) +#define MAX_SOCKETS 16 // max sockets (simultaneous channels) per demux +static unsigned char ca_info[MAX_CA][MAX_INDEX][INFO_SIZE]; + /** * */ @@ -82,7 +106,7 @@ static pthread_cond_t capmt_config_changed; typedef struct capmt_descriptor { uint8_t cad_type; uint8_t cad_length; - uint8_t cad_data[16]; + uint8_t cad_data[17]; } __attribute__((packed)) capmt_descriptor_t; /** @@ -108,6 +132,8 @@ typedef struct capmt_caid_ecm { uint16_t cce_caid; /** ecm pid */ uint16_t cce_ecmpid; + /** provider id */ + uint32_t cce_providerid; /** last ecm size */ uint32_t cce_ecmsize; /** last ecm buffer */ @@ -140,13 +166,24 @@ typedef struct capmt_service { CT_FORBIDDEN } ct_keystate; - /* buffer for keystruct */ - void *ct_keys; + /* buffers for keystructs */ +#if ENABLE_DVBCSA + struct dvbcsa_bs_key_s *ct_key_even; + struct dvbcsa_bs_key_s *ct_key_odd; +#else + void *ct_keys; +#endif /* CSA */ int ct_cluster_size; uint8_t *ct_tsbcluster; int ct_fill; +#if ENABLE_DVBCSA + struct dvbcsa_bs_batch_s *ct_tsbbatch_even; + struct dvbcsa_bs_batch_s *ct_tsbbatch_odd; + int ct_fill_even; + int ct_fill_odd; +#endif /* current sequence number */ uint16_t ct_seq; @@ -172,10 +209,12 @@ typedef struct capmt { int capmt_port; char *capmt_comment; char *capmt_id; + int capmt_oscam; /* capmt sockets */ - int capmt_sock; - int capmt_sock_ca0; + int sids[MAX_SOCKETS]; + int capmt_sock[MAX_SOCKETS]; + int capmt_sock_ca0[MAX_CA]; /* thread flags */ int capmt_connected; @@ -191,43 +230,135 @@ typedef struct capmt { * */ static int -capmt_send_msg(capmt_t *capmt, const uint8_t *buf, size_t len) +capmt_send_msg(capmt_t *capmt, int sid, const uint8_t *buf, size_t len) { - return write(capmt->capmt_sock, buf, len); + if (capmt->capmt_oscam) { + int i; + + // dumping current SID table + for (i = 0; i < MAX_SOCKETS; i++) + tvhlog(LOG_DEBUG, "capmt", "%s: SOCKETS TABLE DUMP [%d]: sid=%d socket=%d", __FUNCTION__, i, capmt->sids[i], capmt->capmt_sock[i]); + if (sid == 0) { + tvhlog(LOG_DEBUG, "capmt", "%s: got empty SID - returning from function", __FUNCTION__); + return -1; + } + + // searching for the SID and socket + int found = 0; + for (i = 0; i < MAX_SOCKETS; i++) { + if (capmt->sids[i] == sid) { + found = 1; + break; + } + } + + if (found) + tvhlog(LOG_DEBUG, "capmt", "%s: found sid, reusing socket, i=%d", __FUNCTION__, i); + else { //not found - adding to first free in table + for (i = 0; i < MAX_SOCKETS; i++) { + if (capmt->sids[i] == 0) { + capmt->sids[i] = sid; + break; + } + } + } + if (i == MAX_SOCKETS) { + tvhlog(LOG_DEBUG, "capmt", "%s: no free space for new SID!!!", __FUNCTION__); + return -1; + } else { + capmt->sids[i] = sid; + tvhlog(LOG_DEBUG, "capmt", "%s: added: i=%d", __FUNCTION__, i); + } + + // check if the socket is still alive by writing 0 bytes + if (capmt->capmt_sock[i] > 0) { + if (write(capmt->capmt_sock[i], NULL, 0) < 0) + capmt->capmt_sock[i] = 0; + else if (found) + return 0; + } + + // opening socket and sending + if (capmt->capmt_sock[i] == 0) { + capmt->capmt_sock[i] = tvh_socket(AF_LOCAL, SOCK_STREAM, 0); + + struct sockaddr_un serv_addr_un; + memset(&serv_addr_un, 0, sizeof(serv_addr_un)); + serv_addr_un.sun_family = AF_LOCAL; + snprintf(serv_addr_un.sun_path, sizeof(serv_addr_un.sun_path), "%s", capmt->capmt_sockfile); + + if (connect(capmt->capmt_sock[i], (const struct sockaddr*)&serv_addr_un, sizeof(serv_addr_un)) != 0) { + tvhlog(LOG_ERR, "capmt", "Cannot connect to %s, Do you have OSCam running?", capmt->capmt_sockfile); + capmt->capmt_sock[i] = 0; + } else + tvhlog(LOG_DEBUG, "capmt", "created socket with socket_fd=%d", capmt->capmt_sock[i]); + } + if (capmt->capmt_sock[i] > 0) { + if (tvh_write(capmt->capmt_sock[i], buf, len)) { + tvhlog(LOG_DEBUG, "capmt", "socket_fd=%d send failed", capmt->capmt_sock[i]); + close(capmt->capmt_sock[i]); + capmt->capmt_sock[i] = 0; + return -1; + } + } + } + else // standard old capmt mode + tvh_write(capmt->capmt_sock[0], buf, len); + return 0; } static void capmt_send_stop(capmt_service_t *t) { - /* buffer for capmt */ - int pos = 0; - uint8_t buf[4094]; + if (t->ct_capmt->capmt_oscam) { + int i; + // searching for socket to close + for (i = 0; i < MAX_SOCKETS; i++) + if (t->ct_capmt->sids[i] == t->ct_service->s_dvb_service_id) + break; - capmt_header_t head = { - .capmt_indicator = { 0x9F, 0x80, 0x32, 0x82, 0x00, 0x00 }, - .capmt_list_management = CAPMT_LIST_ONLY, - .program_number = t->ct_service->s_dvb_service_id, - .version_number = 0, - .current_next_indicator = 0, - .program_info_length = 0, - .capmt_cmd_id = CAPMT_CMD_NOT_SELECTED, - }; - memcpy(&buf[pos], &head, sizeof(head)); - pos += sizeof(head); + if (i == MAX_SOCKETS) { + tvhlog(LOG_DEBUG, "capmt", "%s: socket to close not found", __FUNCTION__); + return; + } - uint8_t end[] = { - 0x01, (t->ct_seq >> 8) & 0xFF, t->ct_seq & 0xFF, 0x00, 0x06 }; - memcpy(&buf[pos], end, sizeof(end)); - pos += sizeof(end); - buf[4] = ((pos - 6) >> 8); - buf[5] = ((pos - 6) & 0xFF); - buf[7] = t->ct_service->s_dvb_service_id >> 8; - buf[8] = t->ct_service->s_dvb_service_id & 0xFF; - buf[9] = 1; - buf[10] = ((pos - 5 - 12) & 0xF00) >> 8; - buf[11] = ((pos - 5 - 12) & 0xFF); + // closing socket (oscam handle this as event and stop decrypting) + tvhlog(LOG_DEBUG, "capmt", "%s: closing socket i=%d, socket_fd=%d", __FUNCTION__, i, t->ct_capmt->capmt_sock[i]); + t->ct_capmt->sids[i] = 0; + if (t->ct_capmt->capmt_sock[i] > 0) + close(t->ct_capmt->capmt_sock[i]); + t->ct_capmt->capmt_sock[i] = 0; + } else { // standard old capmt mode + /* buffer for capmt */ + int pos = 0; + uint8_t buf[4094]; + + capmt_header_t head = { + .capmt_indicator = { 0x9F, 0x80, 0x32, 0x82, 0x00, 0x00 }, + .capmt_list_management = CAPMT_LIST_ONLY, + .program_number = t->ct_service->s_dvb_service_id, + .version_number = 0, + .current_next_indicator = 0, + .program_info_length = 0, + .capmt_cmd_id = CAPMT_CMD_NOT_SELECTED, + }; + memcpy(&buf[pos], &head, sizeof(head)); + pos += sizeof(head); + + uint8_t end[] = { + 0x01, (t->ct_seq >> 8) & 0xFF, t->ct_seq & 0xFF, 0x00, 0x06 }; + memcpy(&buf[pos], end, sizeof(end)); + pos += sizeof(end); + buf[4] = ((pos - 6) >> 8); + buf[5] = ((pos - 6) & 0xFF); + buf[7] = t->ct_service->s_dvb_service_id >> 8; + buf[8] = t->ct_service->s_dvb_service_id & 0xFF; + buf[9] = 1; + buf[10] = ((pos - 5 - 12) & 0xF00) >> 8; + buf[11] = ((pos - 5 - 12) & 0xFF); - capmt_send_msg(t->ct_capmt, buf, pos); + capmt_send_msg(t->ct_capmt, t->ct_service->s_dvb_service_id, buf, pos); + } } /** @@ -257,7 +388,14 @@ capmt_service_destroy(th_descrambler_t *td) LIST_REMOVE(ct, ct_link); +#if ENABLE_DVBCSA + dvbcsa_bs_key_free(ct->ct_key_odd); + dvbcsa_bs_key_free(ct->ct_key_even); + free(ct->ct_tsbbatch_odd); + free(ct->ct_tsbbatch_even); +#else free_key_struct(ct->ct_keys); +#endif free(ct->ct_tsbcluster); free(ct); } @@ -266,35 +404,123 @@ static void handle_ca0(capmt_t* capmt) { capmt_service_t *ct; service_t *t; - int ret; + int ret, bufsize; + int *request; + ca_descr_t *ca; + ca_pid_t *cpd; + int process_key, process_next, cai; + int i, j; - uint8_t invalid[8], buffer[20], *even, *odd; + if (capmt->capmt_oscam) + bufsize = sizeof(int) + sizeof(ca_descr_t); + else + bufsize = 18; + + uint8_t invalid[8], buffer[bufsize], *even, *odd; uint16_t seq; memset(invalid, 0, 8); tvhlog(LOG_INFO, "capmt", "got connection from client ..."); + i = 0; while (capmt->capmt_running) { + process_key = 0; - ret = recv(capmt->capmt_sock_ca0, buffer, 18, MSG_WAITALL); + // receiving data from UDP socket + if (!capmt->capmt_oscam) { + ret = recv(capmt->capmt_sock_ca0[0], buffer, bufsize, MSG_WAITALL); - if (ret < 0) - tvhlog(LOG_ERR, "capmt", "error receiving over socket"); - else if (ret == 0) { - // normal socket shutdown - tvhlog(LOG_INFO, "capmt", "normal socket shutdown"); - break; - } - - /* get control words */ - seq = buffer[0] | ((uint16_t)buffer[1] << 8); - even = &buffer[2]; - odd = &buffer[10]; + if (ret < 0) + tvhlog(LOG_ERR, "capmt", "error receiving over socket"); + else if (ret == 0) { + // normal socket shutdown + tvhlog(LOG_INFO, "capmt", "normal socket shutdown"); + break; + } + } else { + process_next = 0; + if (capmt->capmt_sock_ca0[i] > 0) { + ret = recv(capmt->capmt_sock_ca0[i], buffer, bufsize, MSG_DONTWAIT); + if (ret < 0) + process_next = 1; + else if (ret == 0) { + // normal socket shutdown + tvhlog(LOG_INFO, "capmt", "normal socket shutdown"); + close(capmt->capmt_sock_ca0[i]); + capmt->capmt_sock_ca0[i] = -1; + int still_left = 0; + for (j = 0; j < MAX_CA; j++) { + if (capmt->capmt_sock_ca0[j] > 0) { + still_left = 1; + break; + } + } + if (still_left) //this socket is closed but there are others active + process_next = 1; + else //all sockets closed + break; + } + } else + process_next = 1; + + if (process_next) { + i++; + if (i >= MAX_CA) + i = 0; + usleep(10 * 1000); + continue; + } + } + + // parsing data + if (capmt->capmt_oscam) { + cai = i; + request = (int *) &buffer; + if (*request == CA_SET_PID) { + cpd = (ca_pid_t *)&buffer[sizeof(int)]; + tvhlog(LOG_DEBUG, "capmt", "CA_SET_PID cai %d req %d (%d %04x)", cai, *request, cpd->index, cpd->pid); + + if (cpd->index >=0 && cpd->index < MAX_INDEX) { + ca_info[cai][cpd->index][0] = (cpd->pid >> 0) & 0xff; + ca_info[cai][cpd->index][1] = (cpd->pid >> 8) & 0xff; + } else if (cpd->index == -1) { + memset(&ca_info[cai], 0, sizeof(ca_info[cai])); + } else + tvhlog(LOG_ERR, "capmt", "Invalid index %d in CA_SET_PID (%d) for ca id %d", cpd->index, MAX_INDEX, cai); + } else if (*request == CA_SET_DESCR) { + ca = (ca_descr_t *)&buffer[sizeof(int)]; + tvhlog(LOG_DEBUG, "capmt", "CA_SET_DESCR cai %d req %d par %d idx %d %02x%02x%02x%02x%02x%02x%02x%02x", cai, *request, ca->parity, ca->index, ca->cw[0], ca->cw[1], ca->cw[2], ca->cw[3], ca->cw[4], ca->cw[5], ca->cw[6], ca->cw[7]); + if (ca->index == -1) // skipping removal request + continue; + + if(ca->parity==0) { + memcpy(&ca_info[cai][ca->index][EVEN_OFF],ca->cw,KEY_SIZE); // even key + process_key = 1; + } else if(ca->parity==1) { + memcpy(&ca_info[cai][ca->index][ODD_OFF],ca->cw,KEY_SIZE); // odd key + process_key = 1; + } else + tvhlog(LOG_ERR, "capmt", "Invalid parity %d in CA_SET_DESCR for ca id %d", ca->parity, cai); + + seq = ca_info[cai][ca->index][0] | ((uint16_t)ca_info[cai][ca->index][1] << 8); + even = &ca_info[cai][ca->index][EVEN_OFF]; + odd = &ca_info[cai][ca->index][ODD_OFF]; + } + } else { + /* get control words */ + seq = buffer[0] | ((uint16_t)buffer[1] << 8); + even = &buffer[2]; + odd = &buffer[10]; + process_key = 1; + } + + // processing key + if (process_key) { LIST_FOREACH(ct, &capmt->capmt_services, ct_link) { t = ct->ct_service; - if(ret < 18) { + if(ret < bufsize) { if(ct->ct_keystate != CT_FORBIDDEN) { tvhlog(LOG_ERR, "capmt", "Can not descramble service \"%s\", access denied", t->s_svcname); @@ -308,20 +534,48 @@ handle_ca0(capmt_t* capmt) { continue; if (memcmp(even, invalid, 8)) +#if ENABLE_DVBCSA + dvbcsa_bs_key_set(even, ct->ct_key_even); +#else set_even_control_word(ct->ct_keys, even); +#endif if (memcmp(odd, invalid, 8)) +#if ENABLE_DVBCSA + dvbcsa_bs_key_set(odd, ct->ct_key_odd); +#else set_odd_control_word(ct->ct_keys, odd); +#endif if(ct->ct_keystate != CT_RESOLVED) - tvhlog(LOG_INFO, "capmt", "Obtained key for service \"%s\"",t->s_svcname); + tvhlog(LOG_DEBUG, "capmt", "Obtained key for service \"%s\"",t->s_svcname); ct->ct_keystate = CT_RESOLVED; } + } } tvhlog(LOG_INFO, "capmt", "connection from client closed ..."); } +static int +capmt_create_udp_socket(int *socket, int port) +{ + *socket = tvh_socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); + + struct sockaddr_in serv_addr; + serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + serv_addr.sin_port = htons( (unsigned short int)port); + serv_addr.sin_family = AF_INET; + + if (bind(*socket, (const struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0) + { + perror("[CapmtServer] ERROR binding to ca0"); + return 0; + } + else + return 1; +} + /** * */ @@ -330,11 +584,15 @@ capmt_thread(void *aux) { capmt_t *capmt = aux; struct timespec ts; - int d; + int d, i, bind_ok = 0; while (capmt->capmt_running) { - capmt->capmt_sock = -1; - capmt->capmt_sock_ca0 = -1; + for (i = 0; i < MAX_CA; i++) + capmt->capmt_sock_ca0[i] = -1; + for (i = 0; i < MAX_SOCKETS; i++) { + capmt->sids[i] = 0; + capmt->capmt_sock[i] = 0; + } capmt->capmt_connected = 0; pthread_mutex_lock(&global_lock); @@ -345,27 +603,37 @@ capmt_thread(void *aux) pthread_mutex_unlock(&global_lock); /* open connection to camd.socket */ - capmt->capmt_sock = tvh_socket(AF_LOCAL, SOCK_STREAM, 0); + capmt->capmt_sock[0] = tvh_socket(AF_LOCAL, SOCK_STREAM, 0); struct sockaddr_un serv_addr_un; memset(&serv_addr_un, 0, sizeof(serv_addr_un)); serv_addr_un.sun_family = AF_LOCAL; snprintf(serv_addr_un.sun_path, sizeof(serv_addr_un.sun_path), "%s", capmt->capmt_sockfile); - if (connect(capmt->capmt_sock, (const struct sockaddr*)&serv_addr_un, sizeof(serv_addr_un)) == 0) { + if (connect(capmt->capmt_sock[0], (const struct sockaddr*)&serv_addr_un, sizeof(serv_addr_un)) == 0) { capmt->capmt_connected = 1; /* open connection to emulated ca0 device */ - capmt->capmt_sock_ca0 = tvh_socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); - - struct sockaddr_in serv_addr; - serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - serv_addr.sin_port = htons( (unsigned short int)capmt->capmt_port); - serv_addr.sin_family = AF_INET; - - if (bind(capmt->capmt_sock_ca0, (const struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0) - perror("[CapmtServer] ERROR binding to ca0"); - else + if (!capmt->capmt_oscam) { + bind_ok = capmt_create_udp_socket(&capmt->capmt_sock_ca0[0], capmt->capmt_port); + } else { +#if ENABLE_LINUXDVB + th_dvb_adapter_t *tda; + TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) { + if (!tda->tda_enabled) + continue; + if (tda->tda_rootpath) { //if rootpath is NULL then can't rely on tda_adapter_num because it is always 0 + if (tda->tda_adapter_num > MAX_CA) { + tvhlog(LOG_ERR, "capmt", "adapter number > MAX_CA"); + continue; + } + tvhlog(LOG_INFO, "capmt", "Creating capmt UDP socket for adapter %d", tda->tda_adapter_num); + bind_ok = capmt_create_udp_socket(&capmt->capmt_sock_ca0[tda->tda_adapter_num], 9000 + tda->tda_adapter_num); + } + } +#endif + } + if (bind_ok) handle_ca0(capmt); } else tvhlog(LOG_ERR, "capmt", "Error connecting to %s: %s", capmt->capmt_sockfile, strerror(errno)); @@ -373,10 +641,12 @@ capmt_thread(void *aux) capmt->capmt_connected = 0; /* close opened sockets */ - if (capmt->capmt_sock > 0) - close(capmt->capmt_sock); - if (capmt->capmt_sock_ca0 > 0) - close(capmt->capmt_sock_ca0); + for (i = 0; i < MAX_SOCKETS; i++) + if (capmt->capmt_sock[i] > 0) + close(capmt->capmt_sock[i]); + for (i = 0; i < MAX_CA; i++) + if (capmt->capmt_sock_ca0[i] > 0) + close(capmt->capmt_sock_ca0[i]); /* schedule reconnection */ if(subscriptions_active()) { @@ -408,144 +678,194 @@ capmt_table_input(struct th_descrambler *td, struct service *t, capmt_service_t *ct = (capmt_service_t *)td; capmt_t *capmt = ct->ct_capmt; int adapter_num = t->s_dvb_mux_instance->tdmi_adapter->tda_adapter_num; + int total_caids = 0, current_caid = 0; caid_t *c; - c = LIST_FIRST(&st->es_caids); - if(c == NULL) - return; + LIST_FOREACH(c, &st->es_caids, link) { + total_caids++; + } - if(len > 4096) - return; + LIST_FOREACH(c, &st->es_caids, link) { + current_caid++; - switch(data[0]) { - case 0x80: - case 0x81: - { - /* ECM */ - if (ct->ct_caid_last == -1) - ct->ct_caid_last = c->caid; - - uint16_t caid = c->caid; - /* search ecmpid in list */ - capmt_caid_ecm_t *cce, *cce2; - LIST_FOREACH(cce, &ct->ct_caid_ecm, cce_link) - if (cce->cce_caid == caid) - break; + if(c == NULL) + return; - if (!cce) + if(len > 4096) + return; + + switch(data[0]) { + case 0x80: + case 0x81: { - tvhlog(LOG_DEBUG, "capmt", - "New caid 0x%04X for service \"%s\"", c->caid, t->s_svcname); + /* ECM */ + if (ct->ct_caid_last == -1) + ct->ct_caid_last = c->caid; - /* ecmpid not already seen, add it to list */ - cce = calloc(1, sizeof(capmt_caid_ecm_t)); - cce->cce_caid = c->caid; - cce->cce_ecmpid = st->es_pid; - LIST_INSERT_HEAD(&ct->ct_caid_ecm, cce, cce_link); - } + uint16_t caid = c->caid; + /* search ecmpid in list */ + capmt_caid_ecm_t *cce, *cce2; + LIST_FOREACH(cce, &ct->ct_caid_ecm, cce_link) + if (cce->cce_caid == caid) + break; - if ((cce->cce_ecmsize == len) && !memcmp(cce->cce_ecm, data, len)) - break; /* key already sent */ + if (!cce) + { + tvhlog(LOG_DEBUG, "capmt", + "New caid 0x%04X for service \"%s\"", c->caid, t->s_svcname); - if(capmt->capmt_sock == -1) { - /* New key, but we are not connected (anymore), can not descramble */ - ct->ct_keystate = CT_UNKNOWN; + /* ecmpid not already seen, add it to list */ + cce = calloc(1, sizeof(capmt_caid_ecm_t)); + cce->cce_caid = c->caid; + cce->cce_ecmpid = st->es_pid; + cce->cce_providerid = c->providerid; + LIST_INSERT_HEAD(&ct->ct_caid_ecm, cce, cce_link); + } + + if ((cce->cce_ecmsize == len) && !memcmp(cce->cce_ecm, data, len)) + break; /* key already sent */ + + if(!capmt->capmt_oscam && capmt->capmt_sock[0] == 0) { + /* New key, but we are not connected (anymore), can not descramble */ + ct->ct_keystate = CT_UNKNOWN; + break; + } + + uint16_t sid = t->s_dvb_service_id; + uint16_t ecmpid = st->es_pid; + uint16_t transponder = t->s_dvb_mux_instance->tdmi_transport_stream_id; + uint16_t onid = t->s_dvb_mux_instance->tdmi_network_id; + + + /* don't do too much requests */ + if (current_caid == total_caids && caid != ct->ct_caid_last) + return; + + static uint8_t pmtversion = 1; + + /* buffer for capmt */ + int pos = 0; + uint8_t buf[4094]; + + capmt_header_t head = { + .capmt_indicator = { 0x9F, 0x80, 0x32, 0x82, 0x00, 0x00 }, + .capmt_list_management = CAPMT_LIST_ONLY, + .program_number = sid, + .version_number = 0, + .current_next_indicator = 0, + .program_info_length = 0, + .capmt_cmd_id = CAPMT_CMD_OK_DESCRAMBLING, + }; + memcpy(&buf[pos], &head, sizeof(head)); + pos += sizeof(head); + + if (capmt->capmt_oscam) + { + capmt_descriptor_t dmd = { + .cad_type = CAPMT_DESC_DEMUX, + .cad_length = 0x02, + .cad_data = { + 0, adapter_num }}; + memcpy(&buf[pos], &dmd, dmd.cad_length + 2); + pos += dmd.cad_length + 2; + } + + capmt_descriptor_t prd = { + .cad_type = CAPMT_DESC_PRIVATE, + .cad_length = 0x08, + .cad_data = { 0x00, 0x00, 0x00, 0x00, // enigma namespace goes here + transponder >> 8, transponder & 0xFF, + onid >> 8, onid & 0xFF + }}; + memcpy(&buf[pos], &prd, prd.cad_length + 2); + pos += prd.cad_length + 2; + + if (!capmt->capmt_oscam) + { + capmt_descriptor_t dmd = { + .cad_type = CAPMT_DESC_DEMUX, + .cad_length = 0x02, + .cad_data = { + 1 << adapter_num, adapter_num }}; + memcpy(&buf[pos], &dmd, dmd.cad_length + 2); + pos += dmd.cad_length + 2; + } + + capmt_descriptor_t ecd = { + .cad_type = CAPMT_DESC_PID, + .cad_length = 0x02, + .cad_data = { + ecmpid >> 8, ecmpid & 0xFF }}; + memcpy(&buf[pos], &ecd, ecd.cad_length + 2); + pos += ecd.cad_length + 2; + + LIST_FOREACH(cce2, &ct->ct_caid_ecm, cce_link) { + capmt_descriptor_t cad = { + .cad_type = 0x09, + .cad_length = 0x04, + .cad_data = { + cce2->cce_caid >> 8, cce2->cce_caid & 0xFF, + cce2->cce_ecmpid >> 8 | 0xE0, cce2->cce_ecmpid & 0xFF}}; + if (cce2->cce_providerid) { //we need to add provider ID to the data + if (cce2->cce_caid >> 8 == 0x01) { + cad.cad_length = 0x11; + cad.cad_data[4] = cce2->cce_providerid >> 8; + cad.cad_data[5] = cce2->cce_providerid & 0xffffff; + } else if (cce2->cce_caid >> 8 == 0x05) { + cad.cad_length = 0x0f; + cad.cad_data[10] = 0x14; + cad.cad_data[11] = cce2->cce_providerid >> 24; + cad.cad_data[12] = cce2->cce_providerid >> 16; + cad.cad_data[13] = cce2->cce_providerid >> 8; + cad.cad_data[14] = cce2->cce_providerid & 0xffffff; + } else if (cce2->cce_caid >> 8 == 0x18) { + cad.cad_length = 0x07; + cad.cad_data[5] = cce2->cce_providerid >> 8; + cad.cad_data[6] = cce2->cce_providerid & 0xffffff; + } else if (cce2->cce_caid >> 8 == 0x4a) { + cad.cad_length = 0x05; + cad.cad_data[4] = cce2->cce_providerid & 0xffffff; + } else + tvhlog(LOG_WARNING, "capmt", "Unknown CAID type, don't know where to put provider ID"); + } + memcpy(&buf[pos], &cad, cad.cad_length + 2); + pos += cad.cad_length + 2; + tvhlog(LOG_DEBUG, "capmt", "adding ECMPID=0x%X (%d), CAID=0x%X (%d) PROVID=0x%X (%d)", + cce2->cce_ecmpid, cce2->cce_ecmpid, + cce2->cce_caid, cce2->cce_caid, + cce2->cce_providerid, cce2->cce_providerid); + } + + uint8_t end[] = { + 0x01, (ct->ct_seq >> 8) & 0xFF, ct->ct_seq & 0xFF, 0x00, 0x06 }; + memcpy(&buf[pos], end, sizeof(end)); + pos += sizeof(end); + buf[10] = ((pos - 5 - 12) & 0xF00) >> 8; + buf[11] = ((pos - 5 - 12) & 0xFF); + buf[4] = ((pos - 6) >> 8); + buf[5] = ((pos - 6) & 0xFF); + + buf[7] = sid >> 8; + buf[8] = sid & 0xFF; + + memcpy(cce->cce_ecm, data, len); + cce->cce_ecmsize = len; + + if(ct->ct_keystate != CT_RESOLVED) + tvhlog(LOG_DEBUG, "capmt", + "Trying to obtain key for service \"%s\"",t->s_svcname); + + buf[9] = pmtversion; + pmtversion = (pmtversion + 1) & 0x1F; + + capmt_send_msg(capmt, sid, buf, pos); break; } - - uint16_t sid = t->s_dvb_service_id; - uint16_t ecmpid = st->es_pid; - uint16_t transponder = 0; - - /* don't do too much requests */ - if (caid != ct->ct_caid_last) - return; - - static uint8_t pmtversion = 1; - - /* buffer for capmt */ - int pos = 0; - uint8_t buf[4094]; - - capmt_header_t head = { - .capmt_indicator = { 0x9F, 0x80, 0x32, 0x82, 0x00, 0x00 }, - .capmt_list_management = CAPMT_LIST_ONLY, - .program_number = sid, - .version_number = 0, - .current_next_indicator = 0, - .program_info_length = 0, - .capmt_cmd_id = CAPMT_CMD_OK_DESCRAMBLING, - }; - memcpy(&buf[pos], &head, sizeof(head)); - pos += sizeof(head); - - capmt_descriptor_t prd = { - .cad_type = CAPMT_DESC_PRIVATE, - .cad_length = 0x08, - .cad_data = { 0x00, 0x00, 0x00, 0x00, - sid >> 8, sid & 0xFF, - transponder >> 8, transponder & 0xFF - }}; - memcpy(&buf[pos], &prd, prd.cad_length + 2); - pos += prd.cad_length + 2; - - capmt_descriptor_t dmd = { - .cad_type = CAPMT_DESC_DEMUX, - .cad_length = 0x02, - .cad_data = { - 1 << adapter_num, adapter_num }}; - memcpy(&buf[pos], &dmd, dmd.cad_length + 2); - pos += dmd.cad_length + 2; - - capmt_descriptor_t ecd = { - .cad_type = CAPMT_DESC_PID, - .cad_length = 0x02, - .cad_data = { - ecmpid >> 8, ecmpid & 0xFF }}; - memcpy(&buf[pos], &ecd, ecd.cad_length + 2); - pos += ecd.cad_length + 2; - - LIST_FOREACH(cce2, &ct->ct_caid_ecm, cce_link) { - capmt_descriptor_t cad = { - .cad_type = 0x09, - .cad_length = 0x04, - .cad_data = { - cce2->cce_caid >> 8, cce2->cce_caid & 0xFF, - cce2->cce_ecmpid >> 8 | 0xE0, cce2->cce_ecmpid & 0xFF}}; - memcpy(&buf[pos], &cad, cad.cad_length + 2); - pos += cad.cad_length + 2; - } - - uint8_t end[] = { - 0x01, (ct->ct_seq >> 8) & 0xFF, ct->ct_seq & 0xFF, 0x00, 0x06 }; - memcpy(&buf[pos], end, sizeof(end)); - pos += sizeof(end); - buf[10] = ((pos - 5 - 12) & 0xF00) >> 8; - buf[11] = ((pos - 5 - 12) & 0xFF); - buf[4] = ((pos - 6) >> 8); - buf[5] = ((pos - 6) & 0xFF); - - - buf[7] = sid >> 8; - buf[8] = sid & 0xFF; - - memcpy(cce->cce_ecm, data, len); - cce->cce_ecmsize = len; - - if(ct->ct_keystate != CT_RESOLVED) - tvhlog(LOG_INFO, "capmt", - "Trying to obtain key for service \"%s\"",t->s_svcname); - - buf[9] = pmtversion; - pmtversion = (pmtversion + 1) & 0x1F; - - capmt_send_msg(capmt, buf, pos); + default: + /* EMM */ break; - } - default: - /* EMM */ - break; + } } } @@ -553,6 +873,92 @@ capmt_table_input(struct th_descrambler *td, struct service *t, /** * */ +#if ENABLE_DVBCSA +static int +capmt_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *st, + const uint8_t *tsb) +{ + capmt_service_t *ct = (capmt_service_t *)td; + uint8_t *pkt; + int xc0; + int ev_od; + int len; + int offset; + int n; + // FIXME: //int residue; + int i; + uint8_t *t0; + + if(ct->ct_keystate == CT_FORBIDDEN) + return 1; + + if(ct->ct_keystate != CT_RESOLVED) + return -1; + + pkt = ct->ct_tsbcluster + ct->ct_fill * 188; + memcpy(pkt, tsb, 188); + ct->ct_fill++; + + do { // handle this packet + xc0 = pkt[3] & 0xc0; + if(xc0 == 0x00) { // clear + break; + } + if(xc0 == 0x40) { // reserved + break; + } + if(xc0 == 0x80 || xc0 == 0xc0) { // encrypted + ev_od = (xc0 & 0x40) >> 6; // 0 even, 1 odd + pkt[3] &= 0x3f; // consider it decrypted now + if(pkt[3] & 0x20) { // incomplete packet + offset = 4 + pkt[4] + 1; + len = 188 - offset; + n = len >> 3; + // FIXME: //residue = len - (n << 3); + if(n == 0) { // decrypted==encrypted! + break; // this doesn't need more processing + } + } else { + len = 184; + offset = 4; + // FIXME: //n = 23; + // FIXME: //residue = 0; + } + if(ev_od == 0) { + ct->ct_tsbbatch_even[ct->ct_fill_even].data = pkt + offset; + ct->ct_tsbbatch_even[ct->ct_fill_even].len = len; + ct->ct_fill_even++; + } else { + ct->ct_tsbbatch_odd[ct->ct_fill_odd].data = pkt + offset; + ct->ct_tsbbatch_odd[ct->ct_fill_odd].len = len; + ct->ct_fill_odd++; + } + } + } while(0); + + if(ct->ct_fill != ct->ct_cluster_size) + return 0; + + if(ct->ct_fill_even) { + ct->ct_tsbbatch_even[ct->ct_fill_even].data = NULL; + dvbcsa_bs_decrypt(ct->ct_key_even, ct->ct_tsbbatch_even, 184); + ct->ct_fill_even = 0; + } + if(ct->ct_fill_odd) { + ct->ct_tsbbatch_odd[ct->ct_fill_odd].data = NULL; + dvbcsa_bs_decrypt(ct->ct_key_odd, ct->ct_tsbbatch_odd, 184); + ct->ct_fill_odd = 0; + } + + t0 = ct->ct_tsbcluster; + for(i = 0; i < ct->ct_fill; i++) { + ts_recv_packet2(t, t0); + t0 += 188; + } + ct->ct_fill = 0; + return 0; +} +#else static int capmt_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *st, const uint8_t *tsb) @@ -592,6 +998,7 @@ capmt_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *s } return 0; } +#endif /** * Check if our CAID's matches, and if so, link @@ -609,6 +1016,14 @@ capmt_service_start(service_t *t) lock_assert(&global_lock); TAILQ_FOREACH(capmt, &capmts, capmt_link) { + /* skip, if we're not active */ + if (!capmt->capmt_enabled) + continue; + + + if (!(t->s_dvb_mux_instance && t->s_dvb_mux_instance->tdmi_adapter)) + continue; + tvhlog(LOG_INFO, "capmt", "Starting capmt server for service \"%s\" on tuner %d", t->s_svcname, @@ -617,30 +1032,48 @@ capmt_service_start(service_t *t) elementary_stream_t *st; /* create new capmt service */ - ct = calloc(1, sizeof(capmt_service_t)); - ct->ct_cluster_size = get_suggested_cluster_size(); - ct->ct_tsbcluster = malloc(ct->ct_cluster_size * 188); - ct->ct_seq = capmt->capmt_seq++; + ct = calloc(1, sizeof(capmt_service_t)); +#if ENABLE_DVBCSA + ct->ct_cluster_size = dvbcsa_bs_batch_size(); +#else + ct->ct_cluster_size = get_suggested_cluster_size(); +#endif + ct->ct_tsbcluster = malloc(ct->ct_cluster_size * 188); + ct->ct_seq = capmt->capmt_seq++; +#if ENABLE_DVBCSA + ct->ct_tsbbatch_even = malloc((ct->ct_cluster_size + 1) * + sizeof(struct dvbcsa_bs_batch_s)); + ct->ct_tsbbatch_odd = malloc((ct->ct_cluster_size + 1) * + sizeof(struct dvbcsa_bs_batch_s)); +#endif TAILQ_FOREACH(st, &t->s_components, es_link) { - caid_t *c = LIST_FIRST(&st->es_caids); - if(c == NULL) - continue; + caid_t *c; + LIST_FOREACH(c, &st->es_caids, link) { + if(c == NULL) + continue; - tvhlog(LOG_DEBUG, "capmt", - "New caid 0x%04X for service \"%s\"", c->caid, t->s_svcname); + tvhlog(LOG_DEBUG, "capmt", + "New caid 0x%04X for service \"%s\"", c->caid, t->s_svcname); - /* add it to list */ - cce = calloc(1, sizeof(capmt_caid_ecm_t)); - cce->cce_caid = c->caid; - cce->cce_ecmpid = st->es_pid; - LIST_INSERT_HEAD(&ct->ct_caid_ecm, cce, cce_link); + /* add it to list */ + cce = calloc(1, sizeof(capmt_caid_ecm_t)); + cce->cce_caid = c->caid; + cce->cce_ecmpid = st->es_pid; + cce->cce_providerid = c->providerid; + LIST_INSERT_HEAD(&ct->ct_caid_ecm, cce, cce_link); - /* sending request will be based on first seen caid */ - ct->ct_caid_last = -1; + /* sending request will be based on first seen caid */ + ct->ct_caid_last = -1; + } } +#if ENABLE_DVBCSA + ct->ct_key_even = dvbcsa_bs_key_alloc(); + ct->ct_key_odd = dvbcsa_bs_key_alloc(); +#else ct->ct_keys = get_key_struct(); +#endif ct->ct_capmt = capmt; ct->ct_service = t; @@ -726,6 +1159,7 @@ capmt_record_build(capmt_t *capmt) htsmsg_add_str(e, "camdfilename", capmt->capmt_sockfile ?: ""); htsmsg_add_u32(e, "port", capmt->capmt_port); + htsmsg_add_u32(e, "oscam", !!capmt->capmt_oscam); htsmsg_add_str(e, "comment", capmt->capmt_comment ?: ""); return e; @@ -753,7 +1187,10 @@ capmt_entry_update(void *opaque, const char *id, htsmsg_t *values, int maycreate if(!htsmsg_get_u32(values, "port", &u32)) capmt->capmt_port = u32; - + + if(!htsmsg_get_u32(values, "oscam", &u32)) + capmt->capmt_oscam = u32; + if((s = htsmsg_get_str(values, "comment")) != NULL) { free(capmt->capmt_comment); capmt->capmt_comment = strdup(s); diff --git a/src/channels.c b/src/channels.c index 686c3b16..36404368 100644 --- a/src/channels.c +++ b/src/channels.c @@ -38,7 +38,8 @@ #include "dtable.h" #include "notify.h" #include "dvr/dvr.h" -#include "htsp.h" +#include "htsp_server.h" +#include "imagecache.h" struct channel_tree channel_name_tree; static struct channel_tree channel_identifier_tree; @@ -113,7 +114,6 @@ chidcmp(const channel_t *a, const channel_t *b) return a->ch_id - b->ch_id; } - /** * */ @@ -199,7 +199,9 @@ channel_create2(const char *name, int number) channel_t * channel_create ( void ) { - return channel_create2(NULL, 0); + channel_t *ch = channel_create2(NULL, 0); + channel_save(ch); + return ch; } /** @@ -210,14 +212,14 @@ channel_find_by_name(const char *name, int create, int channel_number) { channel_t skel, *ch; - if (!name || !*name) return NULL; - lock_assert(&global_lock); - skel.ch_name = (char *)name; - ch = RB_FIND(&channel_name_tree, &skel, ch_name_link, channelcmp); - if(ch != NULL || create == 0) - return ch; + if (name) { + skel.ch_name = (char *)name; + ch = RB_FIND(&channel_name_tree, &skel, ch_name_link, channelcmp); + if(ch != NULL || create == 0) + return ch; + } return channel_create2(name, channel_number); } @@ -268,6 +270,7 @@ channel_load_one(htsmsg_t *c, int id) epggrab_channel_add(ch); tvh_str_update(&ch->ch_icon, htsmsg_get_str(c, "icon")); + imagecache_get_id(ch->ch_icon); htsmsg_get_s32(c, "dvr_extra_time_pre", &ch->ch_dvr_extra_time_pre); htsmsg_get_s32(c, "dvr_extra_time_post", &ch->ch_dvr_extra_time_post); @@ -343,6 +346,7 @@ channel_save(channel_t *ch) int channel_rename(channel_t *ch, const char *newname) { + dvr_entry_t *de; service_t *t; lock_assert(&global_lock); @@ -361,6 +365,11 @@ channel_rename(channel_t *ch, const char *newname) LIST_FOREACH(t, &ch->ch_services, s_ch_link) t->s_config_save(t); + + LIST_FOREACH(de, &ch->ch_dvrs, de_channel_link) { + dvr_entry_save(de); + dvr_entry_notify(de); + } channel_save(ch); htsp_channel_update(ch); @@ -452,6 +461,7 @@ channel_set_icon(channel_t *ch, const char *icon) free(ch->ch_icon); ch->ch_icon = strdup(icon); + imagecache_get_id(icon); channel_save(ch); htsp_channel_update(ch); } diff --git a/src/config2.c b/src/config2.c index 5bcbd0d1..cfeeb127 100644 --- a/src/config2.c +++ b/src/config2.c @@ -43,6 +43,31 @@ htsmsg_t *config_get_all ( void ) return htsmsg_copy(config); } +static int _config_set_str ( const char *fld, const char *val ) +{ + const char *c = htsmsg_get_str(config, fld); + if (!c || strcmp(c, val)) { + if (c) htsmsg_delete_field(config, fld); + htsmsg_add_str(config, fld, val); + return 1; + } + return 0; +} + +#if 0 +static int _config_set_u32 ( const char *fld, uint32_t val ) +{ + uint32_t u32; + int ret = htsmsg_get_u32(config, fld, &u32); + if (ret || (u32 != val)) { + if (!ret) htsmsg_delete_field(config, fld); + htsmsg_add_u32(config, fld, val); + return 1; + } + return 0; +} +#endif + const char *config_get_language ( void ) { return htsmsg_get_str(config, "language"); @@ -50,13 +75,7 @@ const char *config_get_language ( void ) int config_set_language ( const char *lang ) { - const char *c = config_get_language(); - if (!c || strcmp(c, lang)) { - if (c) htsmsg_delete_field(config, "language"); - htsmsg_add_str(config, "language", lang); - return 1; - } - return 0; + return _config_set_str("language", lang); } const char *config_get_muxconfpath ( void ) @@ -66,11 +85,5 @@ const char *config_get_muxconfpath ( void ) int config_set_muxconfpath ( const char *path ) { - const char *c = config_get_muxconfpath(); - if (!c || strcmp(c, path)) { - if (c) htsmsg_delete_field(config, "muxconfpath"); - htsmsg_add_str(config, "muxconfpath", path); - return 1; - } - return 0; + return _config_set_str("muxconfpath", path); } diff --git a/src/cwc.c b/src/cwc.c index 51b8f2e7..60d2cb11 100644 --- a/src/cwc.c +++ b/src/cwc.c @@ -1,6 +1,6 @@ /* * tvheadend, CWC interface - * Copyright (C) 2007 Andreas Öman + * 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 @@ -27,19 +27,24 @@ #include #include #include +#include #include "tvheadend.h" #include "tcp.h" #include "psi.h" #include "tsdemux.h" -#include "ffdecsa/FFdecsa.h" #include "cwc.h" #include "notify.h" #include "atomic.h" #include "dtable.h" #include "subscriptions.h" +#include "service.h" -#include +#if ENABLE_DVBCSA +#include +#else +#include "ffdecsa/FFdecsa.h" +#endif /** * @@ -144,7 +149,16 @@ typedef struct cwc_service { LIST_ENTRY(cwc_service) cs_link; - int cs_okchannel; + int cs_channel; + + /** + * ECM Status + */ + enum { + ECM_INIT, + ECM_VALID, + ECM_RESET + } ecm_state; /** * Status of the key(s) in cs_keys @@ -156,8 +170,12 @@ typedef struct cwc_service { CS_IDLE } cs_keystate; +#if ENABLE_DVBCSA + struct dvbcsa_bs_key_s *cs_key_even; + struct dvbcsa_bs_key_s *cs_key_odd; +#else void *cs_keys; - +#endif uint8_t cs_cw[16]; int cs_pending_cw_update; @@ -168,6 +186,12 @@ typedef struct cwc_service { int cs_cluster_size; uint8_t *cs_tsbcluster; int cs_fill; +#if ENABLE_DVBCSA + struct dvbcsa_bs_batch_s *cs_tsbbatch_even; + struct dvbcsa_bs_batch_s *cs_tsbbatch_odd; + int cs_fill_even; + int cs_fill_odd; +#endif LIST_HEAD(, ecm_pid) cs_pids; @@ -462,7 +486,7 @@ cwc_send_msg(cwc_t *cwc, const uint8_t *msg, size_t len, int sid, int enq) { cwc_message_t *cm = malloc(sizeof(cwc_message_t)); uint8_t *buf = cm->cm_data; - int seq, n; + int seq; if(len + 12 > CWS_NETMSGSIZE) { free(cm); @@ -498,8 +522,7 @@ cwc_send_msg(cwc_t *cwc, const uint8_t *msg, size_t len, int sid, int enq) pthread_cond_signal(&cwc->cwc_writer_cond); pthread_mutex_unlock(&cwc->cwc_writer_mutex); } else { - n = write(cwc->cwc_fd, buf, len); - if(n != len) + if (tvh_write(cwc->cwc_fd, buf, len)) tvhlog(LOG_INFO, "cwc", "write error %s", strerror(errno)); free(cm); @@ -754,28 +777,20 @@ handle_ecm_reply(cwc_service_t *ct, ecm_section_t *es, uint8_t *msg, int len, int seq) { service_t *t = ct->cs_service; + ecm_pid_t *ep, *epn; cwc_service_t *ct2; cwc_t *cwc2; - ecm_pid_t *ep; char chaninfo[32]; int i; int64_t delay = (getmonoclock() - es->es_time) / 1000LL; // in ms - - if(es->es_channel != -1) { - snprintf(chaninfo, sizeof(chaninfo), " (channel %d)", es->es_channel); - } else { - chaninfo[0] = 0; - } - es->es_pending = 0; + snprintf(chaninfo, sizeof(chaninfo), " (PID %d)", es->es_channel); + if(len < 19) { /* ERROR */ - if(ct->cs_okchannel == es->es_channel) - ct->cs_okchannel = -1; - if (es->es_nok < 3) es->es_nok++; @@ -823,13 +838,23 @@ forbid: "Can not descramble service \"%s\", access denied (seqno: %d " "Req delay: %"PRId64" ms)", t->s_svcname, seq, delay); + ct->cs_keystate = CS_FORBIDDEN; + ct->ecm_state = ECM_RESET; + return; } else { - ct->cs_okchannel = es->es_channel; es->es_nok = 0; + ct->cs_channel = es->es_channel; + ct->ecm_state = ECM_VALID; + + if(t->s_prefcapid == 0 || t->s_prefcapid != ct->cs_channel) { + t->s_prefcapid = ct->cs_channel; + tvhlog(LOG_DEBUG, "cwc", "Saving prefered PID %d", t->s_prefcapid); + service_request_save(t, 0); + } tvhlog(LOG_DEBUG, "cwc", "Received ECM reply%s for service \"%s\" " @@ -857,7 +882,7 @@ forbid: } if(ct->cs_keystate != CS_RESOLVED) - tvhlog(LOG_INFO, "cwc", + tvhlog(LOG_DEBUG, "cwc", "Obtained key for service \"%s\" in %"PRId64" ms, from %s:%i", t->s_svcname, delay, ct->cs_cwc->cwc_hostname, ct->cs_cwc->cwc_port); @@ -865,6 +890,22 @@ forbid: ct->cs_keystate = CS_RESOLVED; memcpy(ct->cs_cw, msg + 3, 16); ct->cs_pending_cw_update = 1; + + ep = LIST_FIRST(&ct->cs_pids); + while(ep != NULL) { + if (ct->cs_channel == ep->ep_pid) { + ep = LIST_NEXT(ep, ep_link); + } + else { + epn = LIST_NEXT(ep, ep_link); + for(i = 0; i < 256; i++) + free(ep->ep_sections[i]); + LIST_REMOVE(ep, ep_link); + tvhlog(LOG_WARNING, "cwc", "Delete ECM (PID %d) for service \"%s\"", ep->ep_pid, t->s_svcname); + free(ep); + ep = epn; + } + } } } @@ -902,6 +943,9 @@ cwc_running_reply(cwc_t *cwc, uint8_t msgtype, uint8_t *msg, int len) } } tvhlog(LOG_WARNING, "cwc", "Got unexpected ECM reply (seqno: %d)", seq); + LIST_FOREACH(ct, &cwc->cwc_services, cs_link) { + ct->ecm_state = ECM_RESET; + } break; } return 0; @@ -995,7 +1039,8 @@ cwc_writer_thread(void *aux) TAILQ_REMOVE(&cwc->cwc_writeq, cm, cm_link); pthread_mutex_unlock(&cwc->cwc_writer_mutex); // int64_t ts = getmonoclock(); - r = write(cwc->cwc_fd, cm->cm_data, cm->cm_len); + if (tvh_write(cwc->cwc_fd, cm->cm_data, cm->cm_len)) + tvhlog(LOG_INFO, "cwc", "write error %s", strerror(errno)); // printf("Write took %lld usec\n", getmonoclock() - ts); free(cm); pthread_mutex_lock(&cwc->cwc_writer_mutex); @@ -1592,11 +1637,40 @@ cwc_table_input(struct th_descrambler *td, struct service *t, } if(ep == NULL) { - ep = calloc(1, sizeof(ecm_pid_t)); - ep->ep_pid = st->es_pid; - LIST_INSERT_HEAD(&ct->cs_pids, ep, ep_link); + if (ct->ecm_state == ECM_RESET) { + ct->ecm_state = ECM_INIT; + ct->cs_channel = -1; + t->s_prefcapid = 0; + tvhlog(LOG_DEBUG, "cwc", "Reset after unexpected or no reply for service \"%s\"", t->s_svcname); + } + + if (ct->ecm_state == ECM_INIT) { + // Validate prefered ECM PID + if(t->s_prefcapid != 0) { + struct elementary_stream *prefca = service_stream_find(t, t->s_prefcapid); + if (!prefca || prefca->es_type != SCT_CA) { + tvhlog(LOG_DEBUG, "cwc", "Invalid prefered ECM (PID %d) found for service \"%s\"", t->s_prefcapid, t->s_svcname); + t->s_prefcapid = 0; + } + } + + if(t->s_prefcapid == st->es_pid) { + ep = calloc(1, sizeof(ecm_pid_t)); + ep->ep_pid = t->s_prefcapid; + LIST_INSERT_HEAD(&ct->cs_pids, ep, ep_link); + tvhlog(LOG_DEBUG, "cwc", "Insert prefered ECM (PID %d) for service \"%s\"", t->s_prefcapid, t->s_svcname); + } + else if(t->s_prefcapid == 0) { + ep = calloc(1, sizeof(ecm_pid_t)); + ep->ep_pid = st->es_pid; + LIST_INSERT_HEAD(&ct->cs_pids, ep, ep_link); + tvhlog(LOG_DEBUG, "cwc", "Insert new ECM (PID %d) for service \"%s\"", st->es_pid, t->s_svcname); + } + } } + if(ep == NULL) + return; LIST_FOREACH(c, &st->es_caids, link) { if(cwc->cwc_caid == c->caid) @@ -1615,17 +1689,16 @@ cwc_table_input(struct th_descrambler *td, struct service *t, /* ECM */ if(cwc->cwc_caid >> 8 == 6) { - channel = data[6] << 8 | data[7]; - snprintf(chaninfo, sizeof(chaninfo), " (channel %d)", channel); ep->ep_last_section = data[5]; section = data[4]; } else { - channel = -1; - chaninfo[0] = 0; ep->ep_last_section = 0; section = 0; } + channel = st->es_pid; + snprintf(chaninfo, sizeof(chaninfo), " (PID %d)", channel); + if(ep->ep_sections[section] == NULL) ep->ep_sections[section] = calloc(1, sizeof(ecm_section_t)); @@ -1650,18 +1723,17 @@ cwc_table_input(struct th_descrambler *td, struct service *t, memcpy(es->es_ecm, data, len); es->es_ecmsize = len; - if(ct->cs_okchannel != -1 && channel != -1 && - ct->cs_okchannel != channel) { - tvhlog(LOG_DEBUG, "cwc", "Filtering ECM channel %d", channel); + if(ct->cs_channel >= 0 && channel != -1 && + ct->cs_channel != channel) { + tvhlog(LOG_DEBUG, "cwc", "Filtering ECM (PID %d)", channel); return; } es->es_seq = cwc_send_msg(cwc, data, len, sid, 1); tvhlog(LOG_DEBUG, "cwc", - "Sending ECM%s section=%d/%d, for service %s (seqno: %d) PID %d", - chaninfo, section, ep->ep_last_section, t->s_svcname, es->es_seq, - st->es_pid); + "Sending ECM%s section=%d/%d, for service \"%s\" (seqno: %d)", + chaninfo, section, ep->ep_last_section, t->s_svcname, es->es_seq); es->es_time = getmonoclock(); break; @@ -1761,7 +1833,6 @@ cwc_emm_cryptoworks(cwc_t *cwc, uint8_t *data, int len) if (cwc->cwc_cryptoworks_emm.shared_emm) { free(cwc->cwc_cryptoworks_emm.shared_emm); cwc->cwc_cryptoworks_emm.shared_len = 0; - cwc->cwc_cryptoworks_emm.shared_emm = (uint8_t *)malloc(len); } cwc->cwc_cryptoworks_emm.shared_emm = malloc(len); if (cwc->cwc_cryptoworks_emm.shared_emm) { @@ -1789,7 +1860,8 @@ cwc_emm_cryptoworks(cwc_t *cwc, uint8_t *data, int len) cwc_send_msg(cwc, composed, elen + 12, 0, 1); free(composed); free(tmp); - } + } else if (tmp) + free(tmp); cwc->cwc_cryptoworks_emm.shared_emm = NULL; cwc->cwc_cryptoworks_emm.shared_len = 0; } @@ -1812,19 +1884,15 @@ cwc_emm_bulcrypt(cwc_t *cwc, uint8_t *data, int len) int match = 0; switch (data[0]) { - case 0x82: /* unique */ - case 0x85: /* unique */ + case 0x82: /* unique - bulcrypt (1 card) */ + case 0x8a: /* unique - polaris (1 card) */ + case 0x85: /* unique - bulcrypt (4 cards) */ + case 0x8b: /* unique - polaris (4 cards) */ match = len >= 10 && memcmp(data + 3, cwc->cwc_ua + 2, 3) == 0; break; - case 0x84: /* shared */ + case 0x84: /* shared - (1024 cards) */ match = len >= 10 && memcmp(data + 3, cwc->cwc_ua + 2, 2) == 0; break; - case 0x8b: /* shared-unknown */ - match = len >= 10 && memcmp(data + 4, cwc->cwc_ua + 2, 2) == 0; - break; - case 0x8a: /* global */ - match = len >= 10 && memcmp(data + 4, cwc->cwc_ua + 2, 1) == 0; - break; } if (match) @@ -1841,13 +1909,21 @@ update_keys(cwc_service_t *ct) ct->cs_pending_cw_update = 0; for(i = 0; i < 8; i++) if(ct->cs_cw[i]) { +#if ENABLE_DVBCSA + dvbcsa_bs_key_set(ct->cs_cw, ct->cs_key_even); +#else set_even_control_word(ct->cs_keys, ct->cs_cw); +#endif break; } for(i = 0; i < 8; i++) if(ct->cs_cw[8 + i]) { +#if ENABLE_DVBCSA + dvbcsa_bs_key_set(ct->cs_cw + 8, ct->cs_key_odd); +#else set_odd_control_word(ct->cs_keys, ct->cs_cw + 8); +#endif break; } } @@ -1856,6 +1932,101 @@ update_keys(cwc_service_t *ct) /** * */ +#if ENABLE_DVBCSA +static int +cwc_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *st, + const uint8_t *tsb) +{ + cwc_service_t *ct = (cwc_service_t *)td; + uint8_t *pkt; + int xc0; + int ev_od; + int len; + int offset; + int n; + // FIXME: //int residue; + + if(ct->cs_keystate == CS_FORBIDDEN) + return 1; + + if(ct->cs_keystate != CS_RESOLVED) + return -1; + + if(ct->cs_fill == 0 && ct->cs_pending_cw_update) + update_keys(ct); + + pkt = ct->cs_tsbcluster + ct->cs_fill * 188; + memcpy(pkt, tsb, 188); + ct->cs_fill++; + + do { // handle this packet + xc0 = pkt[3] & 0xc0; + if(xc0 == 0x00) { // clear + break; + } + if(xc0 == 0x40) { // reserved + break; + } + if(xc0 == 0x80 || xc0 == 0xc0) { // encrypted + ev_od = (xc0 & 0x40) >> 6; // 0 even, 1 odd + pkt[3] &= 0x3f; // consider it decrypted now + if(pkt[3] & 0x20) { // incomplete packet + offset = 4 + pkt[4] + 1; + len = 188 - offset; + n = len >> 3; + // FIXME: //residue = len - (n << 3); + if(n == 0) { // decrypted==encrypted! + break; // this doesn't need more processing + } + } else { + len = 184; + offset = 4; + // FIXME: //n = 23; + // FIXME: //residue = 0; + } + if(ev_od == 0) { + ct->cs_tsbbatch_even[ct->cs_fill_even].data = pkt + offset; + ct->cs_tsbbatch_even[ct->cs_fill_even].len = len; + ct->cs_fill_even++; + } else { + ct->cs_tsbbatch_odd[ct->cs_fill_odd].data = pkt + offset; + ct->cs_tsbbatch_odd[ct->cs_fill_odd].len = len; + ct->cs_fill_odd++; + } + } + } while(0); + + if(ct->cs_fill != ct->cs_cluster_size) + return 0; + + if(ct->cs_fill_even) { + ct->cs_tsbbatch_even[ct->cs_fill_even].data = NULL; + dvbcsa_bs_decrypt(ct->cs_key_even, ct->cs_tsbbatch_even, 184); + ct->cs_fill_even = 0; + } + if(ct->cs_fill_odd) { + ct->cs_tsbbatch_odd[ct->cs_fill_odd].data = NULL; + dvbcsa_bs_decrypt(ct->cs_key_odd, ct->cs_tsbbatch_odd, 184); + ct->cs_fill_odd = 0; + } + + { + int i; + const uint8_t *t0 = ct->cs_tsbcluster; + + for(i = 0; i < ct->cs_fill; i++) { + ts_recv_packet2(t, t0); + t0 += 188; + } + } + ct->cs_fill = 0; + + if(ct->cs_pending_cw_update) + update_keys(ct); + + return 0; +} +#else static int cwc_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *st, const uint8_t *tsb) @@ -1914,6 +2085,7 @@ cwc_descramble(th_descrambler_t *td, service_t *t, struct elementary_stream *st, return 0; } +#endif /** * cwc_mutex is held @@ -1937,7 +2109,14 @@ cwc_service_destroy(th_descrambler_t *td) LIST_REMOVE(ct, cs_link); +#if ENABLE_DVBCSA + dvbcsa_bs_key_free(ct->cs_key_odd); + dvbcsa_bs_key_free(ct->cs_key_even); + free(ct->cs_tsbbatch_odd); + free(ct->cs_tsbbatch_even); +#else free_key_struct(ct->cs_keys); +#endif free(ct->cs_tsbcluster); free(ct); } @@ -1981,14 +2160,27 @@ cwc_service_start(service_t *t) if(cwc_find_stream_by_caid(t, cwc->cwc_caid) == NULL) continue; - ct = calloc(1, sizeof(cwc_service_t)); - ct->cs_cluster_size = get_suggested_cluster_size(); - ct->cs_tsbcluster = malloc(ct->cs_cluster_size * 188); - - ct->cs_keys = get_key_struct(); - ct->cs_cwc = cwc; - ct->cs_service = t; - ct->cs_okchannel = -1; + ct = calloc(1, sizeof(cwc_service_t)); +#if ENABLE_DVBCSA + ct->cs_cluster_size = dvbcsa_bs_batch_size(); +#else + ct->cs_cluster_size = get_suggested_cluster_size(); +#endif + ct->cs_tsbcluster = malloc(ct->cs_cluster_size * 188); +#if ENABLE_DVBCSA + ct->cs_tsbbatch_even = malloc((ct->cs_cluster_size + 1) * + sizeof(struct dvbcsa_bs_batch_s)); + ct->cs_tsbbatch_odd = malloc((ct->cs_cluster_size + 1) * + sizeof(struct dvbcsa_bs_batch_s)); + ct->cs_key_even = dvbcsa_bs_key_alloc(); + ct->cs_key_odd = dvbcsa_bs_key_alloc(); +#else + ct->cs_keys = get_key_struct(); +#endif + ct->cs_cwc = cwc; + ct->cs_service = t; + ct->cs_channel = -1; + ct->ecm_state = ECM_INIT; td = &ct->cs_head; td->td_stop = cwc_service_destroy; @@ -2012,11 +2204,9 @@ cwc_service_start(service_t *t) static void cwc_destroy(cwc_t *cwc) { - pthread_mutex_lock(&cwc_mutex); TAILQ_REMOVE(&cwcs, cwc, cwc_link); cwc->cwc_running = 0; pthread_cond_signal(&cwc->cwc_cond); - pthread_mutex_unlock(&cwc_mutex); } diff --git a/src/dvb/diseqc.c b/src/dvb/diseqc.c old mode 100644 new mode 100755 index f4468588..29c948d3 --- a/src/dvb/diseqc.c +++ b/src/dvb/diseqc.c @@ -5,34 +5,6 @@ #include "tvheadend.h" #include "diseqc.h" -//#define DISEQC_TRACE - -struct dvb_diseqc_master_cmd diseqc_commited_cmds[] = { - { { 0xe0, 0x10, 0x38, 0xf0, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf1, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf2, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf3, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf4, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf5, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf6, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf7, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf8, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xf9, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xfa, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xfb, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xfc, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xfd, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xfe, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x38, 0xff, 0x00, 0x00 }, 4 } -}; - -struct dvb_diseqc_master_cmd diseqc_uncommited_cmds[] = { - { { 0xe0, 0x10, 0x39, 0xf0, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x39, 0xf1, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x39, 0xf2, 0x00, 0x00 }, 4 }, - { { 0xe0, 0x10, 0x39, 0xf3, 0x00, 0x00 }, 4 } -}; - /*--------------------------------------------------------------------------*/ static inline void @@ -45,60 +17,106 @@ msleep(uint32_t msec) } int -diseqc_setup(int fe_fd, int input, int voltage, int band, int diseqc_ver) +diseqc_send_msg(int fe_fd, __u8 framing_byte, __u8 address, __u8 cmd, + __u8 data_1, __u8 data_2, __u8 data_3, __u8 msg_len) { - int i = (input % 4) * 4 + voltage * 2 + (band ? 1 : 0); - int j = input / 4; int err; + struct dvb_diseqc_master_cmd message; -#ifdef DISEQC_TRACE - tvhlog(LOG_INFO, "diseqc", - "fe_fd %i, input %i, voltage %i, band %i, diseqc_ver %i, i %i, j %i", - fe_fd, input, voltage, band, diseqc_ver, i, j); -#endif - /* check for invalid input number or diseqc command indexes */ - if(input < 0 || input >=16 || i < 0 || i >= 16 || j < 0 || j >= 4) + tvhtrace("diseqc", "sending %X %X %X %X %X %X", + framing_byte, address, cmd, data_1, data_2, data_3); + + message.msg[0] = framing_byte; + message.msg[1] = address; + message.msg[2] = cmd; + message.msg[3] = data_1; + message.msg[4] = data_2; + message.msg[5] = data_3; + message.msg_len = msg_len; + if ((err = ioctl(fe_fd, FE_DISEQC_SEND_MASTER_CMD, &message))) { + tvhlog(LOG_ERR, "diseqc", "error sending diseqc command"); + return err; + } + return 0; +} + +int +diseqc_setup(int fe_fd, int lnb_num, int voltage, int band, + uint32_t version, uint32_t repeats) +{ + int i = (lnb_num % 4) * 4 + voltage * 2 + (band ? 1 : 0); + int j = lnb_num / 4; + int k, err; + + tvhtrace("diseqc", + "fe_fd=%i, lnb_num=%i, voltage=%i, band=%i, version=%i, repeats=%i", + fe_fd, lnb_num, voltage, band, version, repeats); + + /* verify lnb number and diseqc data */ + if(lnb_num < 0 || lnb_num >=64 || i < 0 || i >= 16 || j < 0 || j >= 16) return -1; /* turn off continuous tone */ - if ((err = ioctl(fe_fd, FE_SET_TONE, SEC_TONE_OFF))) + tvhtrace("diseqc", "disabling continuous tone"); + if ((err = ioctl(fe_fd, FE_SET_TONE, SEC_TONE_OFF))) { + tvhlog(LOG_ERR, "diseqc", "error trying to turn off continuous tone"); return err; + } /* set lnb voltage */ - if ((err = ioctl(fe_fd, FE_SET_VOLTAGE, - (i/2) % 2 ? SEC_VOLTAGE_18 : SEC_VOLTAGE_13))) + tvhtrace("diseqc", "setting lnb voltage to %iV", (i/2) % 2 ? 18 : 13); + if ((err = ioctl(fe_fd, FE_SET_VOLTAGE, (i/2) % 2 ? SEC_VOLTAGE_18 : SEC_VOLTAGE_13))) { + tvhlog(LOG_ERR, "diseqc", "error setting lnb voltage"); return err; + } msleep(15); - /* send uncommited command */ - if ((err = ioctl(fe_fd, FE_DISEQC_SEND_MASTER_CMD, - &diseqc_uncommited_cmds[j]))) - return err; + if (repeats == 0) { /* uncommited msg, wait 15ms, commited msg */ + if ((err = diseqc_send_msg(fe_fd, 0xE0, 0x10, 0x39, 0xF0 | j, 0, 0, 4))) + return err; + msleep(15); + if ((err = diseqc_send_msg(fe_fd, 0xE0, 0x10, 0x38, 0xF0 | i, 0, 0, 4))) + return err; + } else { /* commited msg, 25ms, uncommited msg, 25ms, commited msg, etc */ + if ((err = diseqc_send_msg(fe_fd, 0xE0, 0x10, 0x38, 0xF0 | i, 0, 0, 4))) + return err; + for (k = 0; k < repeats; k++) { + msleep(25); + if ((err = diseqc_send_msg(fe_fd, 0xE0, 0x10, 0x39, 0xF0 | j, 0, 0, 4))) + return err; + msleep(25); + if ((err = diseqc_send_msg(fe_fd, 0xE1, 0x10, 0x38, 0xF0 | i, 0, 0, 4))) + return err; + } + } msleep(15); - /* send commited command */ - if ((err = ioctl(fe_fd, FE_DISEQC_SEND_MASTER_CMD, - &diseqc_commited_cmds[i]))) - return err; -#ifdef DISEQC_TRACE - tvhlog(LOG_INFO, "diseqc", "E0 10 39 F%X - E0 10 38 F%X sent", j, i); -#endif - msleep(15); - - /* send toneburst command */ - if ((err = ioctl(fe_fd, FE_DISEQC_SEND_BURST, - (i/4) % 2 ? SEC_MINI_B : SEC_MINI_A))) + /* set toneburst */ + tvhtrace("diseqc", (i/4) % 2 ? "sending mini diseqc B" : "sending mini diseqc A"); + if ((err = ioctl(fe_fd, FE_DISEQC_SEND_BURST, (i/4) % 2 ? SEC_MINI_B : SEC_MINI_A))) { + tvhlog(LOG_ERR, "diseqc", "error sending mini diseqc command"); return err; + } msleep(15); /* set continuous tone */ - if ((ioctl(fe_fd, FE_SET_TONE, i % 2 ? SEC_TONE_ON : SEC_TONE_OFF))) + tvhtrace("diseqc", i % 2 ? "enabling continous tone" : "disabling continuous tone"); + if ((err = ioctl(fe_fd, FE_SET_TONE, i % 2 ? SEC_TONE_ON : SEC_TONE_OFF))) { + tvhlog(LOG_ERR, "diseqc", "error setting continuous tone"); return err; + } return 0; } int diseqc_voltage_off(int fe_fd) { - return ioctl(fe_fd, FE_SET_VOLTAGE, SEC_VOLTAGE_OFF); + int err; + + tvhtrace("diseqc", "sending diseqc voltage off command"); + if ((err = ioctl(fe_fd, FE_SET_VOLTAGE, SEC_VOLTAGE_OFF))) { + tvhlog(LOG_ERR, "diseqc", "error sending diseqc voltage off command"); + return err; + } + return 0; } diff --git a/src/dvb/diseqc.h b/src/dvb/diseqc.h index fdabb6df..852a7948 100644 --- a/src/dvb/diseqc.h +++ b/src/dvb/diseqc.h @@ -7,7 +7,10 @@ /** * set up the switch to position/voltage/tone */ -int diseqc_setup(int fe_fd, int input, int voltage, int band, int diseqc_ver); +int diseqc_send_msg(int fe_fd, __u8 framing_byte, __u8 address, __u8 cmd, + __u8 data_1, __u8 data_2, __u8 data_3, __u8 msg_len); +int diseqc_setup(int fe_fd, int lnb_num, int voltage, int band, + uint32_t version, uint32_t repeats); int diseqc_voltage_off(int fe_fd); #endif diff --git a/src/dvb/dvb.c b/src/dvb/dvb.c index 4c764eba..9e3775b3 100644 --- a/src/dvb/dvb.c +++ b/src/dvb/dvb.c @@ -23,8 +23,8 @@ #include "dvb_charset.h" void -dvb_init(uint32_t adapter_mask) +dvb_init(uint32_t adapter_mask, const char *rawfile) { dvb_charset_init(); - dvb_adapter_init(adapter_mask); + dvb_adapter_init(adapter_mask, rawfile); } diff --git a/src/dvb/dvb.h b/src/dvb/dvb.h index c29bef1a..127d676f 100644 --- a/src/dvb/dvb.h +++ b/src/dvb/dvb.h @@ -23,6 +23,17 @@ #include #include #include "htsmsg.h" +#include "psi.h" + +struct service; +struct th_dvb_table; +struct th_dvb_mux_instance; + +#define TDA_OPT_FE 0x1 +#define TDA_OPT_DVR 0x2 +#define TDA_OPT_DMX 0x4 +#define TDA_OPT_PWR 0x8 +#define TDA_OPT_ALL (TDA_OPT_FE | TDA_OPT_DVR | TDA_OPT_DMX | TDA_OPT_PWR) #define DVB_VER_INT(maj,min) (((maj) << 16) + (min)) @@ -42,7 +53,7 @@ TAILQ_HEAD(dvb_satconf_queue, dvb_satconf); typedef struct dvb_satconf { char *sc_id; TAILQ_ENTRY(dvb_satconf) sc_adapter_link; - int sc_port; // diseqc switchport (0 - 15) + int sc_port; // diseqc switchport (0 - 63) char *sc_name; char *sc_comment; @@ -92,8 +103,10 @@ typedef struct th_dvb_mux_instance { struct th_dvb_adapter *tdmi_adapter; - uint16_t tdmi_snr, tdmi_signal; - uint32_t tdmi_ber, tdmi_uncorrected_blocks; + uint16_t tdmi_signal; + uint32_t tdmi_ber, tdmi_unc; + float tdmi_unc_avg; + float tdmi_snr; #define TDMI_FEC_ERR_HISTOGRAM_SIZE 10 uint32_t tdmi_fec_err_histogram[TDMI_FEC_ERR_HISTOGRAM_SIZE]; @@ -103,6 +116,8 @@ typedef struct th_dvb_mux_instance { LIST_HEAD(, th_dvb_table) tdmi_tables; + int tdmi_num_tables; + TAILQ_HEAD(, th_dvb_table) tdmi_table_queue; int tdmi_table_initial; @@ -143,9 +158,27 @@ typedef struct th_dvb_mux_instance { TAILQ_HEAD(, epggrab_ota_mux) tdmi_epg_grab; + struct th_subscription_list tdmi_subscriptions; + } th_dvb_mux_instance_t; + + +/** + * When in raw mode we need to enqueue raw TS packet + * to a different thread because we need to hold + * global_lock when doing delivery of the tables + */ +TAILQ_HEAD(dvb_table_feed_queue, dvb_table_feed); + +typedef struct dvb_table_feed { + TAILQ_ENTRY(dvb_table_feed) dtf_link; + uint8_t dtf_tsb[188]; +} dvb_table_feed_t; + + + /** * DVB Adapter (one of these per physical adapter) */ @@ -173,6 +206,11 @@ typedef struct th_dvb_adapter { int tda_table_epollfd; + uint32_t tda_enabled; + + int tda_locked; + time_t tda_monitor; + const char *tda_rootpath; char *tda_identifier; uint32_t tda_autodiscovery; @@ -185,21 +223,25 @@ typedef struct th_dvb_adapter { uint32_t tda_sidtochan; uint32_t tda_nitoid; uint32_t tda_diseqc_version; + uint32_t tda_diseqc_repeats; uint32_t tda_disable_pmt_monitor; + int32_t tda_full_mux_rx; + uint32_t tda_grace_period; char *tda_displayname; char *tda_fe_path; int tda_fe_fd; int tda_type; + int tda_snr_valid; struct dvb_frontend_info *tda_fe_info; int tda_adapter_num; char *tda_demux_path; - char *tda_dvr_path; + char *tda_dvr_path; pthread_t tda_dvr_thread; - int tda_dvr_pipe[2]; + th_pipe_t tda_dvr_pipe; int tda_hostconnection; @@ -209,7 +251,6 @@ typedef struct th_dvb_adapter { struct service_list tda_transports; /* Currently bound transports */ gtimer_t tda_fe_monitor_timer; - int tda_fe_monitor_hold; int tda_sat; // Set if this adapter is a satellite receiver (DVB-S, etc) @@ -218,11 +259,6 @@ typedef struct th_dvb_adapter { struct th_dvb_mux_instance_list tda_mux_list; - uint32_t tda_dump_muxes; - - int tda_allpids_dmx_fd; - int tda_dump_fd; - uint32_t tda_last_fec; int tda_unc_is_delta; /* 1 if we believe FE_READ_UNCORRECTED_BLOCKS @@ -230,6 +266,27 @@ typedef struct th_dvb_adapter { uint32_t tda_extrapriority; // extra priority for choosing the best adapter/service + void (*tda_open_service)(struct th_dvb_adapter *tda, struct service *s); + void (*tda_close_service)(struct th_dvb_adapter *tda, struct service *s); + void (*tda_open_table)(struct th_dvb_mux_instance *tdmi, struct th_dvb_table *s); + void (*tda_close_table)(struct th_dvb_mux_instance *tdmi, struct th_dvb_table *s); + + int tda_rawmode; + + int tda_bytes; + + // Full mux streaming, protected via the delivery mutex + + streaming_pad_t tda_streaming_pad; + + + struct dvb_table_feed_queue tda_table_feed; + pthread_cond_t tda_table_feed_cond; // Bound to tda_delivery_mutex + + // PIDs that needs to be requeued and processed as tables + uint8_t tda_table_filter[8192]; + + } th_dvb_adapter_t; /** @@ -256,6 +313,7 @@ typedef struct th_dvb_table { int tdt_fd; LIST_ENTRY(th_dvb_table) tdt_link; + th_dvb_mux_instance_t *tdt_tdmi; char *tdt_name; @@ -267,31 +325,39 @@ typedef struct th_dvb_table { int tdt_count; int tdt_pid; - struct dmx_sct_filter_params *tdt_fparams; - int tdt_id; + int tdt_table; + int tdt_mask; + + int tdt_destroyed; + int tdt_refcount; + + psi_section_t tdt_sect; // Manual reassembly + } th_dvb_table_t; extern struct th_dvb_adapter_queue dvb_adapters; extern struct th_dvb_mux_instance_tree dvb_muxes; -void dvb_init(uint32_t adapter_mask); +void dvb_init(uint32_t adapter_mask, const char *rawfile); /** * DVB Adapter */ -void dvb_adapter_init(uint32_t adapter_mask); +void dvb_adapter_init(uint32_t adapter_mask, const char *rawfile); void dvb_adapter_mux_scanner(void *aux); -void dvb_adapter_start (th_dvb_adapter_t *tda); +void dvb_adapter_start (th_dvb_adapter_t *tda, int opt); -void dvb_adapter_stop (th_dvb_adapter_t *tda); +void dvb_adapter_stop (th_dvb_adapter_t *tda, int opt); void dvb_adapter_set_displayname(th_dvb_adapter_t *tda, const char *s); +void dvb_adapter_set_enabled(th_dvb_adapter_t *tda, int on); + void dvb_adapter_set_auto_discovery(th_dvb_adapter_t *tda, int on); void dvb_adapter_set_skip_initialscan(th_dvb_adapter_t *tda, int on); @@ -302,8 +368,6 @@ void dvb_adapter_set_skip_checksubscr(th_dvb_adapter_t *tda, int on); void dvb_adapter_set_qmon(th_dvb_adapter_t *tda, int on); -void dvb_adapter_set_dump_muxes(th_dvb_adapter_t *tda, int on); - void dvb_adapter_set_idleclose(th_dvb_adapter_t *tda, int on); void dvb_adapter_set_poweroff(th_dvb_adapter_t *tda, int on); @@ -314,8 +378,15 @@ void dvb_adapter_set_nitoid(th_dvb_adapter_t *tda, int nitoid); void dvb_adapter_set_diseqc_version(th_dvb_adapter_t *tda, unsigned int v); +void dvb_adapter_set_diseqc_repeats(th_dvb_adapter_t *tda, + unsigned int repeats); + void dvb_adapter_set_disable_pmt_monitor(th_dvb_adapter_t *tda, int on); +void dvb_adapter_set_full_mux_rx(th_dvb_adapter_t *tda, int r); + +void dvb_adapter_set_grace_period(th_dvb_adapter_t *tda, uint32_t p); + void dvb_adapter_clone(th_dvb_adapter_t *dst, th_dvb_adapter_t *src); void dvb_adapter_clean(th_dvb_adapter_t *tda); @@ -332,6 +403,12 @@ void dvb_adapter_set_extrapriority(th_dvb_adapter_t *tda, int extrapriority); void dvb_adapter_poweroff(th_dvb_adapter_t *tda); +void dvb_input_filtered_setup(th_dvb_adapter_t *tda); + +void dvb_input_raw_setup(th_dvb_adapter_t *tda); + + + /** * DVB Multiplex */ @@ -358,11 +435,13 @@ th_dvb_mux_instance_t *dvb_mux_create(th_dvb_adapter_t *tda, uint16_t onid, uint16_t tsid, const char *network, const char *logprefix, int enabled, int initialscan, const char *identifier, - dvb_satconf_t *satconf); + dvb_satconf_t *satconf, int create, th_dvb_mux_instance_t *src); void dvb_mux_set_networkname(th_dvb_mux_instance_t *tdmi, const char *name); -void dvb_mux_set_tsid(th_dvb_mux_instance_t *tdmi, uint16_t tsid); +void dvb_mux_set_tsid(th_dvb_mux_instance_t *tdmi, uint16_t tsid, int force); + +void dvb_mux_set_onid(th_dvb_mux_instance_t *tdmi, uint16_t onid, int force); void dvb_mux_set_enable(th_dvb_mux_instance_t *tdmi, int enabled); @@ -393,24 +472,33 @@ int dvb_mux_copy(th_dvb_adapter_t *dst, th_dvb_mux_instance_t *tdmi_src, void dvb_mux_add_to_scan_queue (th_dvb_mux_instance_t *tdmi); +th_dvb_mux_instance_t *dvb_mux_find + (th_dvb_adapter_t *tda, const char *netname, uint16_t onid, uint16_t tsid, + int enabled ); + /** * DVB Transport (aka DVB service) */ -void dvb_transport_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier); +void dvb_service_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier); -struct service *dvb_transport_find(th_dvb_mux_instance_t *tdmi, +struct service *dvb_service_find(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, const char *identifier); -struct service *dvb_transport_find2(th_dvb_mux_instance_t *tdmi, +struct service *dvb_service_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, const char *identifier, int *save); -void dvb_transport_notify(struct service *t); +struct service *dvb_service_find3 + (th_dvb_adapter_t *tda, th_dvb_mux_instance_t *tdmi, + const char *netname, uint16_t onid, uint16_t tsid, uint16_t sid, + int enabled, int epgprimary); -void dvb_transport_notify_by_adapter(th_dvb_adapter_t *tda); +void dvb_service_notify(struct service *t); -htsmsg_t *dvb_transport_build_msg(struct service *t); +void dvb_service_notify_by_adapter(th_dvb_adapter_t *tda); + +htsmsg_t *dvb_service_build_msg(struct service *t); /** * DVB Frontend @@ -433,13 +521,10 @@ void dvb_table_add_pmt(th_dvb_mux_instance_t *tdmi, int pmt_pid); void dvb_table_rem_pmt(th_dvb_mux_instance_t *tdmi, int pmt_pid); -struct dmx_sct_filter_params *dvb_fparams_alloc(void); - -void -tdt_add(th_dvb_mux_instance_t *tdmi, struct dmx_sct_filter_params *fparams, - int (*callback)(th_dvb_mux_instance_t *tdmi, uint8_t *buf, int len, - uint8_t tableid, void *opaque), void *opaque, - const char *name, int flags, int pid, th_dvb_table_t *tdt); +void tdt_add(th_dvb_mux_instance_t *tdmi, int table, int mask, + int (*callback)(th_dvb_mux_instance_t *tdmi, uint8_t *buf, int len, + uint8_t tableid, void *opaque), void *opaque, + const char *name, int flags, int pid); int dvb_pidx11_callback (th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, @@ -450,6 +535,10 @@ int dvb_pidx11_callback #define TDT_CA 0x4 #define TDT_TDT 0x8 +void dvb_table_dispatch(uint8_t *sec, int r, th_dvb_table_t *tdt); + +void dvb_table_release(th_dvb_table_t *tdt); + /** * Satellite configuration */ @@ -465,4 +554,17 @@ dvb_satconf_t *dvb_satconf_entry_find(th_dvb_adapter_t *tda, void dvb_lnb_get_frequencies(const char *id, int *f_low, int *f_hi, int *f_switch); + +/** + * Raw demux + */ +struct th_subscription; +struct th_subscription *dvb_subscription_create_from_tdmi(th_dvb_mux_instance_t *tdmi, + const char *name, + streaming_target_t *st, + const char *hostname, + const char *username, + const char *client); + #endif /* DVB_H_ */ + diff --git a/src/dvb/dvb_adapter.c b/src/dvb/dvb_adapter.c index 6fc42092..00a53704 100644 --- a/src/dvb/dvb_adapter.c +++ b/src/dvb/dvb_adapter.c @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +#define _GNU_SOURCE #include #include @@ -23,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -44,11 +47,22 @@ #include "service.h" #include "epggrab.h" #include "diseqc.h" +#include "atomic.h" struct th_dvb_adapter_queue dvb_adapters; struct th_dvb_mux_instance_tree dvb_muxes; static void *dvb_adapter_input_dvr(void *aux); +static void tda_init(th_dvb_adapter_t *tda); +/** + * Adapters that are known to have SNR support + */ +static const char* dvb_adapter_snr_whitelist[] = { + "Sony CXD2820R", + "stv090x", + "TurboSight", + NULL +}; /** * @@ -64,10 +78,7 @@ tda_alloc(void) TAILQ_INIT(&tda->tda_scan_queues[i]); TAILQ_INIT(&tda->tda_initial_scan_queue); TAILQ_INIT(&tda->tda_satconfs); - - tda->tda_allpids_dmx_fd = -1; - tda->tda_dump_fd = -1; - + streaming_pad_init(&tda->tda_streaming_pad); return tda; } @@ -82,24 +93,59 @@ tda_save(th_dvb_adapter_t *tda) lock_assert(&global_lock); + htsmsg_add_u32(m, "enabled", tda->tda_enabled); + if (tda->tda_fe_path) + htsmsg_add_str(m, "fe_path", tda->tda_fe_path); + if (tda->tda_demux_path) + htsmsg_add_str(m, "dmx_path", tda->tda_demux_path); + if (tda->tda_dvr_path) + htsmsg_add_str(m, "dvr_path", tda->tda_dvr_path); htsmsg_add_str(m, "type", dvb_adaptertype_to_str(tda->tda_type)); htsmsg_add_str(m, "displayname", tda->tda_displayname); htsmsg_add_u32(m, "autodiscovery", tda->tda_autodiscovery); htsmsg_add_u32(m, "idlescan", tda->tda_idlescan); htsmsg_add_u32(m, "idleclose", tda->tda_idleclose); htsmsg_add_u32(m, "skip_checksubscr", tda->tda_skip_checksubscr); + htsmsg_add_u32(m, "sidtochan", tda->tda_sidtochan); htsmsg_add_u32(m, "qmon", tda->tda_qmon); - htsmsg_add_u32(m, "dump_muxes", tda->tda_dump_muxes); htsmsg_add_u32(m, "poweroff", tda->tda_poweroff); htsmsg_add_u32(m, "nitoid", tda->tda_nitoid); htsmsg_add_u32(m, "diseqc_version", tda->tda_diseqc_version); + htsmsg_add_u32(m, "diseqc_repeats", tda->tda_diseqc_repeats); htsmsg_add_u32(m, "extrapriority", tda->tda_extrapriority); htsmsg_add_u32(m, "skip_initialscan", tda->tda_skip_initialscan); htsmsg_add_u32(m, "disable_pmt_monitor", tda->tda_disable_pmt_monitor); + htsmsg_add_s32(m, "full_mux_rx", tda->tda_full_mux_rx); + htsmsg_add_u32(m, "grace_period", tda->tda_grace_period); hts_settings_save(m, "dvbadapters/%s", tda->tda_identifier); htsmsg_destroy(m); } +/** + * + */ +void +dvb_adapter_set_enabled(th_dvb_adapter_t *tda, int on) +{ + if(tda->tda_enabled == on) + return; + + lock_assert(&global_lock); + + tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" %s", + tda->tda_displayname, on ? "Enabled" : "Disabled"); + + tda->tda_enabled = on; + tda_save(tda); + if (!on) { + gtimer_disarm(&tda->tda_mux_scanner_timer); + if (tda->tda_mux_current) + dvb_fe_stop(tda->tda_mux_current, 0); + dvb_adapter_stop(tda, TDA_OPT_ALL); + } else { + tda_init(tda); + } +} /** * @@ -123,7 +169,6 @@ dvb_adapter_set_displayname(th_dvb_adapter_t *tda, const char *s) dvb_adapter_notify(tda); } - /** * */ @@ -272,25 +317,6 @@ dvb_adapter_set_poweroff(th_dvb_adapter_t *tda, int on) } -/** - * - */ -void -dvb_adapter_set_dump_muxes(th_dvb_adapter_t *tda, int on) -{ - if(tda->tda_dump_muxes == on) - return; - - lock_assert(&global_lock); - - tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" dump of DVB mux input set to: %s", - tda->tda_displayname, on ? "On" : "Off"); - - tda->tda_dump_muxes = on; - tda_save(tda); -} - - /** * */ @@ -333,6 +359,21 @@ dvb_adapter_set_diseqc_version(th_dvb_adapter_t *tda, unsigned int v) tda_save(tda); } +/** + * sets the number of diseqc repeats to perform + */ +void +dvb_adapter_set_diseqc_repeats(th_dvb_adapter_t *tda, unsigned int repeats) +{ + if(tda->tda_diseqc_repeats == repeats) + return; + lock_assert(&global_lock); + tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" DiSEqC repeats set to: %i", + tda->tda_displayname, repeats); + tda->tda_diseqc_repeats = repeats; + tda_save(tda); +} + /** * */ @@ -370,6 +411,47 @@ dvb_adapter_set_disable_pmt_monitor(th_dvb_adapter_t *tda, int on) } +/** + * + */ +void +dvb_adapter_set_full_mux_rx(th_dvb_adapter_t *tda, int on) +{ + const char* label[] = { "Auto", "Off", "On" }; + + if (on < -1) on = -1; + if (on > 1) on = 1; + + if(tda->tda_full_mux_rx == on) + return; + + lock_assert(&global_lock); + + tvhlog(LOG_NOTICE, "dvb", + "Adapter \"%s\" disabled full MUX receive set to: %s", + tda->tda_displayname, label[on+1]); + + tda->tda_full_mux_rx = on; + tda_save(tda); +} + +/** + * + */ +void +dvb_adapter_set_grace_period(th_dvb_adapter_t *tda, uint32_t p) +{ + if (tda->tda_grace_period == p) + return; + + tvhlog(LOG_NOTICE, "dvb", + "Adapter \"%s\" set grace period to %d", tda->tda_displayname, p); + + tda->tda_grace_period = p; + tda_save(tda); +} + + /** * */ @@ -380,7 +462,44 @@ dvb_adapter_checkspeed(th_dvb_adapter_t *tda) snprintf(dev, sizeof(dev), "dvb/dvb%d.dvr0", tda->tda_adapter_num); tda->tda_hostconnection = get_device_connection(dev); - } +} + + + + +/** + * Return 1 if an adapter is capable of receiving a full mux + */ +static int +check_full_stream(th_dvb_adapter_t *tda) +{ + struct dmx_pes_filter_params dmx_param; + int r; + + if(tda->tda_full_mux_rx != -1) + return tda->tda_full_mux_rx; + + if(tda->tda_hostconnection == HOSTCONNECTION_USB12) + return 0; // Don't even bother, device <-> host interface is too slow + + if(tda->tda_hostconnection == HOSTCONNECTION_USB480) + return 0; // USB in general appears to have CPU loading issues? + + int fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); + if(fd == -1) + return 0; + + memset(&dmx_param, 0, sizeof(dmx_param)); + dmx_param.pid = 0x2000; + dmx_param.input = DMX_IN_FRONTEND; + dmx_param.output = DMX_OUT_TS_TAP; + dmx_param.pes_type = DMX_PES_OTHER; + dmx_param.flags = DMX_IMMEDIATE_START; + + r = ioctl(fd, DMX_SET_PES_FILTER, &dmx_param); + close(fd); + return !r; +} /** @@ -389,51 +508,140 @@ dvb_adapter_checkspeed(th_dvb_adapter_t *tda) static void tda_add(int adapter_num) { - char path[200], fname[256]; - int fe, i, r; + char buf[1024], path[200], fepath[256], dmxpath[256], dvrpath[256]; + int i, j, fd; + th_dvb_adapter_t *tda; + struct dvb_frontend_info fe_info; + DIR *dirp; + const char **str; + + /* Check valid adapter */ + snprintf(path, sizeof(path), "/dev/dvb/adapter%d", adapter_num); + dirp = opendir(path); + if (!dirp) + return; + closedir(dirp); + + /* Check each frontend */ + // Note: this algo will fail if there are really exotic variations + // of tuners and demuxers etc... but you can always manually + // override the config + for (i = 0; i < 32; i++) { + + /* Open/Query frontend */ + snprintf(fepath, sizeof(fepath), "%s/frontend%d", path, i); + fd = tvh_open(fepath, O_RDONLY, 0); + if (fd == -1) { + if (errno != ENOENT) + tvhlog(LOG_ALERT, "dvb", + "%s: unable to open (err=%s)", fepath, strerror(errno)); + continue; + } + if (ioctl(fd, FE_GET_INFO, &fe_info)) { + tvhlog(LOG_ALERT, "dvb", + "%s: unable to query (err=%s)", fepath, strerror(errno)); + close(fd); + continue; + } + close(fd); + + /* Find Demux */ + snprintf(dmxpath, sizeof(dmxpath), "%s/demux%d", path, i); + fd = tvh_open(dmxpath, O_RDONLY, 0); + if (fd == -1) { + snprintf(dmxpath, sizeof(dmxpath), "%s/demux%d", path, 0); + fd = tvh_open(dmxpath, O_RDONLY, 0); + } + if (fd == -1) { + tvhlog(LOG_ALERT, "dvb", "%s: unable to find demux", fepath); + continue; + } + close(fd); + + /* Find DVR */ + snprintf(dvrpath, sizeof(dvrpath), "%s/dvr%d", path, i); + fd = tvh_open(dvrpath, O_RDONLY, 0); + if (fd == -1) { + snprintf(dvrpath, sizeof(dvrpath), "%s/dvr%d", path, 0); + fd = tvh_open(dvrpath, O_RDONLY, 0); + } + if (fd == -1) { + tvhlog(LOG_ALERT, "dvb", "%s: unable to find dvr", fepath); + continue; + } + close(fd); + + /* Create entry */ + tda = tda_alloc(); + tda->tda_adapter_num = adapter_num; + tda->tda_rootpath = strdup(path); + tda->tda_fe_path = strdup(fepath); + tda->tda_demux_path = strdup(dmxpath); + tda->tda_dvr_path = strdup(dvrpath); + tda->tda_fe_fd = -1; + tda->tda_dvr_pipe.rd = -1; + tda->tda_full_mux_rx = -1; + tda->tda_type = fe_info.type; + tda->tda_autodiscovery = tda->tda_type != FE_QPSK; + tda->tda_idlescan = 1; + tda->tda_sat = tda->tda_type == FE_QPSK; + tda->tda_displayname = strdup(fe_info.name); + tda->tda_fe_info = malloc(sizeof(struct dvb_frontend_info)); + memcpy(tda->tda_fe_info, &fe_info, sizeof(struct dvb_frontend_info)); + + /* ID the device (for configuration etc..) */ + // Note: would be better to include frontend dev in name, but that + // would require additional work to migrate config + snprintf(buf, sizeof(buf), "%s_%s", tda->tda_rootpath, + tda->tda_fe_info->name); + for(j = 0; j < strlen(buf); j++) + if(!isalnum((int)buf[j])) + buf[j] = '_'; + tda->tda_identifier = strdup(buf); + + /* Check device connection */ + dvb_adapter_checkspeed(tda); + + /* Adapters known to provide valid SNR */ + str = dvb_adapter_snr_whitelist; + while (*str) { + if (strcasestr(fe_info.name, *str)) { + tda->tda_snr_valid = 1; + break; + } + str++; + } + + /* Store */ + tvhlog(LOG_INFO, "dvb", + "Found adapter %s (%s) via %s%s", path, tda->tda_fe_info->name, + hostconnection2str(tda->tda_hostconnection), + tda->tda_snr_valid ? ", Reports valid SNR values" : ""); + TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link); + } +} + +/** + * + */ +static void +tda_add_from_file(const char *filename) +{ + int i, r; th_dvb_adapter_t *tda; char buf[400]; - snprintf(path, sizeof(path), "/dev/dvb/adapter%d", adapter_num); - snprintf(fname, sizeof(fname), "%s/frontend0", path); - - fe = tvh_open(fname, O_RDWR | O_NONBLOCK, 0); - if(fe == -1) { - if(errno != ENOENT) - tvhlog(LOG_ALERT, "dvb", - "Unable to open %s -- %s", fname, strerror(errno)); - return; - } - tda = tda_alloc(); - tda->tda_adapter_num = adapter_num; - tda->tda_rootpath = strdup(path); - tda->tda_demux_path = malloc(256); - snprintf(tda->tda_demux_path, 256, "%s/demux0", path); - tda->tda_dvr_path = malloc(256); - snprintf(tda->tda_dvr_path, 256, "%s/dvr0", path); - tda->tda_fe_path = strdup(fname); + tda->tda_adapter_num = -1; tda->tda_fe_fd = -1; - tda->tda_dvr_pipe[0] = -1; + tda->tda_dvr_pipe.rd = -1; - tda->tda_fe_info = malloc(sizeof(struct dvb_frontend_info)); + tda->tda_enabled = 1; - if(ioctl(fe, FE_GET_INFO, tda->tda_fe_info)) { - tvhlog(LOG_ALERT, "dvb", "%s: Unable to query adapter", fname); - close(fe); - free(tda); - return; - } - if (tda->tda_idlescan || !tda->tda_idleclose) - tda->tda_fe_fd = fe; - else - close(fe); + tda->tda_type = -1; - tda->tda_type = tda->tda_fe_info->type; - - snprintf(buf, sizeof(buf), "%s_%s", tda->tda_rootpath, - tda->tda_fe_info->name); + snprintf(buf, sizeof(buf), "%s", filename); r = strlen(buf); for(i = 0; i < r; i++) @@ -442,145 +650,190 @@ tda_add(int adapter_num) tda->tda_identifier = strdup(buf); - tda->tda_autodiscovery = tda->tda_type != FE_QPSK; - tda->tda_idlescan = 1; + tda->tda_autodiscovery = 0; + tda->tda_idlescan = 0; - tda->tda_sat = tda->tda_type == FE_QPSK; + tda->tda_sat = 0; + + tda->tda_full_mux_rx = 1; /* Come up with an initial displayname, user can change it and it will be overridden by any stored settings later on */ - tda->tda_displayname = strdup(tda->tda_fe_info->name); - - dvb_adapter_checkspeed(tda); - - tvhlog(LOG_INFO, "dvb", - "Found adapter %s (%s) via %s", path, tda->tda_fe_info->name, - hostconnection2str(tda->tda_hostconnection)); + tda->tda_displayname = strdup(filename); TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link); +} +/** + * Initiliase + */ +static void tda_init (th_dvb_adapter_t *tda) +{ + /* Disabled - ignore */ + if (!tda->tda_enabled || !tda->tda_rootpath) return; - dvb_table_init(tda); - - if(tda->tda_sat) - dvb_satconf_init(tda); + /* Initiliase input mode */ + if(!tda->tda_open_service) { + if(tda->tda_type == -1 || check_full_stream(tda)) { + tvhlog(LOG_INFO, "dvb", "Adapter %s will run in full mux mode", tda->tda_rootpath); + dvb_input_raw_setup(tda); + } else { + tvhlog(LOG_INFO, "dvb", "Adapter %s will run in filtered mode", tda->tda_rootpath); + dvb_input_filtered_setup(tda); + } + } + /* Enable mux scanner */ gtimer_arm(&tda->tda_mux_scanner_timer, dvb_adapter_mux_scanner, tda, 1); } +/** + * + */ void -dvb_adapter_start ( th_dvb_adapter_t *tda ) +dvb_adapter_start ( th_dvb_adapter_t *tda, int opt ) { + if(tda->tda_enabled == 0) { + tvhlog(LOG_INFO, "dvb", "Adapter \"%s\" cannot be started - it's disabled", tda->tda_displayname); + return; + } + + /* Default to ALL */ + if (!opt) + opt = TDA_OPT_ALL; + /* Open front end */ - if (tda->tda_fe_fd == -1) { + if ((opt & TDA_OPT_FE) && (tda->tda_fe_fd == -1)) { tda->tda_fe_fd = tvh_open(tda->tda_fe_path, O_RDWR | O_NONBLOCK, 0); if (tda->tda_fe_fd == -1) return; tvhlog(LOG_DEBUG, "dvb", "%s opened frontend %s", tda->tda_rootpath, tda->tda_fe_path); } /* Start DVR thread */ - if (tda->tda_dvr_pipe[0] == -1) { - assert(pipe(tda->tda_dvr_pipe) != -1); - fcntl(tda->tda_dvr_pipe[0], F_SETFD, fcntl(tda->tda_dvr_pipe[0], F_GETFD) | FD_CLOEXEC); - fcntl(tda->tda_dvr_pipe[0], F_SETFL, fcntl(tda->tda_dvr_pipe[0], F_GETFL) | O_NONBLOCK); - fcntl(tda->tda_dvr_pipe[1], F_SETFD, fcntl(tda->tda_dvr_pipe[1], F_GETFD) | FD_CLOEXEC); + if ((opt & TDA_OPT_DVR) && (tda->tda_dvr_pipe.rd == -1)) { + int err = tvh_pipe(O_NONBLOCK, &tda->tda_dvr_pipe); + assert(err != -1); pthread_create(&tda->tda_dvr_thread, NULL, dvb_adapter_input_dvr, tda); tvhlog(LOG_DEBUG, "dvb", "%s started dvr thread", tda->tda_rootpath); } } void -dvb_adapter_stop ( th_dvb_adapter_t *tda ) +dvb_adapter_stop ( th_dvb_adapter_t *tda, int opt ) { /* Poweroff */ - dvb_adapter_poweroff(tda); + if (opt & TDA_OPT_PWR) + dvb_adapter_poweroff(tda); - /* Don't stop/close */ - if (!tda->tda_idleclose) return; + /* Stop DVR thread */ + if ((opt & TDA_OPT_DVR) && (tda->tda_dvr_pipe.rd != -1)) { + tvhlog(LOG_DEBUG, "dvb", "%s stopping thread", tda->tda_rootpath); + int err = tvh_write(tda->tda_dvr_pipe.wr, "", 1); + assert(!err); + pthread_join(tda->tda_dvr_thread, NULL); + close(tda->tda_dvr_pipe.rd); + close(tda->tda_dvr_pipe.wr); + tda->tda_dvr_pipe.rd = -1; + tvhlog(LOG_DEBUG, "dvb", "%s stopped thread", tda->tda_rootpath); + } + + dvb_adapter_notify(tda); + + /* Don't close FE */ + if (!tda->tda_idleclose && tda->tda_enabled) return; /* Close front end */ - if (tda->tda_fe_fd != -1) { + if ((opt & TDA_OPT_FE) && (tda->tda_fe_fd != -1)) { tvhlog(LOG_DEBUG, "dvb", "%s closing frontend", tda->tda_rootpath); close(tda->tda_fe_fd); tda->tda_fe_fd = -1; } - - /* Stop DVR thread */ - if (tda->tda_dvr_pipe[0] != -1) { - tvhlog(LOG_DEBUG, "dvb", "%s stopping thread", tda->tda_rootpath); - assert(write(tda->tda_dvr_pipe[1], "", 1) == 1); - pthread_join(tda->tda_dvr_thread, NULL); - close(tda->tda_dvr_pipe[0]); - close(tda->tda_dvr_pipe[1]); - tda->tda_dvr_pipe[0] = -1; - tvhlog(LOG_DEBUG, "dvb", "%s stopped thread", tda->tda_rootpath); - } } /** * */ void -dvb_adapter_init(uint32_t adapter_mask) +dvb_adapter_init(uint32_t adapter_mask, const char *rawfile) { htsmsg_t *l, *c; htsmsg_field_t *f; - const char *name, *s; + const char *s; int i, type; + uint32_t u32; th_dvb_adapter_t *tda; TAILQ_INIT(&dvb_adapters); + /* Initialise hardware */ for(i = 0; i < 32; i++) if ((1 << i) & adapter_mask) tda_add(i); - l = hts_settings_load("dvbadapters"); - if(l != NULL) { + /* Initialise rawts test file */ + if(rawfile) + tda_add_from_file(rawfile); + + /* Load configuration */ + if ((l = hts_settings_load("dvbadapters")) != NULL) { HTSMSG_FOREACH(f, l) { if((c = htsmsg_get_map_by_field(f)) == NULL) - continue; + continue; - name = htsmsg_get_str(c, "displayname"); - if((s = htsmsg_get_str(c, "type")) == NULL || - (type = dvb_str_to_adaptertype(s)) < 0) - continue; + (type = dvb_str_to_adaptertype(s)) < 0) + continue; + // TODO: do we really want to do this? useful for debug? if((tda = dvb_adapter_find_by_identifier(f->hmf_name)) == NULL) { - /* Not discovered by hardware, create it */ - - tda = tda_alloc(); - tda->tda_identifier = strdup(f->hmf_name); - tda->tda_type = type; - TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link); + /* Not discovered by hardware, create it */ + tda = tda_alloc(); + tda->tda_identifier = strdup(f->hmf_name); + tda->tda_type = type; + TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link); } else { - if(type != tda->tda_type) - continue; /* Something is wrong, ignore */ + if(type != tda->tda_type) + continue; /* Something is wrong, ignore */ } - free(tda->tda_displayname); - tda->tda_displayname = strdup(name); + tvh_str_update(&tda->tda_displayname, htsmsg_get_str(c, "displayname")); + tvh_str_update(&tda->tda_dvr_path, htsmsg_get_str(c, "dvr_path")); + tvh_str_update(&tda->tda_demux_path, htsmsg_get_str(c, "dmx_path")); - htsmsg_get_u32(c, "autodiscovery", &tda->tda_autodiscovery); - htsmsg_get_u32(c, "idlescan", &tda->tda_idlescan); - htsmsg_get_u32(c, "idleclose", &tda->tda_idleclose); - htsmsg_get_u32(c, "skip_checksubscr", &tda->tda_skip_checksubscr); - htsmsg_get_u32(c, "qmon", &tda->tda_qmon); - htsmsg_get_u32(c, "dump_muxes", &tda->tda_dump_muxes); - htsmsg_get_u32(c, "poweroff", &tda->tda_poweroff); - htsmsg_get_u32(c, "nitoid", &tda->tda_nitoid); - htsmsg_get_u32(c, "diseqc_version", &tda->tda_diseqc_version); - htsmsg_get_u32(c, "extrapriority", &tda->tda_extrapriority); - htsmsg_get_u32(c, "skip_initialscan", &tda->tda_skip_initialscan); + if (htsmsg_get_u32(c, "enabled", &tda->tda_enabled)) + tda->tda_enabled = 1; + htsmsg_get_u32(c, "autodiscovery", &tda->tda_autodiscovery); + htsmsg_get_u32(c, "idlescan", &tda->tda_idlescan); + htsmsg_get_u32(c, "idleclose", &tda->tda_idleclose); + htsmsg_get_u32(c, "skip_checksubscr", &tda->tda_skip_checksubscr); + htsmsg_get_u32(c, "sidtochan", &tda->tda_sidtochan); + htsmsg_get_u32(c, "qmon", &tda->tda_qmon); + htsmsg_get_u32(c, "poweroff", &tda->tda_poweroff); + htsmsg_get_u32(c, "nitoid", &tda->tda_nitoid); + htsmsg_get_u32(c, "diseqc_version", &tda->tda_diseqc_version); + htsmsg_get_u32(c, "diseqc_repeats", &tda->tda_diseqc_repeats); + htsmsg_get_u32(c, "extrapriority", &tda->tda_extrapriority); + htsmsg_get_u32(c, "skip_initialscan", &tda->tda_skip_initialscan); htsmsg_get_u32(c, "disable_pmt_monitor", &tda->tda_disable_pmt_monitor); + if (htsmsg_get_u32(c, "grace_period", &tda->tda_grace_period)) + tda->tda_grace_period = 10; + if (htsmsg_get_s32(c, "full_mux_rx", &tda->tda_full_mux_rx)) + if (!htsmsg_get_u32(c, "disable_full_mux_rx", &u32) && u32) + tda->tda_full_mux_rx = 0; } htsmsg_destroy(l); } - TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) + /* Initialise devices */ + TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) { + tda_init(tda); + + if(tda->tda_sat) + dvb_satconf_init(tda); + dvb_mux_load(tda); + } } @@ -598,6 +851,9 @@ dvb_adapter_mux_scanner(void *aux) if(tda->tda_rootpath == NULL) return; // No hardware + if(!tda->tda_enabled) + return; // disabled + // default period gtimer_arm(&tda->tda_mux_scanner_timer, dvb_adapter_mux_scanner, tda, 20); @@ -611,6 +867,10 @@ dvb_adapter_mux_scanner(void *aux) if(service_compute_weight(&tda->tda_transports) > 0) return; + if(tda->tda_mux_current != NULL && + LIST_FIRST(&tda->tda_mux_current->tdmi_subscriptions) != NULL) + return; // Someone is doing full mux dump + /* Check if we have muxes pending for quickscan, if so, choose them */ if((tdmi = TAILQ_FIRST(&tda->tda_initial_scan_queue)) != NULL) { dvb_fe_tune(tdmi, "Initial autoscan"); @@ -719,8 +979,39 @@ dvb_adapter_clean(th_dvb_adapter_t *tda) service_remove_subscriber(t, NULL, SM_CODE_SUBSCRIPTION_OVERRIDDEN); } +/** + * Install RAW PES filter + */ +static int +dvb_adapter_raw_filter(th_dvb_adapter_t *tda) +{ + int dmx = -1; + struct dmx_pes_filter_params dmx_param; + dmx = tvh_open(tda->tda_demux_path, O_RDWR, 0); + if(dmx == -1) { + tvhlog(LOG_ALERT, "dvb", "Unable to open %s -- %s", + tda->tda_demux_path, strerror(errno)); + return -1; + } + memset(&dmx_param, 0, sizeof(dmx_param)); + dmx_param.pid = 0x2000; + dmx_param.input = DMX_IN_FRONTEND; + dmx_param.output = DMX_OUT_TS_TAP; + dmx_param.pes_type = DMX_PES_OTHER; + dmx_param.flags = DMX_IMMEDIATE_START; + + if(ioctl(dmx, DMX_SET_PES_FILTER, &dmx_param) == -1) { + tvhlog(LOG_ERR, "dvb", + "Unable to configure demuxer \"%s\" for all PIDs -- %s", + tda->tda_demux_path, strerror(errno)); + close(dmx); + return -1; + } + + return dmx; +} /** * @@ -729,56 +1020,81 @@ static void * dvb_adapter_input_dvr(void *aux) { th_dvb_adapter_t *tda = aux; - int fd, i, r, c, efd, nfds; + int fd = -1, i, r, c, efd, nfds, dmx = -1; uint8_t tsb[188 * 10]; service_t *t; struct epoll_event ev; + int delay = 10; - fd = tvh_open(tda->tda_dvr_path, O_RDONLY | O_NONBLOCK, 0); - if(fd == -1) { - tvhlog(LOG_ALERT, "dvb", "%s: unable to open dvr", tda->tda_dvr_path); + /* Install RAW demux */ + if (tda->tda_rawmode) { + if ((dmx = dvb_adapter_raw_filter(tda)) == -1) { + tvhlog(LOG_ALERT, "dvb", "Unable to install raw mux filter"); + return NULL; + } + } + + /* Open DVR */ + if ((fd = tvh_open(tda->tda_dvr_path, O_RDONLY | O_NONBLOCK, 0)) == -1) { + close(dmx); return NULL; } /* Create poll */ efd = epoll_create(2); + memset(&ev, 0, sizeof(ev)); ev.events = EPOLLIN; + ev.data.fd = tda->tda_dvr_pipe.rd; + epoll_ctl(efd, EPOLL_CTL_ADD, tda->tda_dvr_pipe.rd, &ev); ev.data.fd = fd; epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev); - ev.data.fd = tda->tda_dvr_pipe[0]; - epoll_ctl(efd, EPOLL_CTL_ADD, tda->tda_dvr_pipe[0], &ev); r = i = 0; while(1) { /* Wait for input */ - nfds = epoll_wait(efd, &ev, 1, -1); + nfds = epoll_wait(efd, &ev, 1, delay); + + /* No data */ if (nfds < 1) continue; + + /* Exit */ if (ev.data.fd != fd) break; + /* Read data */ c = read(fd, tsb+r, sizeof(tsb)-r); if (c < 0) { if (errno == EAGAIN || errno == EINTR) continue; - else + else if (errno == EOVERFLOW) { + tvhlog(LOG_WARNING, "dvb", "\"%s\" read() EOVERFLOW", + tda->tda_identifier); + continue; + } else { + // TODO: should we try to recover? + tvhlog(LOG_ERR, "dvb", "\"%s\" read() error %d", + tda->tda_identifier, errno); break; + } } r += c; + atomic_add(&tda->tda_bytes, c); /* not enough data */ if (r < 188) continue; + int wakeup_table_feed = 0; // Just wanna wakeup once + pthread_mutex_lock(&tda->tda_delivery_mutex); - /* debug */ - if(tda->tda_dump_fd != -1) { - if(write(tda->tda_dump_fd, tsb, r) != r) { - tvhlog(LOG_ERR, "dvb", - "\"%s\" unable to write to mux dump file -- %s", - tda->tda_identifier, strerror(errno)); - close(tda->tda_dump_fd); - tda->tda_dump_fd = -1; - } + if(LIST_FIRST(&tda->tda_streaming_pad.sp_targets) != NULL) { + streaming_message_t sm; + pktbuf_t *pb = pktbuf_alloc(tsb, r); + memset(&sm, 0, sizeof(sm)); + sm.sm_type = SMT_MPEGTS; + sm.sm_data = pb; + streaming_pad_deliver(&tda->tda_streaming_pad, &sm); + pktbuf_ref_dec(pb); } /* Process */ @@ -786,9 +1102,21 @@ dvb_adapter_input_dvr(void *aux) /* sync */ if (tsb[i] == 0x47) { - LIST_FOREACH(t, &tda->tda_transports, s_active_link) - if(t->s_dvb_mux_instance == tda->tda_mux_current) - ts_recv_packet1(t, tsb + i, NULL); + int pid = (tsb[i+1] & 0x1f) << 8 | tsb[i+2]; + + if(tda->tda_table_filter[pid]) { + if(!(tsb[i+1] & 0x80)) { // Only dispatch to table parser if not error + dvb_table_feed_t *dtf = malloc(sizeof(dvb_table_feed_t)); + memcpy(dtf->dtf_tsb, tsb + i, 188); + TAILQ_INSERT_TAIL(&tda->tda_table_feed, dtf, dtf_link); + wakeup_table_feed = 1; + } + } else { + LIST_FOREACH(t, &tda->tda_transports, s_active_link) + if(t->s_dvb_mux_instance == tda->tda_mux_current) + ts_recv_packet1(t, tsb + i, NULL); + } + i += 188; r -= 188; @@ -800,6 +1128,9 @@ dvb_adapter_input_dvr(void *aux) } } + if(wakeup_table_feed) + pthread_cond_signal(&tda->tda_table_feed_cond); + pthread_mutex_unlock(&tda->tda_delivery_mutex); /* reset buffer */ @@ -807,6 +1138,8 @@ dvb_adapter_input_dvr(void *aux) i = 0; } + if(dmx != -1) + close(dmx); close(efd); close(fd); return NULL; @@ -845,8 +1178,23 @@ dvb_adapter_build_msg(th_dvb_adapter_t *tda) htsmsg_add_u32(m, "initialMuxes", tda->tda_initial_num_mux); if(tda->tda_mux_current != NULL) { + th_dvb_mux_instance_t *tdmi = tda->tda_mux_current; + dvb_mux_nicename(buf, sizeof(buf), tda->tda_mux_current); htsmsg_add_str(m, "currentMux", buf); + + htsmsg_add_u32(m, "signal", MIN(tdmi->tdmi_signal * 100 / 65535, 100)); + htsmsg_add_u32(m, "snr", tdmi->tdmi_snr); + htsmsg_add_u32(m, "ber", tdmi->tdmi_ber); + htsmsg_add_u32(m, "unc", tdmi->tdmi_unc); + htsmsg_add_u32(m, "uncavg", tdmi->tdmi_unc_avg); + } else { + htsmsg_add_str(m, "currentMux", ""); + htsmsg_add_u32(m, "signal", 0); + htsmsg_add_u32(m, "snr", 0); + htsmsg_add_u32(m, "ber", 0); + htsmsg_add_u32(m, "unc", 0); + htsmsg_add_u32(m, "uncavg", 0); } if(tda->tda_rootpath == NULL) @@ -924,16 +1272,17 @@ dvb_fe_opts(th_dvb_adapter_t *tda, const char *which) return a; } -#if DVB_API_VERSION >= 5 if(!strcmp(which, "delsys")) { if(c & FE_CAN_QPSK) { +#if DVB_API_VERSION >= 5 fe_opts_add(a, "SYS_DVBS", SYS_DVBS); fe_opts_add(a, "SYS_DVBS2", SYS_DVBS2); - } else - fe_opts_add(a, "SYS_UNDEFINED", SYS_UNDEFINED); +#else + fe_opts_add(a, "SYS_DVBS", -1); +#endif + } return a; } -#endif if(!strcmp(which, "transmissionmodes")) { if(c & FE_CAN_TRANSMISSION_MODE_AUTO) diff --git a/src/dvb/dvb_fe.c b/src/dvb/dvb_fe.c index 97536abe..99c431d9 100644 --- a/src/dvb/dvb_fe.c +++ b/src/dvb/dvb_fe.c @@ -41,6 +41,7 @@ #include "dvr/dvr.h" #include "service.h" #include "streaming.h" +#include "atomic.h" #include "epggrab.h" @@ -90,14 +91,15 @@ dvb_fe_monitor(void *aux) { th_dvb_adapter_t *tda = aux; fe_status_t fe_status; - int status, v, update = 0, vv, i, fec, q; + int status, v, vv, i, fec, q, bw; th_dvb_mux_instance_t *tdmi = tda->tda_mux_current; char buf[50]; signal_status_t sigstat; streaming_message_t sm; struct service *t; - gtimer_arm(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 1); + int store = 0; + int notify = 0; if(tdmi == NULL) return; @@ -106,9 +108,8 @@ dvb_fe_monitor(void *aux) * Read out front end status */ if(ioctl(tda->tda_fe_fd, FE_READ_STATUS, &fe_status)) - fe_status = 0; - - if(fe_status & FE_HAS_LOCK) + status = TDMI_FE_UNKNOWN; + else if(fe_status & FE_HAS_LOCK) status = -1; else if(fe_status & (FE_HAS_SYNC | FE_HAS_VITERBI | FE_HAS_CARRIER)) status = TDMI_FE_BAD_SIGNAL; @@ -117,23 +118,58 @@ dvb_fe_monitor(void *aux) else status = TDMI_FE_NO_SIGNAL; - if(tda->tda_fe_monitor_hold > 0) { - /* Post tuning threshold */ - if(status == -1) { /* We have a lock, don't hold off */ - tda->tda_fe_monitor_hold = 0; - /* Reset FEC counter */ - dvb_fe_get_unc(tda); + /** + * Waiting for initial lock + */ + if(tda->tda_locked == 0) { + + /* Read */ + if (status == -1) { + tda->tda_locked = 1; + dvb_adapter_start(tda, TDA_OPT_ALL); + gtimer_arm(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 1); + + /* Install table handlers */ + dvb_table_add_default(tdmi); + epggrab_mux_start(tdmi); + + /* Service filters */ + pthread_mutex_lock(&tda->tda_delivery_mutex); + LIST_FOREACH(t, &tda->tda_transports, s_active_link) { + if (t->s_dvb_mux_instance == tdmi) { + tda->tda_open_service(tda, t); + dvb_table_add_pmt(tdmi, t->s_pmt_pid); + } + } + pthread_mutex_unlock(&tda->tda_delivery_mutex); + + /* Re-arm (50ms) */ } else { - tda->tda_fe_monitor_hold--; - return; + gtimer_arm_ms(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 50); + + /* Monitor (1 per sec) */ + if (dispatch_clock < tda->tda_monitor) + return; + tda->tda_monitor = dispatch_clock + 1; } + + } else { + gtimer_arm(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 1); } + /* + * Update stats + */ if(status == -1) { /* Read FEC counter (delta) */ fec = dvb_fe_get_unc(tda); - + + if(tdmi->tdmi_unc != fec) { + tdmi->tdmi_unc = fec; + notify = 1; + } + tdmi->tdmi_fec_err_histogram[tdmi->tdmi_fec_err_ptr++] = fec; if(tdmi->tdmi_fec_err_ptr == TDMI_FEC_ERR_HISTOGRAM_SIZE) tdmi->tdmi_fec_err_ptr = 0; @@ -144,7 +180,13 @@ dvb_fe_monitor(void *aux) v++; vv += tdmi->tdmi_fec_err_histogram[i]; } - vv = vv / TDMI_FEC_ERR_HISTOGRAM_SIZE; + + float avg = (float)vv / TDMI_FEC_ERR_HISTOGRAM_SIZE; + + if(tdmi->tdmi_unc_avg != avg) { + tdmi->tdmi_unc_avg = avg; + notify = 1; + } if(v == 0) { status = TDMI_FE_OK; @@ -154,27 +196,40 @@ dvb_fe_monitor(void *aux) status = TDMI_FE_CONSTANT_FEC; } + int v; /* bit error rate */ - if(ioctl(tda->tda_fe_fd, FE_READ_BER, &tdmi->tdmi_ber) == -1) - tdmi->tdmi_ber = -2; + if(ioctl(tda->tda_fe_fd, FE_READ_BER, &v) != -1 && v != tdmi->tdmi_ber) { + tdmi->tdmi_ber = v; + notify = 1; + } /* signal strength */ - if(ioctl(tda->tda_fe_fd, FE_READ_SIGNAL_STRENGTH, &tdmi->tdmi_signal) == -1) - tdmi->tdmi_signal = -2; + if(ioctl(tda->tda_fe_fd, FE_READ_SIGNAL_STRENGTH, &v) != -1 && v != tdmi->tdmi_signal) { + tdmi->tdmi_signal = v; + notify = 1; + } /* signal/noise ratio */ - if(ioctl(tda->tda_fe_fd, FE_READ_SNR, &tdmi->tdmi_snr) == -1) - tdmi->tdmi_snr = -2; + if(tda->tda_snr_valid) { + if(ioctl(tda->tda_fe_fd, FE_READ_SNR, &v) != -1) { + float snr = v / 10.0; + if(tdmi->tdmi_snr != snr) { + tdmi->tdmi_snr = snr; + notify = 1; + } + } + } } if(status != tdmi->tdmi_fe_status) { tdmi->tdmi_fe_status = status; dvb_mux_nicename(buf, sizeof(buf), tdmi); - tvhlog(LOG_DEBUG, + tvhlog(LOG_DEBUG, "dvb", "\"%s\" on adapter \"%s\", status changed to %s", buf, tda->tda_displayname, dvb_mux_status(tdmi)); - update = 1; + store = 1; + notify = 1; } if(status != TDMI_FE_UNKNOWN) { @@ -186,26 +241,46 @@ dvb_fe_monitor(void *aux) } if(q != tdmi->tdmi_quality) { tdmi->tdmi_quality = q; - update = 1; + store = 1; + notify = 1; } - } + } - if(update) { + bw = atomic_exchange(&tda->tda_bytes, 0); + + if(notify) { htsmsg_t *m = htsmsg_create_map(); - htsmsg_add_str(m, "id", tdmi->tdmi_identifier); htsmsg_add_u32(m, "quality", tdmi->tdmi_quality); + htsmsg_add_u32(m, "signal", tdmi->tdmi_signal); + + if(tda->tda_snr_valid) + htsmsg_add_dbl(m, "snr", tdmi->tdmi_snr); + htsmsg_add_u32(m, "ber", tdmi->tdmi_ber); + htsmsg_add_u32(m, "unc", tdmi->tdmi_unc); notify_by_msg("dvbMux", m); - dvb_mux_save(tdmi); + m = htsmsg_create_map(); + htsmsg_add_str(m, "identifier", tda->tda_identifier); + htsmsg_add_u32(m, "signal", MIN(tdmi->tdmi_signal * 100 / 65535, 100)); + if(tda->tda_snr_valid) + htsmsg_add_dbl(m, "snr", tdmi->tdmi_snr); + htsmsg_add_u32(m, "ber", tdmi->tdmi_ber); + htsmsg_add_u32(m, "unc", tdmi->tdmi_unc); + htsmsg_add_dbl(m, "uncavg", tdmi->tdmi_unc_avg); + htsmsg_add_u32(m, "bw", bw); + notify_by_msg("tvAdapter", m); } + if(store) + dvb_mux_save(tdmi); + /* Streaming message */ sigstat.status_text = dvb_mux_status(tdmi); sigstat.snr = tdmi->tdmi_snr; sigstat.signal = tdmi->tdmi_signal; sigstat.ber = tdmi->tdmi_ber; - sigstat.unc = tdmi->tdmi_uncorrected_blocks; + sigstat.unc = tdmi->tdmi_unc; sm.sm_type = SMT_SIGNAL_STATUS; sm.sm_data = &sigstat; LIST_FOREACH(t, &tda->tda_transports, s_active_link) @@ -225,27 +300,26 @@ void dvb_fe_stop(th_dvb_mux_instance_t *tdmi, int retune) { th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + dvb_table_feed_t *dtf; + + lock_assert(&global_lock); assert(tdmi == tda->tda_mux_current); tda->tda_mux_current = NULL; - if(tda->tda_allpids_dmx_fd != -1) { - close(tda->tda_allpids_dmx_fd); - tda->tda_allpids_dmx_fd = -1; - } - - if(tda->tda_dump_fd != -1) { - close(tda->tda_dump_fd); - tda->tda_dump_fd = -1; - } - if(tdmi->tdmi_table_initial) { tdmi->tdmi_table_initial = 0; tda->tda_initial_num_mux--; dvb_mux_save(tdmi); } + dvb_adapter_stop(tda, TDA_OPT_DVR); + pthread_mutex_lock(&tda->tda_delivery_mutex); + while((dtf = TAILQ_FIRST(&tda->tda_table_feed))) + TAILQ_REMOVE(&tda->tda_table_feed, dtf, dtf_link); + pthread_mutex_unlock(&tda->tda_delivery_mutex); dvb_table_flush_all(tdmi); + tda->tda_locked = 0; assert(tdmi->tdmi_scan_queue == NULL); @@ -259,125 +333,12 @@ dvb_fe_stop(th_dvb_mux_instance_t *tdmi, int retune) if (!retune) { gtimer_disarm(&tda->tda_fe_monitor_timer); - dvb_adapter_stop(tda); + dvb_adapter_stop(tda, TDA_OPT_ALL); } } - -/** - * Open a dump file which we write the entire mux output to - */ -static void -dvb_adapter_open_dump_file(th_dvb_adapter_t *tda) -{ - struct dmx_pes_filter_params dmx_param; - char fullname[1000]; - char path[500]; - const char *fname = tda->tda_mux_current->tdmi_identifier; - - int fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); - if(fd == -1) - return; - - memset(&dmx_param, 0, sizeof(dmx_param)); - dmx_param.pid = 0x2000; - dmx_param.input = DMX_IN_FRONTEND; - dmx_param.output = DMX_OUT_TS_TAP; - dmx_param.pes_type = DMX_PES_OTHER; - dmx_param.flags = DMX_IMMEDIATE_START; - - if(ioctl(fd, DMX_SET_PES_FILTER, &dmx_param)) { - tvhlog(LOG_ERR, "dvb", - "\"%s\" unable to configure demuxer \"%s\" for all PIDs -- %s", - fname, tda->tda_demux_path, - strerror(errno)); - close(fd); - return; - } - - snprintf(path, sizeof(path), "%s/muxdumps", - dvr_config_find_by_name_default("")->dvr_storage); - - if(mkdir(path, 0777) && errno != EEXIST) { - tvhlog(LOG_ERR, "dvb", "\"%s\" unable to create mux dump dir %s -- %s", - fname, path, strerror(errno)); - close(fd); - return; - } - - int attempt = 1; - - while(1) { - struct stat st; - snprintf(fullname, sizeof(fullname), "%s/%s.dump%d.ts", - path, fname, attempt); - - if(stat(fullname, &st) == -1) - break; - - attempt++; - } - - int f = open(fullname, O_CREAT | O_TRUNC | O_WRONLY, 0777); - - if(f == -1) { - tvhlog(LOG_ERR, "dvb", "\"%s\" unable to create mux dump file %s -- %s", - fname, fullname, strerror(errno)); - close(fd); - return; - } - - tvhlog(LOG_WARNING, "dvb", "\"%s\" writing to mux dump file %s", - fname, fullname); - - tda->tda_allpids_dmx_fd = fd; - tda->tda_dump_fd = f; -} - - - #if DVB_API_VERSION >= 5 -static int check_frontend (int fe_fd, int dvr, int human_readable) { - (void)dvr; - fe_status_t status; - uint16_t snr, signal; - uint32_t ber; - int timeout = 0; - - do { - if (ioctl(fe_fd, FE_READ_STATUS, &status) == -1) - perror("FE_READ_STATUS failed"); - /* some frontends might not support all these ioctls, thus we - * avoid printing errors - */ - if (ioctl(fe_fd, FE_READ_SIGNAL_STRENGTH, &signal) == -1) - signal = -2; - if (ioctl(fe_fd, FE_READ_SNR, &snr) == -1) - snr = -2; - if (ioctl(fe_fd, FE_READ_BER, &ber) == -1) - ber = -2; - - if (human_readable) { - printf ("status %02x | signal %3u%% | snr %3u%% | ber %d | ", - status, (signal * 100) / 0xffff, (snr * 100) / 0xffff, ber); - } else { - printf ("status %02x | signal %04x | snr %04x | ber %08x | ", - status, signal, snr, ber); - } - if (status & FE_HAS_LOCK) - printf("FE_HAS_LOCK"); - printf("\n"); - - if ((status & FE_HAS_LOCK) || (++timeout >= 10)) - break; - - usleep(1000000); - } while (1); - - return 0; -} - static struct dtv_property clear_p[] = { { .cmd = DTV_CLEAR }, }; @@ -387,7 +348,6 @@ static struct dtv_properties clear_cmdseq = { .props = clear_p }; - /** * */ @@ -430,8 +390,6 @@ dvb_fe_tune_s2(th_dvb_mux_instance_t *tdmi, dvb_mux_conf_t *dmc) /* do tuning now */ r = ioctl(tda->tda_fe_fd, FE_SET_PROPERTY, &_dvbs_cmdseq); - if(0) - check_frontend (tda->tda_fe_fd, 0, 1); return r; } @@ -453,9 +411,11 @@ dvb_fe_tune(th_dvb_mux_instance_t *tdmi, const char *reason) char buf[256]; int r; - lock_assert(&global_lock); + if(tda->tda_enabled == 0) + return SM_CODE_TUNING_FAILED; + if(tda->tda_mux_current == tdmi) return 0; @@ -466,8 +426,8 @@ dvb_fe_tune(th_dvb_mux_instance_t *tdmi, const char *reason) if(tda->tda_mux_current != NULL) dvb_fe_stop(tda->tda_mux_current, 1); - else - dvb_adapter_start(tda); + + dvb_adapter_start(tda, TDA_OPT_FE | TDA_OPT_PWR); if(tda->tda_type == FE_QPSK) { @@ -507,19 +467,16 @@ dvb_fe_tune(th_dvb_mux_instance_t *tdmi, const char *reason) p->frequency = abs(p->frequency - lowfreq); } - if ((r = diseqc_setup(tda->tda_fe_fd, - port, - pol == POLARISATION_HORIZONTAL || - pol == POLARISATION_CIRCULAR_LEFT, - hiband, tda->tda_diseqc_version)) != 0) + if ((r = diseqc_setup(tda->tda_fe_fd, port, + pol == POLARISATION_HORIZONTAL || + pol == POLARISATION_CIRCULAR_LEFT, + hiband, tda->tda_diseqc_version, + tda->tda_diseqc_repeats)) != 0) tvhlog(LOG_ERR, "dvb", "diseqc setup failed %d\n", r); - } + } dvb_mux_nicename(buf, sizeof(buf), tdmi); - tda->tda_fe_monitor_hold = 4; - - #if DVB_API_VERSION >= 5 if (tda->tda_type == FE_QPSK) { tvhlog(LOG_DEBUG, "dvb", "\"%s\" tuning via s2api to \"%s\" (%d, %d Baud, " @@ -547,20 +504,18 @@ dvb_fe_tune(th_dvb_mux_instance_t *tdmi, const char *reason) } /* Mark as bad */ - dvb_mux_set_enable(tdmi, 0); + if (errno == EINVAL) + dvb_mux_set_enable(tdmi, 0); + + dvb_adapter_stop(tda, TDA_OPT_ALL); return SM_CODE_TUNING_FAILED; - } + } tda->tda_mux_current = tdmi; - if(tda->tda_dump_muxes) - dvb_adapter_open_dump_file(tda); - - gtimer_arm(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 1); - - - dvb_table_add_default(tdmi); - epggrab_mux_start(tdmi); + time(&tda->tda_monitor); + tda->tda_monitor += 4; // wait a few secs before monitoring (unlocked) + gtimer_arm_ms(&tda->tda_fe_monitor_timer, dvb_fe_monitor, tda, 50); dvb_adapter_notify(tda); return 0; diff --git a/src/dvb/dvb_input_filtered.c b/src/dvb/dvb_input_filtered.c new file mode 100644 index 00000000..a4dddf26 --- /dev/null +++ b/src/dvb/dvb_input_filtered.c @@ -0,0 +1,259 @@ +/* + * TV Input - Linux DVB interface + * Copyright (C) 2012 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 . + */ + +/** + * DVB input using hardware filters + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tvheadend.h" +#include "dvb.h" +#include "service.h" + +/** + * Install filters for a service + * + * global_lock must be held + */ +static void +open_service(th_dvb_adapter_t *tda, service_t *s) +{ + struct dmx_pes_filter_params dmx_param; + int fd; + elementary_stream_t *st; + + TAILQ_FOREACH(st, &s->s_components, es_link) { + if(st->es_pid >= 0x2000) + continue; + + if(st->es_demuxer_fd != -1) + continue; + + fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); + st->es_cc_valid = 0; + + if(fd == -1) { + st->es_demuxer_fd = -1; + tvhlog(LOG_ERR, "dvb", + "\"%s\" unable to open demuxer \"%s\" for pid %d -- %s", + s->s_identifier, tda->tda_demux_path, + st->es_pid, strerror(errno)); + continue; + } + + memset(&dmx_param, 0, sizeof(dmx_param)); + dmx_param.pid = st->es_pid; + dmx_param.input = DMX_IN_FRONTEND; + dmx_param.output = DMX_OUT_TS_TAP; + dmx_param.pes_type = DMX_PES_OTHER; + dmx_param.flags = DMX_IMMEDIATE_START; + + if(ioctl(fd, DMX_SET_PES_FILTER, &dmx_param)) { + tvhlog(LOG_ERR, "dvb", + "\"%s\" unable to configure demuxer \"%s\" for pid %d -- %s", + s->s_identifier, tda->tda_demux_path, + st->es_pid, strerror(errno)); + close(fd); + fd = -1; + } + + st->es_demuxer_fd = fd; + } +} + + +/** + * Remove filters for a service + * + * global_lock must be held + */ +static void +close_service(th_dvb_adapter_t *tda, service_t *s) +{ + elementary_stream_t *es; + + TAILQ_FOREACH(es, &s->s_components, es_link) { + if(es->es_demuxer_fd != -1) { + close(es->es_demuxer_fd); + es->es_demuxer_fd = -1; + } + } +} + + + + +/** + * + */ +static void +open_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +{ + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + struct epoll_event e; + static int tdt_id_tally; + + tdt->tdt_fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); + + if(tdt->tdt_fd != -1) { + + tdt->tdt_id = ++tdt_id_tally; + + e.events = EPOLLIN; + e.data.u64 = ((uint64_t)tdt->tdt_fd << 32) | tdt->tdt_id; + + if(epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_ADD, tdt->tdt_fd, &e)) { + close(tdt->tdt_fd); + tdt->tdt_fd = -1; + } else { + + struct dmx_sct_filter_params fp = {0}; + + fp.filter.filter[0] = tdt->tdt_table; + fp.filter.mask[0] = tdt->tdt_mask; + + if(tdt->tdt_flags & TDT_CRC) + fp.flags |= DMX_CHECK_CRC; + fp.flags |= DMX_IMMEDIATE_START; + fp.pid = tdt->tdt_pid; + + if(ioctl(tdt->tdt_fd, DMX_SET_FILTER, &fp)) { + close(tdt->tdt_fd); + tdt->tdt_fd = -1; + } + } + } + + if(tdt->tdt_fd == -1) + TAILQ_INSERT_TAIL(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); +} + + +/** + * Close FD for the given table and put table on the pending list + */ +static void +tdt_close_fd(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +{ + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + + assert(tdt->tdt_fd != -1); + + epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_DEL, tdt->tdt_fd, NULL); + close(tdt->tdt_fd); + + tdt->tdt_fd = -1; + TAILQ_INSERT_TAIL(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); +} + + + +/** + * + */ +static void * +dvb_table_input(void *aux) +{ + th_dvb_adapter_t *tda = aux; + int r, i, tid, fd, x; + struct epoll_event ev[1]; + uint8_t sec[4096]; + th_dvb_mux_instance_t *tdmi; + th_dvb_table_t *tdt; + int64_t cycle_barrier = 0; + + while(1) { + x = epoll_wait(tda->tda_table_epollfd, ev, sizeof(ev) / sizeof(ev[0]), -1); + + for(i = 0; i < x; i++) { + + tid = ev[i].data.u64 & 0xffffffff; + fd = ev[i].data.u64 >> 32; + + if(!(ev[i].events & EPOLLIN)) + continue; + + if((r = read(fd, sec, sizeof(sec))) < 3) + continue; + + pthread_mutex_lock(&global_lock); + if((tdmi = tda->tda_mux_current) != NULL) { + LIST_FOREACH(tdt, &tdmi->tdmi_tables, tdt_link) + if(tdt->tdt_id == tid) + break; + + if(tdt != NULL) { + dvb_table_dispatch(sec, r, tdt); + + /* Any tables pending (that wants a filter/fd), close this one */ + if(TAILQ_FIRST(&tdmi->tdmi_table_queue) != NULL && + cycle_barrier < getmonoclock()) { + tdt_close_fd(tdmi, tdt); + cycle_barrier = getmonoclock() + 100000; + tdt = TAILQ_FIRST(&tdmi->tdmi_table_queue); + assert(tdt != NULL); + TAILQ_REMOVE(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); + + open_table(tdmi, tdt); + } + } + } + pthread_mutex_unlock(&global_lock); + } + } + return NULL; +} + + +static void +close_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +{ + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + + if(tdt->tdt_fd == -1) { + TAILQ_REMOVE(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); + } else { + epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_DEL, tdt->tdt_fd, NULL); + close(tdt->tdt_fd); + } +} + +/** + * + */ +void +dvb_input_filtered_setup(th_dvb_adapter_t *tda) +{ + tda->tda_open_service = open_service; + tda->tda_close_service = close_service; + tda->tda_open_table = open_table; + tda->tda_close_table = close_table; + + pthread_t ptid; + tda->tda_table_epollfd = epoll_create(50); + pthread_create(&ptid, NULL, dvb_table_input, tda); +} + diff --git a/src/dvb/dvb_input_raw.c b/src/dvb/dvb_input_raw.c new file mode 100644 index 00000000..0c3f2a56 --- /dev/null +++ b/src/dvb/dvb_input_raw.c @@ -0,0 +1,176 @@ +/* + * TV Input - Linux DVB interface + * Copyright (C) 2012 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 . + */ + +/** + * DVB input from a raw transport stream + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tvheadend.h" +#include "dvb.h" +#include "service.h" + +/** + * Install filters for a service + * + * global_lock must be held + */ +static void +open_service(th_dvb_adapter_t *tda, service_t *s) +{ + // NOP -- We receive all PIDs anyway +} + + +/** + * Remove filters for a service + * + * global_lock must be held + */ +static void +close_service(th_dvb_adapter_t *tda, service_t *s) +{ + // NOP -- open_service() is a NOP +} + + +/** + * + */ +static void +open_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +{ + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + assert(tdt->tdt_pid < 0x2000); + tda->tda_table_filter[tdt->tdt_pid] = 1; +} + + +/** + * + */ +static void +close_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +{ + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + assert(tdt->tdt_pid < 0x2000); + tda->tda_table_filter[tdt->tdt_pid] = 0; +} + + +/** + * + */ +static void +got_section(const uint8_t *data, size_t len, void *opaque) +{ + th_dvb_table_t *tdt = opaque; + dvb_table_dispatch((uint8_t *)data, len, tdt); +} + + + +/** + * All the tables can be destroyed from any of the callbacks + * so we need to be a bit careful here + */ +static void +dvb_table_raw_dispatch(th_dvb_mux_instance_t *tdmi, + const dvb_table_feed_t *dtf) +{ + int pid = (dtf->dtf_tsb[1] & 0x1f) << 8 | dtf->dtf_tsb[2]; + th_dvb_table_t *vec[tdmi->tdmi_num_tables], *tdt; + int i = 0; + LIST_FOREACH(tdt, &tdmi->tdmi_tables, tdt_link) { + vec[i++] = tdt; + tdt->tdt_refcount++; + } + assert(i == tdmi->tdmi_num_tables); + int len = tdmi->tdmi_num_tables; // can change during callbacks + for(i = 0; i < len; i++) { + tdt = vec[i]; + if(!tdt->tdt_destroyed) { + if(tdt->tdt_pid == pid) + psi_section_reassemble(&tdt->tdt_sect, dtf->dtf_tsb, + 0, got_section, tdt); + } + dvb_table_release(tdt); + } +} + +/** + * + */ +static void * +dvb_table_input(void *aux) +{ + th_dvb_adapter_t *tda = aux; + th_dvb_mux_instance_t *tdmi; + dvb_table_feed_t *dtf; + + while(1) { + + pthread_mutex_lock(&tda->tda_delivery_mutex); + + while((dtf = TAILQ_FIRST(&tda->tda_table_feed)) == NULL) + pthread_cond_wait(&tda->tda_table_feed_cond, &tda->tda_delivery_mutex); + TAILQ_REMOVE(&tda->tda_table_feed, dtf, dtf_link); + + pthread_mutex_unlock(&tda->tda_delivery_mutex); + + pthread_mutex_lock(&global_lock); + + if((tdmi = tda->tda_mux_current) != NULL) + dvb_table_raw_dispatch(tdmi, dtf); + + pthread_mutex_unlock(&global_lock); + free(dtf); + } + return NULL; +} + + +/** + * + */ +void +dvb_input_raw_setup(th_dvb_adapter_t *tda) +{ + tda->tda_rawmode = 1; + tda->tda_open_service = open_service; + tda->tda_close_service = close_service; + tda->tda_open_table = open_table; + tda->tda_close_table = close_table; + + TAILQ_INIT(&tda->tda_table_feed); + pthread_cond_init(&tda->tda_table_feed_cond, NULL); + + pthread_t ptid; + pthread_create(&ptid, NULL, dvb_table_input, tda); + +} + diff --git a/src/dvb/dvb_multiplex.c b/src/dvb/dvb_multiplex.c index 357a2816..ce0f1909 100644 --- a/src/dvb/dvb_multiplex.c +++ b/src/dvb/dvb_multiplex.c @@ -58,9 +58,6 @@ static struct strtab muxfestatustab[] = { { "OK", TDMI_FE_OK }, }; -static void tdmi_set_enable(th_dvb_mux_instance_t *tdmi, int enabled); - - /** * */ @@ -102,14 +99,17 @@ tdmi_global_cmp(th_dvb_mux_instance_t *a, th_dvb_mux_instance_t *b) */ static int tdmi_compare_key(const struct dvb_mux_conf *a, - const struct dvb_mux_conf *b) + const struct dvb_mux_conf *b, + const dvb_satconf_t *satconf) { int32_t fd = (int32_t)a->dmc_fe_params.frequency - (int32_t)b->dmc_fe_params.frequency; + if (!satconf) + satconf = b->dmc_satconf; fd = labs(fd); return fd < 2000 && a->dmc_polarisation == b->dmc_polarisation && - a->dmc_satconf == b->dmc_satconf; + a->dmc_satconf == satconf; } @@ -155,26 +155,59 @@ th_dvb_mux_instance_t * dvb_mux_create(th_dvb_adapter_t *tda, const struct dvb_mux_conf *dmc, uint16_t onid, uint16_t tsid, const char *network, const char *source, int enabled, int initialscan, const char *identifier, - dvb_satconf_t *satconf) + dvb_satconf_t *satconf, int create, th_dvb_mux_instance_t *src) { th_dvb_mux_instance_t *tdmi, *c; char buf[200]; lock_assert(&global_lock); + if (!satconf) + satconf = dmc->dmc_satconf; + /* HACK - we hash/compare based on 2KHz spacing and compare on +/-500Hz */ LIST_FOREACH(tdmi, &tda->tda_mux_list, tdmi_adapter_hash_link) { - if(tdmi_compare_key(&tdmi->tdmi_conf, dmc)) + if(tdmi_compare_key(&tdmi->tdmi_conf, dmc, satconf)) break; /* Mux already exist */ } if(tdmi != NULL) { + /* Update stuff ... */ int save = 0; char buf2[1024]; buf2[0] = 0; + int master = 0; + if (!src) + master = 1; + else if (src->tdmi_network_id == tdmi->tdmi_network_id) + master = 1; - if(tdmi_compare_conf(tda->tda_type, &tdmi->tdmi_conf, dmc)) { + /* Network ID */ + if(tsid != 0xFFFF && tdmi->tdmi_transport_stream_id != tsid) { + if (tdmi->tdmi_transport_stream_id == 0xFFFF || master) { + tdmi->tdmi_transport_stream_id = tsid; + save = 1; + } + } + if(onid && tdmi->tdmi_network_id != onid) { + if (!tdmi->tdmi_network_id || master) { + tdmi->tdmi_network_id = onid; + save = 1; + } + } + if(network && *network && strcmp(tdmi->tdmi_network ?: "", network)) { + if (!tdmi->tdmi_network || master) { + free(tdmi->tdmi_network); + tdmi->tdmi_network = strdup(network); + save = 1; + } + } + + /* Tuning Info */ + // TODO: same protection here? + if(tdmi->tdmi_adapter->tda_autodiscovery && + tdmi_compare_conf(tda->tda_type, &tdmi->tdmi_conf, dmc)) { #if DVB_API_VERSION >= 5 snprintf(buf2, sizeof(buf2), " ("); if (tdmi->tdmi_conf.dmc_fe_modulation != dmc->dmc_fe_modulation) @@ -196,19 +229,10 @@ dvb_mux_create(th_dvb_adapter_t *tda, const struct dvb_mux_conf *dmc, save = 1; } - if(tsid != 0xFFFF && tdmi->tdmi_transport_stream_id != tsid) { - tdmi->tdmi_transport_stream_id = tsid; - save = 1; - } - if(onid && tdmi->tdmi_network_id != onid) { - tdmi->tdmi_network_id = onid; - save = 1; - } - /* HACK - load old transports and remove old mux config */ if(identifier) { save = 1; - dvb_transport_load(tdmi, identifier); + dvb_service_load(tdmi, identifier); hts_settings_remove("dvbmuxes/%s/%s", tda->tda_identifier, identifier); } @@ -225,6 +249,9 @@ dvb_mux_create(th_dvb_adapter_t *tda, const struct dvb_mux_conf *dmc, return NULL; } + if (!create) + return NULL; + tdmi = calloc(1, sizeof(th_dvb_mux_instance_t)); if(identifier == NULL) { @@ -238,9 +265,8 @@ dvb_mux_create(th_dvb_adapter_t *tda, const struct dvb_mux_conf *dmc, snprintf(buf, sizeof(buf), "%s%d%s%s%s", tda->tda_identifier, dmc->dmc_fe_params.frequency, qpsktxt, - (satconf || dmc->dmc_satconf) ? "_satconf_" : "", - (satconf ? satconf->sc_id : - (dmc->dmc_satconf ? dmc->dmc_satconf->sc_id : ""))); + satconf ? "_satconf_" : "", + satconf ? satconf->sc_id : ""); tdmi->tdmi_identifier = strdup(buf); } else { @@ -288,19 +314,21 @@ dvb_mux_create(th_dvb_adapter_t *tda, const struct dvb_mux_conf *dmc, tvhlog(LOG_NOTICE, "dvb", "New mux \"%s\" created by %s", buf, source); dvb_mux_save(tdmi); - dvb_adapter_notify(tda); } - dvb_transport_load(tdmi, identifier); + dvb_service_load(tdmi, identifier); dvb_mux_notify(tdmi); - if(enabled && initialscan) { - tda->tda_initial_num_mux++; - tdmi->tdmi_table_initial = 1; - mux_link_initial(tda, tdmi); - } else { - dvb_mux_add_to_scan_queue(tdmi); + if(enabled) { + if(initialscan) { + tda->tda_initial_num_mux++; + tdmi->tdmi_table_initial = 1; + mux_link_initial(tda, tdmi); + } else { + dvb_mux_add_to_scan_queue(tdmi); + } } + dvb_adapter_notify(tda); return tdmi; } @@ -326,7 +354,7 @@ dvb_mux_destroy(th_dvb_mux_instance_t *tdmi) service_destroy(t); } - dvb_transport_notify_by_adapter(tda); + dvb_service_notify_by_adapter(tda); if(tda->tda_mux_current == tdmi) dvb_fe_stop(tda->tda_mux_current, 0); @@ -789,7 +817,7 @@ tdmi_create_by_msg(th_dvb_adapter_t *tda, htsmsg_t *m, const char *identifier) tdmi = dvb_mux_create(tda, &dmc, onid, tsid, htsmsg_get_str(m, "network"), NULL, enabled, initscan, - identifier, NULL); + identifier, NULL, 1, NULL); if(tdmi != NULL) { if((s = htsmsg_get_str(m, "status")) != NULL) @@ -836,6 +864,12 @@ dvb_mux_set_networkname(th_dvb_mux_instance_t *tdmi, const char *networkname) { htsmsg_t *m; + if (!networkname || !*networkname) + return; + + if (!strcmp(tdmi->tdmi_network ?: "", networkname)) + return; + free(tdmi->tdmi_network); tdmi->tdmi_network = strdup(networkname); dvb_mux_save(tdmi); @@ -851,12 +885,19 @@ dvb_mux_set_networkname(th_dvb_mux_instance_t *tdmi, const char *networkname) * */ void -dvb_mux_set_tsid(th_dvb_mux_instance_t *tdmi, uint16_t tsid) +dvb_mux_set_tsid(th_dvb_mux_instance_t *tdmi, uint16_t tsid, int force) { htsmsg_t *m; + if (!force) + if (tdmi->tdmi_transport_stream_id != 0xFFFF || tsid == 0xFFFF) + return; + + if (tdmi->tdmi_transport_stream_id == tsid) + return; + tdmi->tdmi_transport_stream_id = tsid; - + dvb_mux_save(tdmi); m = htsmsg_create_map(); @@ -865,17 +906,42 @@ dvb_mux_set_tsid(th_dvb_mux_instance_t *tdmi, uint16_t tsid) notify_by_msg("dvbMux", m); } +/** + * + */ +void +dvb_mux_set_onid(th_dvb_mux_instance_t *tdmi, uint16_t onid, int force) +{ + htsmsg_t *m; + + if (!force) + if (tdmi->tdmi_network_id != 0 || onid == 0) + return; + + if (tdmi->tdmi_network_id == onid) + return; + + tdmi->tdmi_network_id = onid; + + dvb_mux_save(tdmi); + + m = htsmsg_create_map(); + htsmsg_add_str(m, "id", tdmi->tdmi_identifier); + htsmsg_add_u32(m, "onid", tdmi->tdmi_network_id); + notify_by_msg("dvbMux", m); +} + /** * */ -static void +static int tdmi_set_enable(th_dvb_mux_instance_t *tdmi, int enabled) { th_dvb_adapter_t *tda = tdmi->tdmi_adapter; if(tdmi->tdmi_enabled == enabled) - return; + return 0; if(tdmi->tdmi_enabled) { @@ -894,6 +960,7 @@ tdmi_set_enable(th_dvb_mux_instance_t *tdmi, int enabled) mux_link_initial(tda, tdmi); subscription_reschedule(); + return 1; } /** @@ -902,8 +969,8 @@ tdmi_set_enable(th_dvb_mux_instance_t *tdmi, int enabled) void dvb_mux_set_enable(th_dvb_mux_instance_t *tdmi, int enabled) { - tdmi_set_enable(tdmi, enabled); - dvb_mux_save(tdmi); + if (tdmi_set_enable(tdmi, enabled)) + dvb_mux_save(tdmi); } @@ -990,6 +1057,8 @@ dvb_mux_build_msg(th_dvb_mux_instance_t *tdmi) htsmsg_t *m = htsmsg_create_map(); char buf[100]; + htsmsg_add_str(m, "adapterId", tdmi->tdmi_adapter->tda_identifier); + htsmsg_add_str(m, "id", tdmi->tdmi_identifier); htsmsg_add_u32(m, "enabled", tdmi->tdmi_enabled); htsmsg_add_str(m, "network", tdmi->tdmi_network ?: ""); @@ -1129,7 +1198,7 @@ dvb_mux_add_by_params(th_dvb_adapter_t *tda, break; case FE_ATSC: - dmc.dmc_fe_params.frequency = freq; + dmc.dmc_fe_params.frequency = freq * 1000; if(!val2str(constellation, qamtab)) return "Invalid VSB constellation"; @@ -1148,7 +1217,7 @@ dvb_mux_add_by_params(th_dvb_adapter_t *tda, } dmc.dmc_polarisation = polarisation; - tdmi = dvb_mux_create(tda, &dmc, 0, 0xffff, NULL, NULL, 1, 1, NULL, NULL); + tdmi = dvb_mux_create(tda, &dmc, 0, 0xffff, NULL, NULL, 1, 1, NULL, NULL, 1, NULL); if(tdmi == NULL) return "Mux already exist"; @@ -1175,13 +1244,13 @@ dvb_mux_copy(th_dvb_adapter_t *dst, th_dvb_mux_instance_t *tdmi_src, tdmi_src->tdmi_transport_stream_id, tdmi_src->tdmi_network, "copy operation", tdmi_src->tdmi_enabled, - 1, NULL, satconf); + 1, NULL, satconf, 1, tdmi_src); if(tdmi_dst == NULL) return -1; // Already exist LIST_FOREACH(t_src, &tdmi_src->tdmi_transports, s_group_link) { - t_dst = dvb_transport_find(tdmi_dst, + t_dst = dvb_service_find(tdmi_dst, t_src->s_dvb_service_id, t_src->s_pmt_pid, NULL); @@ -1244,3 +1313,86 @@ void dvb_mux_add_to_scan_queue ( th_dvb_mux_instance_t *tdmi ) tdmi->tdmi_scan_queue = &tda->tda_scan_queues[ti]; TAILQ_INSERT_TAIL(tdmi->tdmi_scan_queue, tdmi, tdmi_scan_link); } + +th_dvb_mux_instance_t *dvb_mux_find + ( th_dvb_adapter_t *tda, const char *netname, uint16_t onid, uint16_t tsid, + int enabled ) +{ + th_dvb_mux_instance_t *tdmi; + if (tda) { + LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) { + if (enabled && !tdmi->tdmi_enabled) continue; + if (onid && onid != tdmi->tdmi_network_id) continue; + if (tsid && tsid != tdmi->tdmi_transport_stream_id) continue; + if (netname && strcmp(netname, tdmi->tdmi_network ?: "")) continue; + return tdmi; + } + } else { + TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) + if ((tdmi = dvb_mux_find(tda, netname, onid, tsid, enabled))) + return tdmi; + } + return NULL; +} + + +/** + * + */ +th_subscription_t * +dvb_subscription_create_from_tdmi(th_dvb_mux_instance_t *tdmi, + const char *name, + streaming_target_t *st, + const char *hostname, + const char *username, + const char *client) +{ + th_subscription_t *s; + streaming_message_t *sm; + streaming_start_t *ss; + int r; + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + char buf[100]; + + s = subscription_create(INT32_MAX, name, st, SUBSCRIPTION_RAW_MPEGTS, + NULL, hostname, username, client); + + + s->ths_tdmi = tdmi; + LIST_INSERT_HEAD(&tdmi->tdmi_subscriptions, s, ths_tdmi_link); + + r = dvb_fe_tune(tdmi, "Full mux subscription"); + + pthread_mutex_lock(&tda->tda_delivery_mutex); + + streaming_target_connect(&tda->tda_streaming_pad, &s->ths_input); + + if(r) { + sm = streaming_msg_create_code(SMT_NOSTART, SM_CODE_NO_INPUT); + streaming_target_deliver(s->ths_output, sm); + } else { + ss = calloc(1, sizeof(streaming_start_t)); + ss->ss_num_components = 0; + ss->ss_refcount = 1; + + ss->ss_si.si_type = S_MPEG_TS; + ss->ss_si.si_device = strdup(tdmi->tdmi_adapter->tda_rootpath); + ss->ss_si.si_adapter = strdup(tdmi->tdmi_adapter->tda_displayname); + ss->ss_si.si_service = strdup("Full mux subscription"); + + if(tdmi->tdmi_network != NULL) + ss->ss_si.si_network = strdup(tdmi->tdmi_network); + + dvb_mux_nicename(buf, sizeof(buf), tdmi); + ss->ss_si.si_mux = strdup(buf); + + + sm = streaming_msg_create_data(SMT_START, ss); + streaming_target_deliver(s->ths_output, sm); + } + + pthread_mutex_unlock(&tda->tda_delivery_mutex); + + notify_reload("subscriptions"); + return s; +} diff --git a/src/dvb/dvb_preconf.c b/src/dvb/dvb_preconf.c index 755340db..bf9e8e34 100644 --- a/src/dvb/dvb_preconf.c +++ b/src/dvb/dvb_preconf.c @@ -98,7 +98,7 @@ dvb_mux_preconf_add(th_dvb_adapter_t *tda, const network_t *net, dmc.dmc_satconf = dvb_satconf_entry_find(tda, satconf, 0); - dvb_mux_create(tda, &dmc, 0, 0xffff, NULL, source, 1, 1, NULL, NULL); + dvb_mux_create(tda, &dmc, 0, 0xffff, NULL, source, 1, 1, NULL, NULL, 1, NULL); } } diff --git a/src/dvb/dvb_satconf.c b/src/dvb/dvb_satconf.c index 94a44e3a..ba607ef5 100644 --- a/src/dvb/dvb_satconf.c +++ b/src/dvb/dvb_satconf.c @@ -296,6 +296,7 @@ dvb_lnblist_get(void) add_to_lnblist(array, "C-Band"); add_to_lnblist(array, "C-Multi"); add_to_lnblist(array, "Circular 10750"); + add_to_lnblist(array, "Ku 11300"); return array; } @@ -338,5 +339,9 @@ dvb_lnb_get_frequencies(const char *id, int *f_low, int *f_hi, int *f_switch) *f_low = 10750000; *f_hi = 0; *f_switch = 0; + } else if(!strcmp(id, "Ku 11300")) { + *f_low = 11300000; + *f_hi = 0; + *f_switch = 0; } } diff --git a/src/dvb/dvb_transport.c b/src/dvb/dvb_service.c similarity index 71% rename from src/dvb/dvb_transport.c rename to src/dvb/dvb_service.c index 7106d133..f8f8def6 100644 --- a/src/dvb/dvb_transport.c +++ b/src/dvb/dvb_service.c @@ -44,54 +44,6 @@ #include "dvb_support.h" #include "notify.h" -/** - * - */ -static void -dvb_transport_open_demuxers(th_dvb_adapter_t *tda, service_t *t) -{ - struct dmx_pes_filter_params dmx_param; - int fd; - elementary_stream_t *st; - - TAILQ_FOREACH(st, &t->s_components, es_link) { - if(st->es_pid >= 0x2000) - continue; - - if(st->es_demuxer_fd != -1) - continue; - - fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); - st->es_cc_valid = 0; - - if(fd == -1) { - st->es_demuxer_fd = -1; - tvhlog(LOG_ERR, "dvb", - "\"%s\" unable to open demuxer \"%s\" for pid %d -- %s", - t->s_identifier, tda->tda_demux_path, - st->es_pid, strerror(errno)); - continue; - } - - memset(&dmx_param, 0, sizeof(dmx_param)); - dmx_param.pid = st->es_pid; - dmx_param.input = DMX_IN_FRONTEND; - dmx_param.output = DMX_OUT_TS_TAP; - dmx_param.pes_type = DMX_PES_OTHER; - dmx_param.flags = DMX_IMMEDIATE_START; - - if(ioctl(fd, DMX_SET_PES_FILTER, &dmx_param)) { - tvhlog(LOG_ERR, "dvb", - "\"%s\" unable to configure demuxer \"%s\" for pid %d -- %s", - t->s_identifier, tda->tda_demux_path, - st->es_pid, strerror(errno)); - close(fd); - fd = -1; - } - - st->es_demuxer_fd = fd; - } -} @@ -103,7 +55,7 @@ dvb_transport_open_demuxers(th_dvb_adapter_t *tda, service_t *t) * transports that is subscribing to the adapter */ static int -dvb_transport_start(service_t *t, unsigned int weight, int force_start) +dvb_service_start(service_t *t, unsigned int weight, int force_start) { int w, r; th_dvb_adapter_t *tda = t->s_dvb_mux_instance->tdmi_adapter; @@ -127,22 +79,28 @@ dvb_transport_start(service_t *t, unsigned int weight, int force_start) if(w && w >= weight && !force_start) /* We are outranked by weight, cant use it */ return SM_CODE_NOT_FREE; - + + if(LIST_FIRST(&tdmi->tdmi_subscriptions) != NULL) + return SM_CODE_NOT_FREE; + dvb_adapter_clean(tda); } + r = dvb_fe_tune(t->s_dvb_mux_instance, "Transport start"); + pthread_mutex_lock(&tda->tda_delivery_mutex); - r = dvb_fe_tune(t->s_dvb_mux_instance, "Transport start"); if(!r) LIST_INSERT_HEAD(&tda->tda_transports, t, s_active_link); pthread_mutex_unlock(&tda->tda_delivery_mutex); - if(!r) - dvb_transport_open_demuxers(tda, t); + if (tda->tda_locked) { + if(!r) + tda->tda_open_service(tda, t); - dvb_table_add_pmt(t->s_dvb_mux_instance, t->s_pmt_pid); + dvb_table_add_pmt(t->s_dvb_mux_instance, t->s_pmt_pid); + } return r; } @@ -152,10 +110,9 @@ dvb_transport_start(service_t *t, unsigned int weight, int force_start) * */ static void -dvb_transport_stop(service_t *t) +dvb_service_stop(service_t *t) { th_dvb_adapter_t *tda = t->s_dvb_mux_instance->tdmi_adapter; - elementary_stream_t *st; lock_assert(&global_lock); @@ -163,12 +120,8 @@ dvb_transport_stop(service_t *t) LIST_REMOVE(t, s_active_link); pthread_mutex_unlock(&tda->tda_delivery_mutex); - TAILQ_FOREACH(st, &t->s_components, es_link) { - if(st->es_demuxer_fd != -1) { - close(st->es_demuxer_fd); - st->es_demuxer_fd = -1; - } - } + tda->tda_close_service(tda, t); + t->s_status = SERVICE_IDLE; } @@ -177,12 +130,23 @@ dvb_transport_stop(service_t *t) * */ static void -dvb_transport_refresh(service_t *t) +dvb_service_refresh(service_t *t) { th_dvb_adapter_t *tda = t->s_dvb_mux_instance->tdmi_adapter; lock_assert(&global_lock); - dvb_transport_open_demuxers(tda, t); + tda->tda_open_service(tda, t); +} + +/** + * + */ +static int +dvb_service_is_enabled(service_t *t) +{ + th_dvb_mux_instance_t *tdmi = t->s_dvb_mux_instance; + th_dvb_adapter_t *tda = tdmi->tdmi_adapter; + return tda->tda_enabled && tdmi->tdmi_enabled && t->s_enabled && t->s_pmt_pid; } @@ -190,7 +154,7 @@ dvb_transport_refresh(service_t *t) * */ static void -dvb_transport_save(service_t *t) +dvb_service_save(service_t *t) { htsmsg_t *m = htsmsg_create_map(); @@ -221,6 +185,9 @@ dvb_transport_save(service_t *t) if(t->s_default_authority) htsmsg_add_str(m, "default_authority", t->s_default_authority); + if(t->s_prefcapid) + htsmsg_add_u32(m, "prefcapid", t->s_prefcapid); + pthread_mutex_lock(&t->s_stream_mutex); psi_save_service_settings(m, t); pthread_mutex_unlock(&t->s_stream_mutex); @@ -230,7 +197,7 @@ dvb_transport_save(service_t *t) t->s_identifier); htsmsg_destroy(m); - dvb_transport_notify(t); + dvb_service_notify(t); } @@ -238,7 +205,7 @@ dvb_transport_save(service_t *t) * Load config for the given mux */ void -dvb_transport_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier) +dvb_service_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier) { htsmsg_t *l, *c; htsmsg_field_t *f; @@ -269,7 +236,7 @@ dvb_transport_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier) if(htsmsg_get_u32(c, "pmt", &pmt)) continue; - t = dvb_transport_find(tdmi, sid, pmt, f->hmf_name); + t = dvb_service_find(tdmi, sid, pmt, f->hmf_name); htsmsg_get_u32(c, "stype", &t->s_servicetype); if(htsmsg_get_u32(c, "scrambled", &u32)) @@ -309,9 +276,13 @@ dvb_transport_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier) if(s && u32) service_map_channel(t, channel_find_by_name(s, 1, 0), 0); + if(htsmsg_get_u32(c, "prefcapid", &u32)) + u32 = 0; + t->s_prefcapid = u32; + /* HACK - force save for old config */ if(old) - dvb_transport_save(t); + dvb_service_save(t); } /* HACK - remove old settings */ @@ -332,7 +303,7 @@ dvb_transport_load(th_dvb_mux_instance_t *tdmi, const char *tdmi_identifier) * return that value */ static int -dvb_transport_quality(service_t *t) +dvb_service_quality(service_t *t) { th_dvb_mux_instance_t *tdmi = t->s_dvb_mux_instance; @@ -346,7 +317,7 @@ dvb_transport_quality(service_t *t) * Generate a descriptive name for the source */ static void -dvb_transport_setsourceinfo(service_t *t, struct source_info *si) +dvb_service_setsourceinfo(service_t *t, struct source_info *si) { th_dvb_mux_instance_t *tdmi = t->s_dvb_mux_instance; char buf[100]; @@ -355,6 +326,8 @@ dvb_transport_setsourceinfo(service_t *t, struct source_info *si) lock_assert(&global_lock); + si->si_type = S_MPEG_TS; + if(tdmi->tdmi_adapter->tda_rootpath != NULL) si->si_device = strdup(tdmi->tdmi_adapter->tda_rootpath); @@ -380,9 +353,47 @@ dvb_transport_setsourceinfo(service_t *t, struct source_info *si) static int dvb_grace_period(service_t *t) { + if (t->s_dvb_mux_instance && t->s_dvb_mux_instance->tdmi_adapter) + return t->s_dvb_mux_instance->tdmi_adapter->tda_grace_period ?: 10; return 10; } +/* + * Find transport based on the DVB identification + */ +service_t * +dvb_service_find3 + (th_dvb_adapter_t *tda, th_dvb_mux_instance_t *tdmi, + const char *netname, uint16_t onid, uint16_t tsid, uint16_t sid, + int enabled, int epgprimary) +{ + service_t *svc; + if (tdmi) { + LIST_FOREACH(svc, &tdmi->tdmi_transports, s_group_link) { + if (sid != svc->s_dvb_service_id) continue; + if (enabled && !svc->s_enabled) continue; + if (epgprimary && !service_is_primary_epg(svc)) continue; + return svc; + } + } else if (tda) { + LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) { + if (enabled && !tdmi->tdmi_enabled) continue; + if (onid && onid != tdmi->tdmi_network_id) continue; + if (tsid && tsid != tdmi->tdmi_transport_stream_id) continue; + if (netname && strcmp(netname, tdmi->tdmi_network ?: "")) continue; + if ((svc = dvb_service_find3(tda, tdmi, NULL, 0, 0, sid, + enabled, epgprimary))) + return svc; + } + } else { + TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) + if ((svc = dvb_service_find3(tda, NULL, netname, onid, tsid, + sid, enabled, epgprimary))) + return svc; + } + return NULL; +} + /** * Find a transport based on 'serviceid' on the given mux @@ -390,14 +401,14 @@ dvb_grace_period(service_t *t) * If it cannot be found we create it if 'pmt_pid' is also set */ service_t * -dvb_transport_find(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, +dvb_service_find(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, const char *identifier) { - return dvb_transport_find2(tdmi, sid, pmt_pid, identifier, NULL); + return dvb_service_find2(tdmi, sid, pmt_pid, identifier, NULL); } service_t * -dvb_transport_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, +dvb_service_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, const char *identifier, int *save) { service_t *t; @@ -408,12 +419,18 @@ dvb_transport_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, LIST_FOREACH(t, &tdmi->tdmi_transports, s_group_link) { if(t->s_dvb_service_id == sid) - return t; + break; + } + + /* Existing - updated PMT_PID if required */ + if (t) { + if (pmt_pid && pmt_pid != t->s_pmt_pid) { + t->s_pmt_pid = pmt_pid; + *save = 1; + } + return t; } - if(pmt_pid == 0) - return NULL; - if(identifier == NULL) { snprintf(tmp, sizeof(tmp), "%s_%04x", tdmi->tdmi_identifier, sid); identifier = tmp; @@ -428,13 +445,14 @@ dvb_transport_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, t->s_dvb_service_id = sid; t->s_pmt_pid = pmt_pid; - t->s_start_feed = dvb_transport_start; - t->s_refresh_feed = dvb_transport_refresh; - t->s_stop_feed = dvb_transport_stop; - t->s_config_save = dvb_transport_save; - t->s_setsourceinfo = dvb_transport_setsourceinfo; - t->s_quality_index = dvb_transport_quality; + t->s_start_feed = dvb_service_start; + t->s_refresh_feed = dvb_service_refresh; + t->s_stop_feed = dvb_service_stop; + t->s_config_save = dvb_service_save; + t->s_setsourceinfo = dvb_service_setsourceinfo; + t->s_quality_index = dvb_service_quality; t->s_grace_period = dvb_grace_period; + t->s_is_enabled = dvb_service_is_enabled; t->s_dvb_mux_instance = tdmi; LIST_INSERT_HEAD(&tdmi->tdmi_transports, t, s_group_link); @@ -452,10 +470,11 @@ dvb_transport_find2(th_dvb_mux_instance_t *tdmi, uint16_t sid, int pmt_pid, * */ htsmsg_t * -dvb_transport_build_msg(service_t *t) +dvb_service_build_msg(service_t *t) { th_dvb_mux_instance_t *tdmi = t->s_dvb_mux_instance; htsmsg_t *m = htsmsg_create_map(); + uint16_t caid; char buf[100]; htsmsg_add_str(m, "id", t->s_identifier); @@ -466,16 +485,25 @@ dvb_transport_build_msg(service_t *t) htsmsg_add_u32(m, "pmt", t->s_pmt_pid); htsmsg_add_u32(m, "pcr", t->s_pcr_pid); - htsmsg_add_str(m, "type", service_servicetype_txt(t)); + snprintf(buf, sizeof(buf), "%s (0x%04X)", service_servicetype_txt(t), t->s_servicetype); + htsmsg_add_str(m, "type", buf); + htsmsg_add_str(m, "typestr", service_servicetype_txt(t)); + htsmsg_add_u32(m, "typenum", t->s_servicetype); htsmsg_add_str(m, "svcname", t->s_svcname ?: ""); htsmsg_add_str(m, "provider", t->s_provider ?: ""); htsmsg_add_str(m, "network", tdmi->tdmi_network ?: ""); + if((caid = service_get_encryption(t)) != 0) + htsmsg_add_str(m, "encryption", psi_caid2name(caid)); + dvb_mux_nicefreq(buf, sizeof(buf), tdmi); htsmsg_add_str(m, "mux", buf); + if(tdmi->tdmi_conf.dmc_satconf != NULL) + htsmsg_add_str(m, "satconf", tdmi->tdmi_conf.dmc_satconf->sc_id); + if(t->s_ch != NULL) htsmsg_add_str(m, "channelname", t->s_ch->ch_name); @@ -484,6 +512,8 @@ dvb_transport_build_msg(service_t *t) htsmsg_add_u32(m, "dvb_eit_enable", t->s_dvb_eit_enable); + htsmsg_add_u32(m, "prefcapid", t->s_prefcapid); + return m; } @@ -492,7 +522,7 @@ dvb_transport_build_msg(service_t *t) * */ void -dvb_transport_notify_by_adapter(th_dvb_adapter_t *tda) +dvb_service_notify_by_adapter(th_dvb_adapter_t *tda) { htsmsg_t *m = htsmsg_create_map(); htsmsg_add_str(m, "adapterId", tda->tda_identifier); @@ -504,7 +534,7 @@ dvb_transport_notify_by_adapter(th_dvb_adapter_t *tda) * */ void -dvb_transport_notify(service_t *t) +dvb_service_notify(service_t *t) { th_dvb_mux_instance_t *tdmi = t->s_dvb_mux_instance; htsmsg_t *m = htsmsg_create_map(); diff --git a/src/dvb/dvb_support.c b/src/dvb/dvb_support.c index 64b35b2a..6d883e00 100644 --- a/src/dvb/dvb_support.c +++ b/src/dvb/dvb_support.c @@ -95,10 +95,7 @@ static inline size_t conv_8859(int conv, (*dstlen)--; dst++; } else if (c <= 0x9f) { - // codes 0x80 - 0x9f (control codes) are mapped to ' ' - *dst = ' '; - (*dstlen)--; - dst++; + // codes 0x80 - 0x9f (control codes) are ignored } else { // map according to character table, skipping // unmapped chars (value 0 in the table) @@ -135,10 +132,7 @@ static inline size_t conv_6937(const uint8_t *src, size_t srclen, (*dstlen)--; dst++; } else if (c <= 0x9f) { - // codes 0x80 - 0x9f (control codes) are mapped to ' ' - *dst = ' '; - (*dstlen)--; - dst++; + // codes 0x80 - 0x9f (control codes) are ignored } else { uint16_t uc; if (c >= 0xc0 && c <= 0xcf) { diff --git a/src/dvb/dvb_support.h b/src/dvb/dvb_support.h index 22e81eca..e9baf7be 100644 --- a/src/dvb/dvb_support.h +++ b/src/dvb/dvb_support.h @@ -47,6 +47,7 @@ #define DVB_DESC_PARENTAL_RAT 0x55 #define DVB_DESC_TELETEXT 0x56 #define DVB_DESC_SUBTITLE 0x59 +#define DVB_DESC_TERR 0x5a #define DVB_DESC_AC3 0x6a #define DVB_DESC_DEF_AUTHORITY 0x73 #define DVB_DESC_CRID 0x76 diff --git a/src/dvb/dvb_tables.c b/src/dvb/dvb_tables.c index dea5892e..037fbbbb 100644 --- a/src/dvb/dvb_tables.c +++ b/src/dvb/dvb_tables.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -41,17 +42,7 @@ #include "psi.h" #include "notify.h" #include "cwc.h" - -static int tdt_id_tally; - -/** - * Helper for preparing a section filter parameter struct - */ -struct dmx_sct_filter_params * -dvb_fparams_alloc(void) -{ - return calloc(1, sizeof(struct dmx_sct_filter_params)); -} +#include "tvhtime.h" /** * @@ -85,69 +76,17 @@ dvb_table_fastswitch(th_dvb_mux_instance_t *tdmi) /** * */ -static void -tdt_open_fd(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) +void +dvb_table_dispatch(uint8_t *sec, int r, th_dvb_table_t *tdt) { - th_dvb_adapter_t *tda = tdmi->tdmi_adapter; - struct epoll_event e; - - assert(tdt->tdt_fd == -1); - TAILQ_REMOVE(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); + if(tdt->tdt_destroyed) + return; - tdt->tdt_fd = tvh_open(tda->tda_demux_path, O_RDWR, 0); - - if(tdt->tdt_fd != -1) { - - tdt->tdt_id = ++tdt_id_tally; - - e.events = EPOLLIN; - e.data.u64 = ((uint64_t)tdt->tdt_fd << 32) | tdt->tdt_id; - - if(epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_ADD, tdt->tdt_fd, &e)) { - close(tdt->tdt_fd); - tdt->tdt_fd = -1; - } else { - if(ioctl(tdt->tdt_fd, DMX_SET_FILTER, tdt->tdt_fparams)) { - close(tdt->tdt_fd); - tdt->tdt_fd = -1; - } - } - } - - if(tdt->tdt_fd == -1) - TAILQ_INSERT_TAIL(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); -} - - -/** - * Close FD for the given table and put table on the pending list - */ -static void -tdt_close_fd(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) -{ - th_dvb_adapter_t *tda = tdmi->tdmi_adapter; - - assert(tdt->tdt_fd != -1); - - epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_DEL, tdt->tdt_fd, NULL); - close(tdt->tdt_fd); - - tdt->tdt_fd = -1; - TAILQ_INSERT_TAIL(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); -} - - -/** - * - */ -static void -dvb_proc_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt, uint8_t *sec, - int r) -{ int chkcrc = tdt->tdt_flags & TDT_CRC; int tableid, len; uint8_t *ptr; int ret; + th_dvb_mux_instance_t *tdmi = tdt->tdt_tdmi; /* It seems some hardware (or is it the dvb API?) does not honour the DMX_CHECK_CRC flag, so we check it again */ @@ -161,6 +100,9 @@ dvb_proc_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt, uint8_t *sec, if(len < r) return; + if((tableid & tdt->tdt_mask) != tdt->tdt_table) + return; + ptr = &sec[3]; if(chkcrc) len -= 4; /* Strip trailing CRC */ @@ -168,9 +110,9 @@ dvb_proc_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt, uint8_t *sec, ret = tdt->tdt_callback((th_dvb_mux_instance_t *)tdt, sec, len + 3, tableid, tdt->tdt_opaque); else if(tdt->tdt_flags & TDT_TDT) - ret = tdt->tdt_callback(tdmi, ptr, len, tableid, tdt); + ret = tdt->tdt_callback(tdt->tdt_tdmi, ptr, len, tableid, tdt); else - ret = tdt->tdt_callback(tdmi, ptr, len, tableid, tdt->tdt_opaque); + ret = tdt->tdt_callback(tdt->tdt_tdmi, ptr, len, tableid, tdt->tdt_opaque); if(ret == 0) tdt->tdt_count++; @@ -179,72 +121,15 @@ dvb_proc_table(th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt, uint8_t *sec, dvb_table_fastswitch(tdmi); } -/** - * - */ -static void * -dvb_table_input(void *aux) -{ - th_dvb_adapter_t *tda = aux; - int r, i, tid, fd, x; - struct epoll_event ev[1]; - uint8_t sec[4096]; - th_dvb_mux_instance_t *tdmi; - th_dvb_table_t *tdt; - int64_t cycle_barrier = 0; - - while(1) { - x = epoll_wait(tda->tda_table_epollfd, ev, sizeof(ev) / sizeof(ev[0]), -1); - - for(i = 0; i < x; i++) { - - tid = ev[i].data.u64 & 0xffffffff; - fd = ev[i].data.u64 >> 32; - - if(!(ev[i].events & EPOLLIN)) - continue; - - if((r = read(fd, sec, sizeof(sec))) < 3) - continue; - - pthread_mutex_lock(&global_lock); - if((tdmi = tda->tda_mux_current) != NULL) { - LIST_FOREACH(tdt, &tdmi->tdmi_tables, tdt_link) - if(tdt->tdt_id == tid) - break; - - if(tdt != NULL) { - dvb_proc_table(tdmi, tdt, sec, r); - - /* Any tables pending (that wants a filter/fd), close this one */ - if(TAILQ_FIRST(&tdmi->tdmi_table_queue) != NULL && - cycle_barrier < getmonoclock()) { - tdt_close_fd(tdmi, tdt); - cycle_barrier = getmonoclock() + 100000; - tdt = TAILQ_FIRST(&tdmi->tdmi_table_queue); - assert(tdt != NULL); - - tdt_open_fd(tdmi, tdt); - } - } - } - pthread_mutex_unlock(&global_lock); - } - } - return NULL; -} - - /** * */ void -dvb_table_init(th_dvb_adapter_t *tda) +dvb_table_release(th_dvb_table_t *tdt) { - pthread_t ptid; - tda->tda_table_epollfd = epoll_create(50); - pthread_create(&ptid, NULL, dvb_table_input, tda); + if(--tdt->tdt_refcount == 0) + free(tdt); } @@ -255,31 +140,25 @@ static void dvb_tdt_destroy(th_dvb_adapter_t *tda, th_dvb_mux_instance_t *tdmi, th_dvb_table_t *tdt) { + lock_assert(&global_lock); + assert(tdt->tdt_tdmi == tdmi); LIST_REMOVE(tdt, tdt_link); - - if(tdt->tdt_fd == -1) { - TAILQ_REMOVE(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); - } else { - epoll_ctl(tda->tda_table_epollfd, EPOLL_CTL_DEL, tdt->tdt_fd, NULL); - close(tdt->tdt_fd); - } - + tdmi->tdmi_num_tables--; + tda->tda_close_table(tdmi, tdt); free(tdt->tdt_name); - free(tdt->tdt_fparams); - free(tdt); + tdt->tdt_destroyed = 1; + dvb_table_release(tdt); } - - /** * Add a new DVB table */ void -tdt_add(th_dvb_mux_instance_t *tdmi, struct dmx_sct_filter_params *fparams, +tdt_add(th_dvb_mux_instance_t *tdmi, int tableid, int mask, int (*callback)(th_dvb_mux_instance_t *tdmi, uint8_t *buf, int len, uint8_t tableid, void *opaque), void *opaque, - const char *name, int flags, int pid, th_dvb_table_t *tdt) + const char *name, int flags, int pid) { th_dvb_table_t *t; @@ -290,34 +169,25 @@ tdt_add(th_dvb_mux_instance_t *tdmi, struct dmx_sct_filter_params *fparams, LIST_FOREACH(t, &tdmi->tdmi_tables, tdt_link) { if(pid == t->tdt_pid && t->tdt_callback == callback && t->tdt_opaque == opaque) { - if (tdt) free(tdt); - if (fparams) free(fparams); return; } } - if(fparams == NULL) - fparams = dvb_fparams_alloc(); - - if(flags & TDT_CRC) fparams->flags |= DMX_CHECK_CRC; - fparams->flags |= DMX_IMMEDIATE_START; - fparams->pid = pid; - - - if(tdt == NULL) - tdt = calloc(1, sizeof(th_dvb_table_t)); - + th_dvb_table_t *tdt = calloc(1, sizeof(th_dvb_table_t)); + tdt->tdt_refcount = 1; tdt->tdt_name = strdup(name); tdt->tdt_callback = callback; tdt->tdt_opaque = opaque; tdt->tdt_pid = pid; tdt->tdt_flags = flags; - tdt->tdt_fparams = fparams; + tdt->tdt_table = tableid; + tdt->tdt_mask = mask; + tdt->tdt_tdmi = tdmi; LIST_INSERT_HEAD(&tdmi->tdmi_tables, tdt, tdt_link); + tdmi->tdmi_num_tables++; tdt->tdt_fd = -1; - TAILQ_INSERT_TAIL(&tdmi->tdmi_table_queue, tdt, tdt_pending_link); - tdt_open_fd(tdmi, tdt); + tdmi->tdmi_adapter->tda_open_table(tdmi, tdt); } /** @@ -452,7 +322,11 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, char provider[256]; char chname0[256], *chname; uint8_t stype; +#if ENABLE_TRACE + uint8_t running_status; +#endif int l; + uint8_t *dlptr, *dptr; th_dvb_adapter_t *tda = tdmi->tdmi_adapter; @@ -462,21 +336,27 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, tsid = ptr[0] << 8 | ptr[1]; onid = ptr[5] << 8 | ptr[6]; + + tvhtrace("sdt", "onid %04X tsid %04X", onid, tsid); + tvhlog_hexdump("sdt", ptr, len); + + /* Find Transport Stream */ if (tableid == 0x42) { + dvb_mux_set_tsid(tdmi, tsid, 0); + dvb_mux_set_onid(tdmi, onid, 0); if(tdmi->tdmi_transport_stream_id != tsid || tdmi->tdmi_network_id != onid) return -1; } else { LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) if(tdmi->tdmi_transport_stream_id == tsid && - tdmi->tdmi_network_id != onid) + tdmi->tdmi_network_id == onid) break; - if (!tdmi) return 0; + if (!tdmi) return -1; } // version = ptr[2] >> 1 & 0x1f; // section_number = ptr[3]; // last_section_number = ptr[4]; - // original_network_id = ptr[5] << 8 | ptr[6]; // reserved = ptr[7]; if((ptr[2] & 1) == 0) { @@ -487,44 +367,45 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, len -= 8; ptr += 8; - while(len >= 5) { int save = 0; service_id = ptr[0] << 8 | ptr[1]; // reserved = ptr[2]; - // running_status = (ptr[3] >> 5) & 0x7; +#if ENABLE_TRACE + running_status = (ptr[3] >> 5) & 0x7; +#endif free_ca_mode = (ptr[3] >> 4) & 0x1; dllen = ((ptr[3] & 0x0f) << 8) | ptr[4]; + dlptr = ptr + 5; + tvhtrace("sdt", " sid %04X running %d free_ca %d dllen %d", + service_id, running_status, free_ca_mode, dllen); - len -= 5; - ptr += 5; - - if(dllen > len) + ptr += (5 + dllen); + len -= (5 + dllen); + if (len < 0) break; - if (!(t = dvb_transport_find(tdmi, service_id, 0, NULL))) { - len -= dllen; - ptr += dllen; - continue; - } - stype = 0; chname = NULL; *crid = 0; while(dllen > 2) { - dtag = ptr[0]; - dlen = ptr[1]; + dtag = dlptr[0]; + dlen = dlptr[1]; + dptr = dlptr + 2; - len -= 2; ptr += 2; dllen -= 2; + dlptr += (2 + dlen); + dllen -= (2 + dlen); - if(dlen > len) break; + if(dllen < 0) break; switch(dtag) { case DVB_DESC_SERVICE: - if(dvb_desc_service(ptr, dlen, &stype, + if(dvb_desc_service(dptr, dlen, &stype, provider, sizeof(provider), chname0, sizeof(chname0)) == 0) { + tvhtrace("sdt", " stype = %d, provider = %s, name = %s", + stype, provider, chname0); chname = chname0; /* Some providers insert spaces. Clean up that (both heading and trailing) */ @@ -544,12 +425,14 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, } break; case DVB_DESC_DEF_AUTHORITY: - dvb_desc_def_authority(ptr, dlen, crid, sizeof(crid)); + dvb_desc_def_authority(dptr, dlen, crid, sizeof(crid)); break; } - len -= dlen; ptr += dlen; dllen -= dlen; } - + + if (!(t = dvb_service_find(tdmi, service_id, 0, NULL))) + continue; + if(t->s_servicetype != stype || t->s_scrambled != free_ca_mode) { t->s_servicetype = stype; @@ -559,17 +442,30 @@ dvb_sdt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, if (chname && (strcmp(t->s_provider ?: "", provider) || strcmp(t->s_svcname ?: "", chname))) { - free(t->s_provider); - t->s_provider = strdup(provider); + int save2 = 0; + int master = 0; + if (t->s_dvb_mux_instance && t->s_dvb_mux_instance->tdmi_network_id && + t->s_dvb_mux_instance->tdmi_network_id == tdmi->tdmi_network_id) + master = 1; + + if (!t->s_provider || master) { + free(t->s_provider); + t->s_provider = strdup(provider); + save2 = 1; + } - free(t->s_svcname); - t->s_svcname = strdup(chname); + if (!t->s_svcname || master) { + free(t->s_svcname); + t->s_svcname = strdup(chname); + save2 = 1; + } - pthread_mutex_lock(&t->s_stream_mutex); - service_make_nicename(t); - pthread_mutex_unlock(&t->s_stream_mutex); - - save = 1; + if (save2) { + pthread_mutex_lock(&t->s_stream_mutex); + service_make_nicename(t); + pthread_mutex_unlock(&t->s_stream_mutex); + save = 1; + } } if (*crid && strcmp(t->s_default_authority ?: "", crid)) { @@ -607,7 +503,6 @@ static int dvb_pat_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, uint8_t tableid, void *opaque) { - th_dvb_mux_instance_t *other; th_dvb_adapter_t *tda = tdmi->tdmi_adapter; uint16_t service, pmt, tsid; @@ -620,21 +515,9 @@ dvb_pat_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, } tsid = (ptr[0] << 8) | ptr[1]; - - // Make sure this TSID is not already known on another mux - // That might indicate that we have accedentally received a PAT - // from another mux - LIST_FOREACH(other, &tda->tda_muxes, tdmi_adapter_link) - if(other != tdmi && - other->tdmi_conf.dmc_satconf == tdmi->tdmi_conf.dmc_satconf && - other->tdmi_transport_stream_id == tsid && - other->tdmi_network_id == tdmi->tdmi_network_id) - return -1; - - if(tdmi->tdmi_transport_stream_id == 0xffff) - dvb_mux_set_tsid(tdmi, tsid); - else if (tdmi->tdmi_transport_stream_id != tsid) - return -1; // TSID mismatches, skip packet, may be from another mux + dvb_mux_set_tsid(tdmi, tsid, 0); + if (tdmi->tdmi_transport_stream_id != tsid) + return -1; ptr += 5; len -= 5; @@ -645,7 +528,7 @@ dvb_pat_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, if(service != 0 && pmt != 0) { int save = 0; - dvb_transport_find2(tdmi, service, pmt, NULL, &save); + dvb_service_find2(tdmi, service, pmt, NULL, &save); if (save || !tda->tda_disable_pmt_monitor) dvb_table_add_pmt(tdmi, pmt); } @@ -663,7 +546,9 @@ static int dvb_ca_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, uint8_t tableid, void *opaque) { +#if ENABLE_CWC cwc_emm(ptr, len, (uintptr_t)opaque, (void *)tdmi); +#endif return 0; } @@ -698,8 +583,8 @@ dvb_cat_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, if(pid == 0) break; - tdt_add(tdmi, NULL, dvb_ca_callback, (void *)caid, "CA", - TDT_CA, pid, NULL); + tdt_add(tdmi, 0, 0, dvb_ca_callback, (void *)caid, "CA", + TDT_CA, pid); break; default: @@ -739,19 +624,49 @@ static const fe_modulation_t qam_tab [6] = { QAM_AUTO, QAM_16, QAM_32, QAM_64, QAM_128, QAM_256 }; +static const fe_bandwidth_t bandwidth_tab [8] = { + BANDWIDTH_8_MHZ, BANDWIDTH_7_MHZ, BANDWIDTH_6_MHZ, BANDWIDTH_AUTO, + BANDWIDTH_AUTO, BANDWIDTH_AUTO, BANDWIDTH_AUTO, BANDWIDTH_AUTO +}; + +static const fe_modulation_t constellation_tab [4] = { + QPSK, QAM_16, QAM_64, QAM_AUTO +}; + +static const fe_code_rate_t code_rate_tab [8] = { + FEC_1_2, FEC_2_3, FEC_3_4, FEC_5_6, FEC_7_8, FEC_AUTO, FEC_AUTO, FEC_AUTO +}; + +static const fe_guard_interval_t guard_interval_tab [4] = { + GUARD_INTERVAL_1_32, GUARD_INTERVAL_1_16, GUARD_INTERVAL_1_8, GUARD_INTERVAL_1_4 +}; + +static const fe_transmit_mode_t transmission_mode_tab [4] = { + TRANSMISSION_MODE_2K, + TRANSMISSION_MODE_8K, +#if DVB_VER_ATLEAST(5,3) + TRANSMISSION_MODE_4K, +#else + TRANSMISSION_MODE_AUTO, /* For older DVB API versions - hope the device can detect */ +#endif + TRANSMISSION_MODE_AUTO +}; + +static const fe_hierarchy_t hierarchy_info_tab [8] = { + HIERARCHY_NONE, HIERARCHY_1, HIERARCHY_2, HIERARCHY_4, + HIERARCHY_NONE, HIERARCHY_1, HIERARCHY_2, HIERARCHY_4 +}; + /** * Cable delivery descriptor */ static int dvb_table_cable_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, - uint16_t tsid, uint16_t onid) + uint16_t tsid, uint16_t onid, const char *netname) { struct dvb_mux_conf dmc; int freq, symrate; - if(!tdmi->tdmi_adapter->tda_autodiscovery) - return -1; - if(len < 11) return -1; @@ -766,6 +681,7 @@ dvb_table_cable_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, return -1; dmc.dmc_fe_params.frequency = freq * 100; + tvhtrace("nit", " dvb-c frequency %d", dmc.dmc_fe_params.frequency); symrate = bcdtoint(ptr[7]) * 100000 + bcdtoint(ptr[8]) * 1000 + @@ -781,8 +697,10 @@ dvb_table_cable_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, dmc.dmc_fe_params.u.qam.fec_inner = fec_tab[ptr[10] & 0x07]; - dvb_mux_create(tdmi->tdmi_adapter, &dmc, onid, tsid, NULL, - "automatic mux discovery", 1, 1, NULL, NULL); + dvb_mux_create(tdmi->tdmi_adapter, &dmc, onid, tsid, netname, + "automatic mux discovery", 1, 1, NULL, NULL, + tdmi->tdmi_adapter->tda_autodiscovery, tdmi); + return 0; } @@ -791,14 +709,13 @@ dvb_table_cable_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, */ static int dvb_table_sat_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, - uint16_t tsid, uint16_t onid) + uint16_t tsid, uint16_t onid, const char *netname) { int freq, symrate; - // uint16_t orbital_pos; struct dvb_mux_conf dmc; - - if(!tdmi->tdmi_adapter->tda_autodiscovery) - return -1; +#if ENABLE_TRACE + uint16_t orbital_pos; +#endif if(len < 11) return -1; @@ -810,11 +727,17 @@ dvb_table_sat_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, bcdtoint(ptr[0]) * 1000000 + bcdtoint(ptr[1]) * 10000 + bcdtoint(ptr[2]) * 100 + bcdtoint(ptr[3]); dmc.dmc_fe_params.frequency = freq * 10; + tvhtrace("nit", " dvb-s frequency %d", dmc.dmc_fe_params.frequency); - if(!freq) + if(!freq) { + tvhlog(LOG_ERR, "nit", "invalid frequency (%d)", freq); return -1; + } - // orbital_pos = bcdtoint(ptr[4]) * 100 + bcdtoint(ptr[5]); +#if ENABLE_TRACE + orbital_pos = bcdtoint(ptr[4]) * 100 + bcdtoint(ptr[5]); +#endif + tvhtrace("nit", " orbital pos %d", orbital_pos); symrate = bcdtoint(ptr[7]) * 100000 + bcdtoint(ptr[8]) * 1000 + @@ -861,13 +784,54 @@ dvb_table_sat_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, } if (dmc.dmc_fe_delsys == SYS_DVBS && dmc.dmc_fe_rolloff != ROLLOFF_35) { - printf ("error descriptor\n"); + tvhlog(LOG_ERR, "nit", "error descriptor"); return -1; } #endif - dvb_mux_create(tdmi->tdmi_adapter, &dmc, onid, tsid, NULL, - "automatic mux discovery", 1, 1, NULL, tdmi->tdmi_conf.dmc_satconf); + + dvb_mux_create(tdmi->tdmi_adapter, &dmc, onid, tsid, netname, + "automatic mux discovery", 1, 1, NULL, tdmi->tdmi_conf.dmc_satconf, + tdmi->tdmi_adapter->tda_autodiscovery, tdmi); + + return 0; +} + + +/** + * Terrestrial delivery descriptor + */ +static int +dvb_table_terr_delivery(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, + uint16_t tsid, uint16_t onid, const char *netname) +{ + struct dvb_mux_conf dmc; + int freq; + + if(len < 11) + return -1; + + memset(&dmc, 0, sizeof(dmc)); + dmc.dmc_fe_params.inversion = INVERSION_AUTO; + + freq = ((ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3]) * 10; + + if(!freq) + return -1; + + dmc.dmc_fe_params.frequency = freq; + tvhtrace("nit", " dvb-t frequency %d", dmc.dmc_fe_params.frequency); + dmc.dmc_fe_params.u.ofdm.bandwidth = bandwidth_tab[(ptr[4] & 0xe0) >> 5]; + dmc.dmc_fe_params.u.ofdm.constellation=constellation_tab[(ptr[5] & 0xc0) >> 6]; + dmc.dmc_fe_params.u.ofdm.hierarchy_information=hierarchy_info_tab[(ptr[5] & 0x38) >> 3]; + dmc.dmc_fe_params.u.ofdm.code_rate_HP=code_rate_tab[ptr[5] & 0x3]; + dmc.dmc_fe_params.u.ofdm.code_rate_LP=code_rate_tab[(ptr[6] & 0xe0) >> 5]; + dmc.dmc_fe_params.u.ofdm.guard_interval=guard_interval_tab[(ptr[6] & 0x18) >> 3]; + dmc.dmc_fe_params.u.ofdm.transmission_mode=transmission_mode_tab[(ptr[6] & 0x06) >> 1]; + + dvb_mux_create(tdmi->tdmi_adapter, &dmc, onid, tsid, netname, + "automatic mux discovery", 1, 1, NULL, NULL, + tdmi->tdmi_adapter->tda_autodiscovery, tdmi); return 0; } @@ -896,14 +860,14 @@ dvb_table_local_channel(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, chan = ((ptr[2] & 3) << 8) | ptr[3]; if(chan != 0) { - t = dvb_transport_find(tdmi, sid, 0, NULL); + t = dvb_service_find(tdmi, sid, 0, NULL); if(t != NULL) { - if(t->s_channel_number != chan) { - t->s_channel_number = chan; - t->s_config_save(t); - service_refresh_channel(t); - } + if(t->s_channel_number != chan) { + t->s_channel_number = chan; + t->s_config_save(t); + service_refresh_channel(t); + } } } ptr += 4; @@ -920,103 +884,104 @@ static int dvb_nit_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, uint8_t tableid, void *opaque) { - uint8_t tag, tlen; - int ntl; - char networkname[256]; + uint8_t dtag, dlen; + uint16_t llen; + char netname[256]; uint16_t tsid, onid; uint16_t network_id = (ptr[0] << 8) | ptr[1]; + netname[0] = '\0'; + tvhtrace("nit", "tableid 0x%02x", tableid); + tvhlog_hexdump("nit", ptr, len); + + /* Specific NID requested */ if(tdmi->tdmi_adapter->tda_nitoid) { - if(tableid != 0x41) - return -1; - - if(network_id != tdmi->tdmi_adapter->tda_nitoid) - return -1; - - } else { - if(tableid != 0x40) + if (tdmi->tdmi_adapter->tda_nitoid != network_id) return -1; + } else if (tableid != 0x40) { + return -1; } - if((ptr[2] & 1) == 0) { - /* current_next_indicator == next, skip this */ - return -1; - } - - ptr += 5; - len -= 5; - - ntl = ((ptr[0] & 0xf) << 8) | ptr[1]; - ptr += 2; - len -= 2; - if(ntl > len) + /* Ignore non-current */ + if((ptr[2] & 1) == 0) return -1; - while(ntl > 2) { - tag = *ptr++; - tlen = *ptr++; - len -= 2; - ntl -= 2; + /* Network descriptors */ + llen = ((ptr[5] & 0xf) << 8) | ptr[6]; + ptr += 7; + len -= llen + 7; + if (len < 0) + return -1; - switch(tag) { - case DVB_DESC_NETWORK_NAME: - if(dvb_get_string(networkname, sizeof(networkname), ptr, tlen, NULL, NULL)) - return -1; + while(llen > 2) { + dtag = ptr[0]; + dlen = ptr[1]; - if(strcmp(tdmi->tdmi_network ?: "", networkname)) - dvb_mux_set_networkname(tdmi, networkname); - break; + tvhtrace("nit", "dtag %02X dlen %d", dtag, dlen); + + switch(dtag) { + case DVB_DESC_NETWORK_NAME: + if(dvb_get_string(netname, sizeof(netname), ptr+2, dlen, NULL, NULL)) + return -1; + if((tableid == 0x40) && (!tdmi->tdmi_network || *tdmi->tdmi_network == '\0')) + dvb_mux_set_networkname(tdmi, netname); + break; } - ptr += tlen; - len -= tlen; - ntl -= tlen; + ptr += dlen + 2; + llen -= dlen + 2; } - - if(len < 2) + tvhtrace("nit", "network %d/%s", network_id, netname); + if (llen) return -1; - ntl = ((ptr[0] & 0xf) << 8) | ptr[1]; + /* Transport loop */ + llen = ((ptr[0] & 0xf) << 8) | ptr[1]; ptr += 2; len -= 2; - - if(len < ntl) + if (llen > len) return -1; - while(len >= 6) { tsid = ( ptr[0] << 8) | ptr[1]; onid = ( ptr[2] << 8) | ptr[3]; - ntl = ((ptr[4] & 0xf) << 8) | ptr[5]; + llen = ((ptr[4] & 0xf) << 8) | ptr[5]; + + tvhtrace("nit", " onid %04X tsid %04X", onid, tsid); ptr += 6; - len -= 6; - if(ntl > len) - break; + len -= llen + 6; + if(len < 0) + return -1; - while(ntl > 2) { - tag = *ptr++; - tlen = *ptr++; - len -= 2; - ntl -= 2; + while(llen > 2) { + dtag = ptr[0]; + dlen = ptr[1]; - switch(tag) { - case DVB_DESC_SAT: - if(tdmi->tdmi_adapter->tda_type == FE_QPSK) - dvb_table_sat_delivery(tdmi, ptr, tlen, tsid, onid); - break; - case DVB_DESC_CABLE: - if(tdmi->tdmi_adapter->tda_type == FE_QAM) - dvb_table_cable_delivery(tdmi, ptr, tlen, tsid, onid); - break; - case DVB_DESC_LOCAL_CHAN: - dvb_table_local_channel(tdmi, ptr, tlen, tsid, onid); - break; + tvhtrace("nit", " dtag %02X dlen %d", dtag, dlen); + + switch(dtag) { + case DVB_DESC_SAT: + if(tdmi->tdmi_adapter->tda_type == FE_QPSK) + dvb_table_sat_delivery(tdmi, ptr+2, dlen, tsid, onid, netname); + break; + case DVB_DESC_CABLE: + if(tdmi->tdmi_adapter->tda_type == FE_QAM) + dvb_table_cable_delivery(tdmi, ptr+2, dlen, tsid, onid, netname); + break; + case DVB_DESC_TERR: + if(tdmi->tdmi_adapter->tda_type == FE_OFDM) + dvb_table_terr_delivery(tdmi, ptr+2, dlen, tsid, onid, netname); + break; + case DVB_DESC_LOCAL_CHAN: + dvb_table_local_channel(tdmi, ptr+2, dlen, tsid, onid); + break; } - ptr += tlen; - len -= tlen; - ntl -= tlen; + llen -= dlen + 2; + ptr += dlen + 2; } + if (llen) + return -1; } return 0; } @@ -1029,6 +994,7 @@ static int atsc_vct_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, uint8_t tableid, void *opaque) { + th_dvb_mux_instance_t *tdmi0 = tdmi; th_dvb_adapter_t *tda = tdmi->tdmi_adapter; service_t *t; int numch; @@ -1062,17 +1028,21 @@ atsc_vct_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, tsid = (ptr[22] << 8) | ptr[23]; onid = (ptr[24] << 8) | ptr[25]; - - /* Search all muxes on adapter */ - LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) - if(tdmi->tdmi_transport_stream_id == tsid && tdmi->tdmi_network_id == onid); - break; - - if(tdmi == NULL) - continue; + + if(tsid == 0) { + tdmi = tdmi0; + } else { + /* Search all muxes on adapter */ + LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) { + if(tdmi->tdmi_transport_stream_id == tsid && tdmi->tdmi_network_id == onid) + break; + } + if(tdmi == NULL) + continue; + } service_id = (ptr[24] << 8) | ptr[25]; - if((t = dvb_transport_find(tdmi, service_id, 0, NULL)) == NULL) + if((t = dvb_service_find(tdmi, service_id, 0, NULL)) == NULL) continue; atsc_stype = ptr[27] & 0x3f; @@ -1131,6 +1101,51 @@ dvb_pmt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, return 0; } +/* + * Time Offset table handler + */ +static int +dvb_tot_callback(th_dvb_mux_instance_t *tdmi, uint8_t *buf, int len, + uint8_t tableid, void *opaque) +{ + uint16_t mjd; + uint8_t hour, min, sec; + int year, mon, day; + struct tm utc; + + if (tableid != 0x73) + return -1; + + /* DVB format MJD, Hour, Min, Sec */ + mjd = (buf[0] << 8) | buf[1]; + hour = bcdtoint(buf[2]); + min = bcdtoint(buf[3]); + sec = bcdtoint(buf[4]); + + /* Convert MJD (using algo from EN 300 468 v1.13.1 Annex C) */ + year = (int)((mjd - 15078.2) / 365.25); + mon = (int)((mjd - 14956.1 - (int)(year * 365.25)) / 30.6001); + day = mjd - 14956 - (int)(year * 365.25) - (int)(mon * 30.6001); + if (mon == 14 || mon == 15) { + year++; + mon -= 12; + } + mon--; + + /* Convert to UTC time_t */ + utc.tm_wday = 0; + utc.tm_yday = 0; + utc.tm_isdst = 0; + utc.tm_year = year; + utc.tm_mon = mon - 1; + utc.tm_mday = day; + utc.tm_hour = hour; + utc.tm_min = min; + utc.tm_sec = sec; + tvhtime_update(&utc); + + return 0; +} /** * Demux for default DVB tables that we want @@ -1138,25 +1153,19 @@ dvb_pmt_callback(th_dvb_mux_instance_t *tdmi, uint8_t *ptr, int len, static void dvb_table_add_default_dvb(th_dvb_mux_instance_t *tdmi) { - struct dmx_sct_filter_params *fp; - /* Network Information Table */ - fp = dvb_fparams_alloc(); - - if(tdmi->tdmi_adapter->tda_nitoid) { - fp->filter.filter[0] = 0x41; - } else { - fp->filter.filter[0] = 0x40; - } - fp->filter.mask[0] = 0xff; - tdt_add(tdmi, fp, dvb_nit_callback, NULL, "nit", - TDT_QUICKREQ | TDT_CRC, 0x10, NULL); + tdt_add(tdmi, 0, 0, dvb_nit_callback, NULL, "nit", + TDT_QUICKREQ | TDT_CRC, 0x10); /* Service Descriptor Table and Bouqeut Allocation Table */ - tdt_add(tdmi, NULL, dvb_pidx11_callback, NULL, "pidx11", - TDT_QUICKREQ | TDT_CRC, 0x11, NULL); + tdt_add(tdmi, 0, 0, dvb_pidx11_callback, NULL, "pidx11", + TDT_QUICKREQ | TDT_CRC, 0x11); + + /* Time Offset Table */ + + tdt_add(tdmi, 0, 0, dvb_tot_callback, NULL, "tot", TDT_CRC, 0x14); } @@ -1166,7 +1175,6 @@ dvb_table_add_default_dvb(th_dvb_mux_instance_t *tdmi) static void dvb_table_add_default_atsc(th_dvb_mux_instance_t *tdmi) { - struct dmx_sct_filter_params *fp; int tableid; if(tdmi->tdmi_conf.dmc_fe_params.u.vsb.modulation == VSB_8) { @@ -1175,12 +1183,8 @@ dvb_table_add_default_atsc(th_dvb_mux_instance_t *tdmi) tableid = 0xc9; // Cable } - /* Virtual Channel Table */ - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = tableid; - fp->filter.mask[0] = 0xff; - tdt_add(tdmi, fp, atsc_vct_callback, NULL, "vct", - TDT_QUICKREQ | TDT_CRC, 0x1ffb, NULL); + tdt_add(tdmi, tableid, 0xff, atsc_vct_callback, NULL, "vct", + TDT_QUICKREQ | TDT_CRC, 0x1ffb); } @@ -1192,23 +1196,13 @@ dvb_table_add_default_atsc(th_dvb_mux_instance_t *tdmi) void dvb_table_add_default(th_dvb_mux_instance_t *tdmi) { - struct dmx_sct_filter_params *fp; - /* Program Allocation Table */ - - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0x00; - fp->filter.mask[0] = 0xff; - tdt_add(tdmi, fp, dvb_pat_callback, NULL, "pat", - TDT_QUICKREQ | TDT_CRC, 0, NULL); + tdt_add(tdmi, 0x00, 0xff, dvb_pat_callback, NULL, "pat", + TDT_QUICKREQ | TDT_CRC, 0); /* Conditional Access Table */ - - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0x1; - fp->filter.mask[0] = 0xff; - tdt_add(tdmi, fp, dvb_cat_callback, NULL, "cat", - TDT_CRC, 1, NULL); + tdt_add(tdmi, 0x1, 0xff, dvb_cat_callback, NULL, "cat", + TDT_CRC, 1); switch(tdmi->tdmi_adapter->tda_type) { @@ -1231,15 +1225,11 @@ dvb_table_add_default(th_dvb_mux_instance_t *tdmi) void dvb_table_add_pmt(th_dvb_mux_instance_t *tdmi, int pmt_pid) { - struct dmx_sct_filter_params *fp; char pmtname[100]; snprintf(pmtname, sizeof(pmtname), "PMT(%d)", pmt_pid); - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0x02; - fp->filter.mask[0] = 0xff; - tdt_add(tdmi, fp, dvb_pmt_callback, NULL, pmtname, - TDT_CRC | TDT_QUICKREQ | TDT_TDT, pmt_pid, NULL); + tdt_add(tdmi, 0x2, 0xff, dvb_pmt_callback, NULL, pmtname, + TDT_CRC | TDT_QUICKREQ | TDT_TDT, pmt_pid); } void diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 327b7b02..59d285cc 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -65,6 +65,7 @@ extern struct dvr_entry_list dvrentries; #define DVR_EPISODE_IN_TITLE 0x80 #define DVR_CLEAN_TITLE 0x100 #define DVR_TAG_FILES 0x200 +#define DVR_SKIP_COMMERCIALS 0x400 typedef enum { DVR_PRIO_IMPORTANT, @@ -113,6 +114,8 @@ typedef struct dvr_entry { channel_t *de_channel; LIST_ENTRY(dvr_entry) de_channel_link; + char *de_channel_name; + gtimer_t de_timer; /** @@ -134,6 +137,8 @@ typedef struct dvr_entry { lang_str_t *de_desc; /* Description in UTF-8 (from EPG) */ epg_genre_t de_content_type; /* Content type (from EPG) */ + uint16_t de_dvb_eid; + dvr_prio_t de_pri; uint32_t de_dont_reschedule; @@ -188,8 +193,16 @@ typedef struct dvr_entry { struct muxer *de_mux; + /** + * Inotify + */ +#if ENABLE_INOTIFY + LIST_ENTRY(dvr_entry) de_inotify_link; +#endif + } dvr_entry_t; +#define DVR_CH_NAME(e) ((e)->de_channel == NULL ? (e)->de_channel_name : (e)-> de_channel->ch_name) /** * Autorec entry @@ -247,6 +260,8 @@ void dvr_config_delete(const char *name); void dvr_entry_notify(dvr_entry_t *de); +void dvr_entry_save(dvr_entry_t *de); + const char *dvr_entry_status(dvr_entry_t *de); const char *dvr_entry_schedstatus(dvr_entry_t *de); @@ -280,6 +295,8 @@ void dvr_init(void); void dvr_autorec_init(void); +void dvr_autorec_update(void); + void dvr_destroy_by_channel(channel_t *ch); void dvr_rec_subscribe(dvr_entry_t *de); @@ -298,7 +315,7 @@ dvr_entry_t *dvr_entry_find_by_event_fuzzy(epg_broadcast_t *e); dvr_entry_t *dvr_entry_find_by_episode(epg_broadcast_t *e); -off_t dvr_get_filesize(dvr_entry_t *de); +int64_t dvr_get_filesize(dvr_entry_t *de); dvr_entry_t *dvr_entry_cancel(dvr_entry_t *de); @@ -331,10 +348,19 @@ typedef struct dvr_query_result { int dqr_alloced; } dvr_query_result_t; +typedef int (dvr_entry_filter)(dvr_entry_t *entry); +typedef int (dvr_entry_comparator)(const void *a, const void *b); + void dvr_query(dvr_query_result_t *dqr); +void dvr_query_filter(dvr_query_result_t *dqr, dvr_entry_filter filter); void dvr_query_free(dvr_query_result_t *dqr); + +void dvr_query_sort_cmp(dvr_query_result_t *dqr, dvr_entry_comparator cmp); void dvr_query_sort(dvr_query_result_t *dqr); +int dvr_sort_start_descending(const void *A, const void *B); +int dvr_sort_start_ascending(const void *A, const void *B); + /** * */ @@ -364,4 +390,11 @@ dvr_prio_t dvr_pri2val(const char *s); const char *dvr_val2pri(dvr_prio_t v); +/** + * Inotify support + */ +void dvr_inotify_init ( void ); +void dvr_inotify_add ( dvr_entry_t *de ); +void dvr_inotify_del ( dvr_entry_t *de ); + #endif /* DVR_H */ diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index 447cbcba..09cd228f 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -38,12 +38,14 @@ dtable_t *autorec_dt; TAILQ_HEAD(dvr_autorec_entry_queue, dvr_autorec_entry); +static int dvr_autorec_in_init = 0; + struct dvr_autorec_entry_queue autorec_entries; -static void dvr_autorec_changed(dvr_autorec_entry_t *dae); +static void dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge); /** - * + * Unlink - and remove any unstarted */ static void dvr_autorec_purge_spawns(dvr_autorec_entry_t *dae) @@ -53,7 +55,10 @@ dvr_autorec_purge_spawns(dvr_autorec_entry_t *dae) while((de = LIST_FIRST(&dae->dae_spawns)) != NULL) { LIST_REMOVE(de, de_autorec_link); de->de_autorec = NULL; - dvr_entry_cancel(de); + if (de->de_sched_state == DVR_SCHEDULED) + dvr_entry_cancel(de); + else + dvr_entry_save(de); } } @@ -385,7 +390,8 @@ autorec_record_update(void *opaque, const char *id, htsmsg_t *values, } } - dae->dae_content_type.code = htsmsg_get_u32_or_default(values, "contenttype", 0); + if (!htsmsg_get_u32(values, "contenttype", &u32)) + dae->dae_content_type.code = u32; if((s = htsmsg_get_str(values, "approx_time")) != NULL) { if(strchr(s, ':') != NULL) { @@ -422,7 +428,8 @@ autorec_record_update(void *opaque, const char *id, htsmsg_t *values, if (dae->dae_serieslink) dae->dae_serieslink->getref(dae->dae_serieslink); } - dvr_autorec_changed(dae); + if (!dvr_autorec_in_init) + dvr_autorec_changed(dae, 1); return autorec_record_build(dae); } @@ -465,7 +472,18 @@ dvr_autorec_init(void) { TAILQ_INIT(&autorec_entries); autorec_dt = dtable_create(&autorec_dtc, "autorec", NULL); + dvr_autorec_in_init = 1; dtable_load(autorec_dt); + dvr_autorec_in_init = 0; +} + +void +dvr_autorec_update(void) +{ + dvr_autorec_entry_t *dae; + TAILQ_FOREACH(dae, &autorec_entries, dae_link) { + dvr_autorec_changed(dae, 0); + } } static void @@ -520,12 +538,10 @@ _dvr_autorec_add(const char *config_name, htsmsg_destroy(m); /* Notify web clients that we have messed with the tables */ - - m = htsmsg_create_map(); - htsmsg_add_u32(m, "reload", 1); - notify_by_msg("autorec", m); - dvr_autorec_changed(dae); + notify_reload("autorec"); + + dvr_autorec_changed(dae, 1); } void @@ -544,9 +560,11 @@ void dvr_autorec_add_series_link ( const char *dvr_config_name, epg_broadcast_t *event, const char *creator, const char *comment ) { + char *title; if (!event || !event->episode) return; + title = regexp_escape(epg_broadcast_get_title(event, NULL)); _dvr_autorec_add(dvr_config_name, - epg_broadcast_get_title(event, NULL), + title, event->channel, NULL, 0, // tag/content type NULL, @@ -554,6 +572,8 @@ void dvr_autorec_add_series_link event->serieslink, 0, NULL, creator, comment); + if (title) + free(title); } @@ -564,14 +584,10 @@ void dvr_autorec_check_event(epg_broadcast_t *e) { dvr_autorec_entry_t *dae; - dvr_entry_t *existingde; TAILQ_FOREACH(dae, &autorec_entries, dae_link) - if(autorec_cmp(dae, e)) { - existingde = dvr_entry_find_by_event_fuzzy(e); - if (existingde == NULL) - dvr_entry_create_by_autorec(e, dae); - } + if(autorec_cmp(dae, e)) + dvr_entry_create_by_autorec(e, dae); // Note: no longer updating event here as it will be done from EPG // anyway } @@ -598,12 +614,13 @@ void dvr_autorec_check_serieslink(epg_serieslink_t *s) * */ static void -dvr_autorec_changed(dvr_autorec_entry_t *dae) +dvr_autorec_changed(dvr_autorec_entry_t *dae, int purge) { channel_t *ch; epg_broadcast_t *e; - dvr_autorec_purge_spawns(dae); + if (purge) + dvr_autorec_purge_spawns(dae); RB_FOREACH(ch, &channel_name_tree, ch_name_link) { RB_FOREACH(e, &ch->ch_epg_schedule, sched_link) { diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 1edb2bc9..d6a1562c 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -27,7 +27,7 @@ #include "tvheadend.h" #include "dvr.h" #include "notify.h" -#include "htsp.h" +#include "htsp_server.h" #include "streaming.h" static int de_tally; @@ -37,11 +37,21 @@ int dvr_iov_max; struct dvr_config_list dvrconfigs; struct dvr_entry_list dvrentries; -static void dvr_entry_save(dvr_entry_t *de); - static void dvr_timer_expire(void *aux); static void dvr_timer_start_recording(void *aux); +/* + * Completed + */ +static void +_dvr_entry_completed(dvr_entry_t *de) +{ + de->de_sched_state = DVR_COMPLETED; +#if ENABLE_INOTIFY + dvr_inotify_add(de); +#endif +} + /** * Return printable status for a dvr entry */ @@ -70,6 +80,8 @@ dvr_entry_status(dvr_entry_t *de) } case DVR_COMPLETED: + if(dvr_get_filesize(de) == -1) + return "File Missing"; if(de->de_last_error) return streaming_code2txt(de->de_last_error); else @@ -99,7 +111,7 @@ dvr_entry_schedstatus(dvr_entry_t *de) else return "recording"; case DVR_COMPLETED: - if(de->de_last_error) + if(de->de_last_error || dvr_get_filesize(de) == -1) return "completedError"; else return "completed"; @@ -162,7 +174,7 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); if(cfg->dvr_flags & DVR_CHANNEL_IN_TITLE) - snprintf(output, outlen, "%s-", de->de_channel->ch_name); + snprintf(output, outlen, "%s-", DVR_CH_NAME(de)); else output[0] = 0; @@ -203,19 +215,12 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) } } -/** - * - */ static void -dvr_entry_link(dvr_entry_t *de) +dvr_entry_set_timer(dvr_entry_t *de) { time_t now, preamble; dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - de->de_refcnt = 1; - - LIST_INSERT_HEAD(&dvrentries, de, de_global_link); - time(&now); preamble = de->de_start - (60 * de->de_start_extra) - 30; @@ -224,18 +229,68 @@ dvr_entry_link(dvr_entry_t *de) if(de->de_filename == NULL) de->de_sched_state = DVR_MISSED_TIME; else - de->de_sched_state = DVR_COMPLETED; + _dvr_entry_completed(de); gtimer_arm_abs(&de->de_timer, dvr_timer_expire, de, de->de_stop + cfg->dvr_retention_days * 86400); - } else { + } else if (de->de_channel) { de->de_sched_state = DVR_SCHEDULED; + tvhtrace("dvr", "entry timer scheduled for %"PRItime_t, preamble); gtimer_arm_abs(&de->de_timer, dvr_timer_start_recording, de, preamble); + } else { + de->de_sched_state = DVR_NOSTATE; } +} + +/** + * + */ +static void +dvr_entry_link(dvr_entry_t *de) +{ + de->de_refcnt = 1; + + LIST_INSERT_HEAD(&dvrentries, de, de_global_link); + + dvr_entry_set_timer(de); + htsp_dvr_entry_add(de); } +/** + * Find dvr entry using 'fuzzy' search + */ +static int +dvr_entry_fuzzy_match(dvr_entry_t *de, epg_broadcast_t *e) +{ + time_t t1, t2; + const char *title1, *title2; + + /* Matching ID */ + if (de->de_dvb_eid && de->de_dvb_eid == e->dvb_eid) + return 1; + + /* No title */ + if (!(title1 = epg_broadcast_get_title(e, NULL))) + return 0; + if (!(title2 = lang_str_get(de->de_title, NULL))) + return 0; + + /* Wrong length (+/-20%) */ + t1 = de->de_stop - de->de_start; + t2 = e->stop - e->start; + if ( abs(t2 - t1) > (t1 / 5) ) + return 0; + + /* Outside of window (should it be configurable)? */ + if ( abs(e->start - de->de_start) > 86400 ) + return 0; + + /* Title match (or contains?) */ + return strcmp(title1, title2) == 0; +} + /** * Create the event */ @@ -258,19 +313,6 @@ static dvr_entry_t *_dvr_entry_create ( if(de->de_start == start && de->de_sched_state != DVR_COMPLETED) return NULL; - /* Reject duplicate episodes (unless earlier) */ - if (e && cfg->dvr_dup_detect_episode) { - de = dvr_entry_find_by_episode(e); - if (de) { - if (de->de_start > start) { - dvr_event_replaced(de->de_bcast, e); - return de; - } else { - return NULL; - } - } - } - de = calloc(1, sizeof(dvr_entry_t)); de->de_id = ++de_tally; @@ -300,6 +342,7 @@ static dvr_entry_t *_dvr_entry_create ( de->de_desc = NULL; // TODO: this really needs updating if (e) { + de->de_dvb_eid = e->dvb_eid; if (e->episode && e->episode->title) de->de_title = lang_str_copy(e->episode->title); if (e->description) @@ -334,9 +377,10 @@ static dvr_entry_t *_dvr_entry_create ( LIST_INSERT_HEAD(&dae->dae_spawns, de, de_autorec_link); } - tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\" starting at %s, " + tvhlog(LOG_INFO, "dvr", "entry %d \"%s\" on \"%s\" starting at %s, " "scheduled for recording by \"%s\"", - lang_str_get(de->de_title, NULL), de->de_channel->ch_name, tbuf, creator); + de->de_id, + lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), tbuf, creator); dvrdb_changed(); dvr_entry_save(de); @@ -449,12 +493,18 @@ dvr_entry_remove(dvr_entry_t *de) hts_settings_remove("dvr/log/%d", de->de_id); htsp_dvr_entry_delete(de); + +#if ENABLE_INOTIFY + dvr_inotify_del(de); +#endif gtimer_disarm(&de->de_timer); - LIST_REMOVE(de, de_channel_link); + if (de->de_channel) + LIST_REMOVE(de, de_channel_link); LIST_REMOVE(de, de_global_link); de->de_channel = NULL; + free(de->de_channel_name); dvrdb_changed(); @@ -469,9 +519,9 @@ static void dvr_db_load_one(htsmsg_t *c, int id) { dvr_entry_t *de; - const char *s, *creator; + const char *chname, *s, *creator; channel_t *ch; - uint32_t start, stop, bcid; + uint32_t start, stop, bcid, u32; int d; dvr_config_t *cfg; lang_str_t *title, *ls; @@ -481,11 +531,10 @@ dvr_db_load_one(htsmsg_t *c, int id) if(htsmsg_get_u32(c, "stop", &stop)) return; - if((s = htsmsg_get_str(c, "channel")) == NULL) + if((chname = htsmsg_get_str(c, "channel")) == NULL) return; - if((ch = channel_find_by_name(s, 0, 0)) == NULL) - return; - + ch = channel_find_by_name(chname, 0, 0); + s = htsmsg_get_str(c, "config_name"); cfg = dvr_config_find_by_name_default(s); @@ -500,8 +549,12 @@ dvr_db_load_one(htsmsg_t *c, int id) de_tally = MAX(id, de_tally); - de->de_channel = ch; - LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link); + if (ch) { + de->de_channel = ch; + LIST_INSERT_HEAD(&de->de_channel->ch_dvrs, de, de_channel_link); + } else { + de->de_channel_name = strdup(chname); + } de->de_start = start; de->de_stop = stop; @@ -509,9 +562,11 @@ dvr_db_load_one(htsmsg_t *c, int id) de->de_creator = strdup(creator); de->de_title = title; de->de_pri = dvr_pri2val(htsmsg_get_str(c, "pri")); + if (!htsmsg_get_u32(c, "dvb_eid", &u32)) + de->de_dvb_eid = (uint16_t)u32; if(htsmsg_get_s32(c, "start_extra", &d)) - if (ch->ch_dvr_extra_time_pre) + if (ch && ch->ch_dvr_extra_time_pre) de->de_start_extra = ch->ch_dvr_extra_time_pre; else de->de_start_extra = cfg->dvr_extra_time_pre; @@ -519,7 +574,7 @@ dvr_db_load_one(htsmsg_t *c, int id) de->de_start_extra = d; if(htsmsg_get_s32(c, "stop_extra", &d)) - if (ch->ch_dvr_extra_time_post) + if (ch && ch->ch_dvr_extra_time_post) de->de_stop_extra = ch->ch_dvr_extra_time_post; else de->de_stop_extra = cfg->dvr_extra_time_post; @@ -586,14 +641,14 @@ dvr_db_load(void) /** * */ -static void +void dvr_entry_save(dvr_entry_t *de) { htsmsg_t *m = htsmsg_create_map(); lock_assert(&global_lock); - htsmsg_add_str(m, "channel", de->de_channel->ch_name); + htsmsg_add_str(m, "channel", DVR_CH_NAME(de)); htsmsg_add_u32(m, "start", de->de_start); htsmsg_add_u32(m, "stop", de->de_stop); @@ -609,6 +664,9 @@ dvr_entry_save(dvr_entry_t *de) lang_str_serialize(de->de_title, m, "title"); + if(de->de_dvb_eid) + htsmsg_add_u32(m, "dvb_eid", de->de_dvb_eid); + if(de->de_desc != NULL) lang_str_serialize(de->de_desc, m, "description"); @@ -677,6 +735,8 @@ static dvr_entry_t *_dvr_entry_update de->de_stop_extra = stop_extra; save = 1; } + if (save) + dvr_entry_set_timer(de); /* Title */ if (e && e->episode && e->episode->title) { @@ -686,6 +746,12 @@ static dvr_entry_t *_dvr_entry_update if (!de->de_title) de->de_title = lang_str_create(); save = lang_str_add(de->de_title, title, lang, 1); } + + /* EID */ + if (e && e->dvb_eid != de->de_dvb_eid) { + de->de_dvb_eid = e->dvb_eid; + save = 1; + } // TODO: description @@ -700,7 +766,8 @@ static dvr_entry_t *_dvr_entry_update /* Broadcast */ if (e && (de->de_bcast != e)) { - de->de_bcast->putref(de->de_bcast); + if (de->de_bcast) + de->de_bcast->putref(de->de_bcast); de->de_bcast = e; e->getref(e); save = 1; @@ -712,7 +779,7 @@ static dvr_entry_t *_dvr_entry_update htsp_dvr_entry_update(de); dvr_entry_notify(de); tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": Updated Timer", - lang_str_get(de->de_title, NULL), de->de_channel->ch_name); + lang_str_get(de->de_title, NULL), DVR_CH_NAME(de)); } return de; @@ -734,23 +801,53 @@ dvr_entry_update /** * Used to notify the DVR that an event has been replaced in the EPG - * - * TODO: I think this will record the title slot event if its now a - * completely different episode etc... */ void dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e) { - dvr_entry_t *de, *ude; + dvr_entry_t *de; + assert(e != NULL); + assert(new_e != NULL); + + /* Ignore */ if ( e == new_e ) return; - de = dvr_entry_find_by_event(e); - if (de != NULL) { - ude = dvr_entry_find_by_event_fuzzy(new_e); - if (ude == NULL && de->de_sched_state == DVR_SCHEDULED) - dvr_entry_cancel(de); - else if(new_e->episode && new_e->episode->title) - _dvr_entry_update(de, new_e, NULL, NULL, NULL, 0, 0, 0, 0); + /* Existing entry */ + if ((de = dvr_entry_find_by_event(e))) { + tvhtrace("dvr", + "dvr entry %d event replaced %s on %s @ %"PRItime_t + " to %"PRItime_t, + de->de_id, epg_broadcast_get_title(e, NULL), e->channel->ch_name, + e->start, e->stop); + + /* Ignore - already in progress */ + if (de->de_sched_state != DVR_SCHEDULED) + return; + + /* Unlink the broadcast */ + e->putref(e); + de->de_bcast = NULL; + + /* If this was craeted by autorec - just remove it, it'll get recreated */ + if (de->de_autorec) { + dvr_entry_remove(de); + + /* Find match */ + } else { + RB_FOREACH(e, &e->channel->ch_epg_schedule, sched_link) { + if (dvr_entry_fuzzy_match(de, e)) { + tvhtrace("dvr", + " replacement event %s on %s @ %"PRItime_t + " to %"PRItime_t, + epg_broadcast_get_title(e, NULL), e->channel->ch_name, + e->start, e->stop); + e->getref(e); + de->de_bcast = e; + _dvr_entry_update(de, e, NULL, NULL, NULL, 0, 0, 0, 0); + break; + } + } + } } } @@ -758,7 +855,27 @@ void dvr_event_updated ( epg_broadcast_t *e ) { dvr_entry_t *de; de = dvr_entry_find_by_event(e); - if (de) _dvr_entry_update(de, e, NULL, NULL, NULL, 0, 0, 0, 0); + if (de) + _dvr_entry_update(de, e, NULL, NULL, NULL, 0, 0, 0, 0); + else { + LIST_FOREACH(de, &dvrentries, de_global_link) { + if (de->de_sched_state != DVR_SCHEDULED) continue; + if (de->de_bcast) continue; + if (de->de_channel != e->channel) continue; + if (dvr_entry_fuzzy_match(de, e)) { + tvhtrace("dvr", + "dvr entry %d link to event %s on %s @ %"PRItime_t + " to %"PRItime_t, + de->de_id, epg_broadcast_get_title(e, NULL), + e->channel->ch_name, + e->start, e->stop); + e->getref(e); + de->de_bcast = e; + _dvr_entry_update(de, e, NULL, NULL, NULL, 0, 0, 0, 0); + break; + } + } + } } /** @@ -772,13 +889,13 @@ dvr_stop_recording(dvr_entry_t *de, int stopcode) if (de->de_rec_state == DVR_RS_PENDING || de->de_rec_state == DVR_RS_WAIT_PROGRAM_START) de->de_sched_state = DVR_MISSED_TIME; else - de->de_sched_state = DVR_COMPLETED; + _dvr_entry_completed(de); dvr_rec_unsubscribe(de, stopcode); tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": " "End of program: %s", - lang_str_get(de->de_title, NULL), de->de_channel->ch_name, + lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), dvr_entry_status(de)); dvr_entry_save(de); @@ -813,7 +930,7 @@ dvr_timer_start_recording(void *aux) de->de_rec_state = DVR_RS_PENDING; tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\" recorder starting", - lang_str_get(de->de_title, NULL), de->de_channel->ch_name); + lang_str_get(de->de_title, NULL), DVR_CH_NAME(de)); dvr_entry_notify(de); htsp_dvr_entry_update(de); @@ -846,29 +963,13 @@ dvr_entry_find_by_event(epg_broadcast_t *e) { dvr_entry_t *de; + if(!e->channel) return NULL; + LIST_FOREACH(de, &e->channel->ch_dvrs, de_channel_link) if(de->de_bcast == e) return de; return NULL; } -/** - * Find dvr entry using 'fuzzy' search - */ -dvr_entry_t * -dvr_entry_find_by_event_fuzzy(epg_broadcast_t *e) -{ - dvr_entry_t *de; - - if (!e->episode || !e->episode->title) - return NULL; - - LIST_FOREACH(de, &e->channel->ch_dvrs, de_channel_link) - if ((abs(de->de_start - e->start) < 600) && (abs(de->de_stop - e->stop) < 600)) { - return de; - } - return NULL; -} - /* * Find DVR entry based on an episode */ @@ -926,8 +1027,6 @@ dvr_entry_purge(dvr_entry_t *de) { if(de->de_sched_state == DVR_RECORDING) dvr_stop_recording(de, SM_CODE_SOURCE_DELETED); - - dvr_entry_remove(de); } /** @@ -938,8 +1037,12 @@ dvr_destroy_by_channel(channel_t *ch) { dvr_entry_t *de; - while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) + while((de = LIST_FIRST(&ch->ch_dvrs)) != NULL) { + LIST_REMOVE(de, de_channel_link); + de->de_channel = NULL; + de->de_channel_name = strdup(ch->ch_name); dvr_entry_purge(de); + } } /** @@ -1015,6 +1118,9 @@ dvr_init(void) if(!htsmsg_get_u32(m, "tag-files", &u32) && !u32) cfg->dvr_flags &= ~DVR_TAG_FILES; + if(!htsmsg_get_u32(m, "skip-commercials", &u32) && !u32) + cfg->dvr_flags &= ~DVR_SKIP_COMMERCIALS; + tvh_str_set(&cfg->dvr_postproc, htsmsg_get_str(m, "postproc")); } @@ -1047,9 +1153,12 @@ dvr_init(void) } } - dvr_db_load(); - +#if ENABLE_INOTIFY + dvr_inotify_init(); +#endif dvr_autorec_init(); + dvr_db_load(); + dvr_autorec_update(); } /** @@ -1110,7 +1219,7 @@ dvr_config_create(const char *name) cfg->dvr_config_name = strdup(name); cfg->dvr_retention_days = 31; cfg->dvr_mc = MC_MATROSKA; - cfg->dvr_flags = DVR_TAG_FILES; + cfg->dvr_flags = DVR_TAG_FILES | DVR_SKIP_COMMERCIALS; /* series link support */ cfg->dvr_sl_brand_lock = 1; // use brand linking @@ -1175,6 +1284,7 @@ dvr_save(dvr_config_t *cfg) htsmsg_add_u32(m, "episode-in-title", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE)); htsmsg_add_u32(m, "clean-title", !!(cfg->dvr_flags & DVR_CLEAN_TITLE)); htsmsg_add_u32(m, "tag-files", !!(cfg->dvr_flags & DVR_TAG_FILES)); + htsmsg_add_u32(m, "skip-commercials", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS)); if(cfg->dvr_postproc != NULL) htsmsg_add_str(m, "postproc", cfg->dvr_postproc); @@ -1312,6 +1422,22 @@ dvr_query_add_entry(dvr_query_result_t *dqr, dvr_entry_t *de) dqr->dqr_array[dqr->dqr_entries++] = de; } +void +dvr_query_filter(dvr_query_result_t *dqr, dvr_entry_filter filter) +{ + dvr_entry_t *de; + + memset(dqr, 0, sizeof(dvr_query_result_t)); + + LIST_FOREACH(de, &dvrentries, de_global_link) + if (filter(de)) + dvr_query_add_entry(dqr, de); +} + +static int all_filter(dvr_entry_t *entry) +{ + return 1; +} /** * @@ -1319,15 +1445,9 @@ dvr_query_add_entry(dvr_query_result_t *dqr, dvr_entry_t *de) void dvr_query(dvr_query_result_t *dqr) { - dvr_entry_t *de; - - memset(dqr, 0, sizeof(dvr_query_result_t)); - - LIST_FOREACH(de, &dvrentries, de_global_link) - dvr_query_add_entry(dqr, de); + return dvr_query_filter(dqr, all_filter); } - /** * */ @@ -1340,7 +1460,7 @@ dvr_query_free(dvr_query_result_t *dqr) /** * Sorting functions */ -static int +int dvr_sort_start_descending(const void *A, const void *B) { dvr_entry_t *a = *(dvr_entry_t **)A; @@ -1348,36 +1468,44 @@ dvr_sort_start_descending(const void *A, const void *B) return b->de_start - a->de_start; } +int +dvr_sort_start_ascending(const void *A, const void *B) +{ + return -dvr_sort_start_descending(A, B); +} + /** * */ void -dvr_query_sort(dvr_query_result_t *dqr) +dvr_query_sort_cmp(dvr_query_result_t *dqr, dvr_entry_comparator sf) { - int (*sf)(const void *a, const void *b); - if(dqr->dqr_array == NULL) return; - sf = dvr_sort_start_descending; qsort(dqr->dqr_array, dqr->dqr_entries, sizeof(dvr_entry_t *), sf); } +void +dvr_query_sort(dvr_query_result_t *dqr) +{ + dvr_query_sort_cmp(dqr, dvr_sort_start_descending); +} /** * */ -off_t +int64_t dvr_get_filesize(dvr_entry_t *de) { struct stat st; if(de->de_filename == NULL) - return 0; + return -1; if(stat(de->de_filename, &st) != 0) - return 0; + return -1; return st.st_size; } @@ -1415,6 +1543,9 @@ void dvr_entry_delete(dvr_entry_t *de) { if(de->de_filename != NULL) { +#if ENABLE_INOTIFY + dvr_inotify_del(de); +#endif if(unlink(de->de_filename) && errno != ENOENT) tvhlog(LOG_WARNING, "dvr", "Unable to remove file '%s' from disk -- %s", de->de_filename, strerror(errno)); diff --git a/src/dvr/dvr_inotify.c b/src/dvr/dvr_inotify.c new file mode 100644 index 00000000..7d6f177c --- /dev/null +++ b/src/dvr/dvr_inotify.c @@ -0,0 +1,305 @@ +/* + * Digital Video Recorder - inotify processing + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvheadend.h" +#include "redblack.h" +#include "dvr/dvr.h" +#include "htsp_server.h" + +/* inotify limits */ +#define EVENT_SIZE ( sizeof (struct inotify_event) ) +#define EVENT_BUF_LEN ( 10 * ( EVENT_SIZE + 16 ) ) +#define EVENT_MASK IN_CREATE | IN_DELETE | IN_DELETE_SELF |\ + IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO + +static int _inot_fd; +static RB_HEAD(,dvr_inotify_entry) _inot_tree; + +typedef struct dvr_inotify_entry +{ + RB_ENTRY(dvr_inotify_entry) link; + char *path; + int fd; + struct dvr_entry_list entries; +} dvr_inotify_entry_t; + +static void* _dvr_inotify_thread ( void *p ); + +static int _str_cmp ( void *a, void *b ) +{ + return strcmp(((dvr_inotify_entry_t*)a)->path, ((dvr_inotify_entry_t*)b)->path); +} + +/** + * Initialise threads + */ +void dvr_inotify_init ( void ) +{ + pthread_t tid; + + _inot_fd = inotify_init(); + if (_inot_fd == -1) { + tvhlog(LOG_ERR, "dvr", "failed to initialise inotify (err=%s)", + strerror(errno)); + return; + } + + pthread_create(&tid, NULL, _dvr_inotify_thread, NULL); +} + +/** + * Add an entry for monitoring + */ +void dvr_inotify_add ( dvr_entry_t *de ) +{ + static dvr_inotify_entry_t *skel = NULL; + dvr_inotify_entry_t *e; + char *path; + struct stat st; + + if (_inot_fd == -1) + return; + + if (!de->de_filename || stat(de->de_filename, &st)) + return; + + path = strdup(de->de_filename); + + if (!skel) + skel = calloc(1, sizeof(dvr_inotify_entry_t)); + skel->path = dirname(path); + + if (stat(skel->path, &st)) + return; + + e = RB_INSERT_SORTED(&_inot_tree, skel, link, _str_cmp); + if (!e) { + e = skel; + skel = NULL; + e->path = strdup(e->path); + e->fd = inotify_add_watch(_inot_fd, e->path, EVENT_MASK); + if (e->fd == -1) { + tvhlog(LOG_ERR, "dvr", "failed to add inotify watch to %s (err=%s)", + e->path, strerror(errno)); + free(path); + dvr_inotify_del(de); + return; + } + } + + LIST_INSERT_HEAD(&e->entries, de, de_inotify_link); + + free(path); +} + +/* + * Delete an entry from the monitor + */ +void dvr_inotify_del ( dvr_entry_t *de ) +{ + dvr_entry_t *det; + dvr_inotify_entry_t *e; + RB_FOREACH(e, &_inot_tree, link) { + LIST_FOREACH(det, &e->entries, de_inotify_link) + if (det == de) break; + if (det) break; + } + + if (e && det) { + LIST_REMOVE(det, de_inotify_link); + if (LIST_FIRST(&e->entries) == NULL) { + RB_REMOVE(&_inot_tree, e, link); + inotify_rm_watch(_inot_fd, e->fd); + free(e->path); + free(e); + } + } +} + +/* + * Find inotify entry + */ +static dvr_inotify_entry_t * +_dvr_inotify_find + ( int fd ) +{ + dvr_inotify_entry_t *e = NULL; + RB_FOREACH(e, &_inot_tree, link) + if (e->fd == fd) + break; + return e; +} + +/* + * Find DVR entry + */ +static dvr_entry_t * +_dvr_inotify_find2 + ( dvr_inotify_entry_t *die, const char *name ) +{ + dvr_entry_t *de = NULL; + char path[512]; + + snprintf(path, sizeof(path), "%s/%s", die->path, name); + + LIST_FOREACH(de, &die->entries, de_inotify_link) + if (!strcmp(path, de->de_filename)) + break; + + return de; +} + +/* + * File moved + */ +static void +_dvr_inotify_moved + ( int fd, const char *from, const char *to ) +{ + dvr_inotify_entry_t *die; + dvr_entry_t *de; + + if (!(die = _dvr_inotify_find(fd))) + return; + + if (!(de = _dvr_inotify_find2(die, from))) + return; + + if (to) { + char path[512]; + snprintf(path, sizeof(path), "%s/%s", die->path, to); + tvh_str_update(&de->de_filename, path); + dvr_entry_save(de); + } else + dvr_inotify_del(de); + + htsp_dvr_entry_update(de); + dvr_entry_notify(de); +} + +/* + * File deleted + */ +static void +_dvr_inotify_delete + ( int fd, const char *path ) +{ + _dvr_inotify_moved(fd, path, NULL); +} + +/* + * Directory moved + */ +static void +_dvr_inotify_moved_all + ( int fd, const char *to ) +{ + dvr_entry_t *de; + dvr_inotify_entry_t *die; + + if (!(die = _dvr_inotify_find(fd))) + return; + + while ((de = LIST_FIRST(&die->entries))) { + htsp_dvr_entry_update(de); + dvr_entry_notify(de); + dvr_inotify_del(de); + } +} + +/* + * Directory deleted + */ +static void +_dvr_inotify_delete_all + ( int fd ) +{ + _dvr_inotify_moved_all(fd, NULL); +} + +/* + * Process events + */ +void* _dvr_inotify_thread ( void *p ) +{ + int i, len; + char buf[EVENT_BUF_LEN]; + const char *from; + int fromfd; + int cookie; + + while (1) { + + /* Read events */ + fromfd = 0; + cookie = 0; + from = NULL; + i = 0; + len = read(_inot_fd, buf, EVENT_BUF_LEN); + + /* Process */ + pthread_mutex_lock(&global_lock); + while ( i < len ) { + struct inotify_event *ev = (struct inotify_event*)&buf[i]; + i += EVENT_SIZE + ev->len; + + /* Moved */ + if (ev->mask & IN_MOVED_FROM) { + from = ev->name; + fromfd = ev->wd; + cookie = ev->cookie; + continue; + + } else if ((ev->mask & IN_MOVED_TO) && from && ev->cookie == cookie) { + _dvr_inotify_moved(ev->wd, from, ev->name); + from = NULL; + + /* Removed */ + } else if (ev->mask & IN_DELETE) { + _dvr_inotify_delete(ev->wd, ev->name); + + /* Moved self */ + } else if (ev->mask & IN_MOVE_SELF) { + _dvr_inotify_moved_all(ev->wd, NULL); + + /* Removed self */ + } else if (ev->mask & IN_DELETE_SELF) { + _dvr_inotify_delete_all(ev->wd); + } + + if (from) { + _dvr_inotify_moved(fromfd, from, NULL); + from = NULL; + cookie = 0; + } + } + if (from) + _dvr_inotify_moved(fromfd, from, NULL); + pthread_mutex_unlock(&global_lock); + } + + return NULL; +} + diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c old mode 100755 new mode 100644 index 9ba9e103..c48eafff --- a/src/dvr/dvr_rec.c +++ b/src/dvr/dvr_rec.c @@ -32,6 +32,7 @@ #include "service.h" #include "plumbing/tsfix.h" #include "plumbing/globalheaders.h" +#include "htsp_server.h" #include "muxer.h" @@ -80,13 +81,14 @@ dvr_rec_subscribe(dvr_entry_t *de) } else { streaming_queue_init(&de->de_sq, 0); de->de_gh = globalheaders_create(&de->de_sq.sq_st); - de->de_tsfix = tsfix_create(de->de_gh); - st = de->de_tsfix; + st = de->de_tsfix = tsfix_create(de->de_gh); + tsfix_set_start_time(de->de_tsfix, de->de_start - (60 * de->de_start_extra)); flags = 0; } de->de_s = subscription_create_from_channel(de->de_channel, weight, - buf, st, flags); + buf, st, flags, + NULL, NULL, NULL); pthread_create(&de->de_thread, NULL, dvr_thread, de); } @@ -116,55 +118,6 @@ dvr_rec_unsubscribe(dvr_entry_t *de, int stopcode) } -/** - * - */ -static int -makedirs(const char *path) -{ - struct stat st; - char *p; - int l, r; - - if(stat(path, &st) == 0 && S_ISDIR(st.st_mode)) - return 0; /* Dir already there */ - - if(mkdir(path, 0777) == 0) - return 0; /* Dir created ok */ - - if(errno == ENOENT) { - - /* Parent does not exist, try to create it */ - /* Allocate new path buffer and strip off last directory component */ - - l = strlen(path); - p = alloca(l + 1); - memcpy(p, path, l); - p[l--] = 0; - - for(; l >= 0; l--) - if(p[l] == '/') - break; - if(l == 0) { - return ENOENT; - } - p[l] = 0; - - if((r = makedirs(p)) != 0) - return r; - - /* Try again */ - if(mkdir(path, 0777) == 0) - return 0; /* Dir created ok */ - } - r = errno; - - tvhlog(LOG_ERR, "dvr", "Unable to create directory \"%s\" -- %s", - path, strerror(r)); - return r; -} - - /** * Replace various chars with a dash */ @@ -173,12 +126,18 @@ cleanupfilename(char *s, int dvr_flags) { int i, len = strlen(s); for(i = 0; i < len; i++) { - if(s[i] == '/' || s[i] == ':' || s[i] == '\\' || s[i] == '<' || - s[i] == '>' || s[i] == '|' || s[i] == '*' || s[i] == '?') + + if(s[i] == '/') s[i] = '-'; - if((dvr_flags & DVR_WHITESPACE_IN_TITLE) && s[i] == ' ') + else if((dvr_flags & DVR_WHITESPACE_IN_TITLE) && + (s[i] == ' ' || s[i] == '\t')) s[i] = '-'; + + else if((dvr_flags & DVR_CLEAN_TITLE) && + ((s[i] < 32) || (s[i] > 122) || + (strchr("/:\\<>|*?'\"", s[i]) != NULL))) + s[i] = '-'; } } @@ -205,6 +164,11 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) snprintf(path, sizeof(path), "%s", cfg->dvr_storage); + /* Remove trailing slash */ + + if (path[strlen(path)-1] == '/') + path[strlen(path)-1] = '\0'; + /* Append per-day directory */ if(cfg->dvr_flags & DVR_DIR_PER_DAY) { @@ -219,7 +183,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) if(cfg->dvr_flags & DVR_DIR_PER_CHANNEL) { - char *chname = strdup(de->de_channel->ch_name); + char *chname = strdup(DVR_CH_NAME(de)); cleanupfilename(chname,cfg->dvr_flags); snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", chname); @@ -241,7 +205,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) /* */ - if(makedirs(path) != 0) { + if(makedirs(path, 0777) != 0) { return -1; } @@ -316,7 +280,7 @@ dvr_rec_set_state(dvr_entry_t *de, dvr_rs_state_t newstate, int error) /** * */ -static void +static int dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) { const source_info_t *si = &ss->ss_si; @@ -324,31 +288,31 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) int i; dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - de->de_mux = muxer_create(de->de_s->ths_service, de->de_mc); + de->de_mux = muxer_create(de->de_mc); if(!de->de_mux) { dvr_rec_fatal_error(de, "Unable to create muxer"); - return; + return -1; } if(pvr_generate_filename(de, ss) != 0) { dvr_rec_fatal_error(de, "Unable to create directories"); - return; + return -1; } if(muxer_open_file(de->de_mux, de->de_filename)) { dvr_rec_fatal_error(de, "Unable to open file"); - return; + return -1; } if(muxer_init(de->de_mux, ss, lang_str_get(de->de_title, NULL))) { dvr_rec_fatal_error(de, "Unable to init file"); - return; + return -1; } - if(cfg->dvr_flags & DVR_TAG_FILES) { + if(cfg->dvr_flags & DVR_TAG_FILES && de->de_bcast) { if(muxer_write_meta(de->de_mux, de->de_bcast)) { dvr_rec_fatal_error(de, "Unable to write meta data"); - return; + return -1; } } @@ -365,25 +329,32 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) tvhlog(LOG_INFO, "dvr", - " # %-20s %-4s %-16s %-10s %-10s", + " # %-16s %-4s %-10s %-12s %-11s %-8s", "type", "lang", "resolution", - "samplerate", + "aspect ratio", + "sample rate", "channels"); for(i = 0; i < ss->ss_num_components; i++) { ssc = &ss->ss_components[i]; - char res[16]; + char res[11]; + char asp[6]; char sr[6]; char ch[7]; if(SCT_ISAUDIO(ssc->ssc_type)) { - snprintf(sr, sizeof(sr), "%d", sri_to_rate(ssc->ssc_sri)); + if(ssc->ssc_sri) + snprintf(sr, sizeof(sr), "%d", sri_to_rate(ssc->ssc_sri)); + else + strcpy(sr, "?"); if(ssc->ssc_channels == 6) snprintf(ch, sizeof(ch), "5.1"); + else if(ssc->ssc_channels == 0) + strcpy(ch, "?"); else snprintf(ch, sizeof(ch), "%d", ssc->ssc_channels); } else { @@ -391,24 +362,39 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) ch[0] = 0; } - if(SCT_ISVIDEO(ssc->ssc_type)) { - snprintf(res, sizeof(res), "%d x %d", - ssc->ssc_width, ssc->ssc_height); + if(ssc->ssc_width && ssc->ssc_height) + snprintf(res, sizeof(res), "%dx%d", + ssc->ssc_width, ssc->ssc_height); + else + strcpy(res, "?"); } else { res[0] = 0; } + if(SCT_ISVIDEO(ssc->ssc_type)) { + if(ssc->ssc_aspect_num && ssc->ssc_aspect_den) + snprintf(asp, sizeof(asp), "%d:%d", + ssc->ssc_aspect_num, ssc->ssc_aspect_den); + else + strcpy(asp, "?"); + } else { + asp[0] = 0; + } + tvhlog(LOG_INFO, "dvr", - "%2d %-20s %-4s %-16s %-10s %-10s %s", + "%2d %-16s %-4s %-10s %-12s %-11s %-8s %s", ssc->ssc_index, streaming_component_type2txt(ssc->ssc_type), ssc->ssc_lang, res, + asp, sr, ch, ssc->ssc_disabled ? "" : ""); } + + return 0; } @@ -419,9 +405,14 @@ static void * dvr_thread(void *aux) { dvr_entry_t *de = aux; + dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); streaming_queue_t *sq = &de->de_sq; streaming_message_t *sm; + th_pkt_t *pkt; int run = 1; + int started = 0; + int comm_skip = (cfg->dvr_flags & DVR_SKIP_COMMERCIALS); + int commercial = COMMERCIAL_UNKNOWN; pthread_mutex_lock(&sq->sq_mutex); @@ -437,47 +428,89 @@ dvr_thread(void *aux) pthread_mutex_unlock(&sq->sq_mutex); switch(sm->sm_type) { - case SMT_MPEGTS: + case SMT_PACKET: - if(dispatch_clock > de->de_start - (60 * de->de_start_extra)) { + pkt = sm->sm_data; + if(pkt->pkt_commercial == COMMERCIAL_YES) + dvr_rec_set_state(de, DVR_RS_COMMERCIAL, 0); + else dvr_rec_set_state(de, DVR_RS_RUNNING, 0); - if(!muxer_write_pkt(de->de_mux, sm->sm_data)) - sm->sm_data = NULL; + if(pkt->pkt_commercial == COMMERCIAL_YES && comm_skip) + break; + + if(commercial != pkt->pkt_commercial) + muxer_add_marker(de->de_mux); + + commercial = pkt->pkt_commercial; + + if(started) { + muxer_write_pkt(de->de_mux, sm->sm_type, sm->sm_data); + sm->sm_data = NULL; + } + break; + + case SMT_MPEGTS: + if(started) { + dvr_rec_set_state(de, DVR_RS_RUNNING, 0); + muxer_write_pkt(de->de_mux, sm->sm_type, sm->sm_data); + sm->sm_data = NULL; } break; case SMT_START: - pthread_mutex_lock(&global_lock); - dvr_rec_set_state(de, DVR_RS_WAIT_PROGRAM_START, 0); - dvr_rec_start(de, sm->sm_data); - pthread_mutex_unlock(&global_lock); + if(started && + muxer_reconfigure(de->de_mux, sm->sm_data) < 0) { + tvhlog(LOG_WARNING, + "dvr", "Unable to reconfigure \"%s\"", + de->de_filename ?: lang_str_get(de->de_title, NULL)); + + // Try to restart the recording if the muxer doesn't + // support reconfiguration of the streams. + dvr_thread_epilog(de); + started = 0; + } + + if(!started) { + pthread_mutex_lock(&global_lock); + dvr_rec_set_state(de, DVR_RS_WAIT_PROGRAM_START, 0); + if(dvr_rec_start(de, sm->sm_data) == 0) { + started = 1; + dvr_entry_notify(de); + htsp_dvr_entry_update(de); + dvr_entry_save(de); + } + pthread_mutex_unlock(&global_lock); + } break; case SMT_STOP: + if(sm->sm_code == SM_CODE_SOURCE_RECONFIGURED) { + // Subscription is restarting, wait for SMT_START - if(sm->sm_code == 0) { - /* Completed */ + } else if(sm->sm_code == 0) { + // Recording is completed de->de_last_error = 0; - tvhlog(LOG_INFO, "dvr", "Recording completed: \"%s\"", de->de_filename ?: lang_str_get(de->de_title, NULL)); - } else { + dvr_thread_epilog(de); + started = 0; - if(de->de_last_error != sm->sm_code) { - dvr_rec_set_state(de, DVR_RS_ERROR, sm->sm_code); + }else if(de->de_last_error != sm->sm_code) { + // Error during recording - tvhlog(LOG_ERR, - "dvr", "Recording stopped: \"%s\": %s", - de->de_filename ?: lang_str_get(de->de_title, NULL), - streaming_code2txt(sm->sm_code)); - } + dvr_rec_set_state(de, DVR_RS_ERROR, sm->sm_code); + tvhlog(LOG_ERR, + "dvr", "Recording stopped: \"%s\": %s", + de->de_filename ?: lang_str_get(de->de_title, NULL), + streaming_code2txt(sm->sm_code)); + + dvr_thread_epilog(de); + started = 0; } - - dvr_thread_epilog(de); break; case SMT_SERVICE_STATUS: @@ -516,7 +549,10 @@ dvr_thread(void *aux) } break; + case SMT_SPEED: + case SMT_SKIP: case SMT_SIGNAL_STATUS: + case SMT_TIMESHIFT_STATUS: break; case SMT_EXIT: @@ -528,6 +564,10 @@ dvr_thread(void *aux) pthread_mutex_lock(&sq->sq_mutex); } pthread_mutex_unlock(&sq->sq_mutex); + + if(de->de_mux) + dvr_thread_epilog(de); + return NULL; } @@ -559,7 +599,7 @@ dvr_spawn_postproc(dvr_entry_t *de, const char *dvr_postproc) memset(fmap, 0, sizeof(fmap)); fmap['f'] = de->de_filename; /* full path to recoding */ fmap['b'] = basename(fbasename); /* basename of recoding */ - fmap['c'] = de->de_channel->ch_name; /* channel name */ + fmap['c'] = DVR_CH_NAME(de); /* channel name */ fmap['C'] = de->de_creator; /* user who created this recording */ fmap['t'] = lang_str_get(de->de_title, NULL); /* program title */ fmap['d'] = lang_str_get(de->de_desc, NULL); /* program description */ @@ -595,6 +635,6 @@ dvr_thread_epilog(dvr_entry_t *de) de->de_mux = NULL; dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - if(cfg->dvr_postproc) + if(cfg->dvr_postproc && de->de_filename) dvr_spawn_postproc(de,cfg->dvr_postproc); } diff --git a/src/epg.c b/src/epg.c index 58009eb8..e031ae07 100644 --- a/src/epg.c +++ b/src/epg.c @@ -30,8 +30,9 @@ #include "settings.h" #include "epg.h" #include "dvr/dvr.h" -#include "htsp.h" +#include "htsp_server.h" #include "epggrab.h" +#include "imagecache.h" /* Broadcast hashing */ #define EPG_HASH_WIDTH 1024 @@ -99,8 +100,8 @@ void epg_updated ( void ) /* Remove unref'd */ while ((eo = LIST_FIRST(&epg_object_unref))) { - tvhlog(LOG_DEBUG, "epg", - "unref'd object %u (%s) created during update", eo->id, eo->uri); + tvhtrace("epg", + "unref'd object %u (%s) created during update", eo->id, eo->uri); LIST_REMOVE(eo, un_link); eo->destroy(eo); } @@ -125,10 +126,8 @@ static void _epg_object_destroy ( epg_object_t *eo, epg_object_tree_t *tree ) { assert(eo->refcount == 0); -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] destroy", - eo, eo->id, eo->type, eo->uri); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] destroy", + eo, eo->id, eo->type, eo->uri); if (eo->uri) free(eo->uri); if (tree) RB_REMOVE(tree, eo, uri_link); if (eo->_updated) LIST_REMOVE(eo, up_link); @@ -138,10 +137,8 @@ static void _epg_object_destroy static void _epg_object_getref ( void *o ) { epg_object_t *eo = o; -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] getref %d", - eo, eo->id, eo->type, eo->uri, eo->refcount+1); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] getref %d", + eo, eo->id, eo->type, eo->uri, eo->refcount+1); if (eo->refcount == 0) LIST_REMOVE(eo, un_link); eo->refcount++; } @@ -149,10 +146,8 @@ static void _epg_object_getref ( void *o ) static void _epg_object_putref ( void *o ) { epg_object_t *eo = o; -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] putref %d", - eo, eo->id, eo->type, eo->uri, eo->refcount-1); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] putref %d", + eo, eo->id, eo->type, eo->uri, eo->refcount-1); assert(eo->refcount>0); eo->refcount--; if (!eo->refcount) eo->destroy(eo); @@ -162,10 +157,8 @@ static void _epg_object_set_updated ( void *o ) { epg_object_t *eo = o; if (!eo->_updated) { -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] updated", - eo, eo->id, eo->type, eo->uri); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] updated", + eo, eo->id, eo->type, eo->uri); eo->_updated = 1; eo->updated = dispatch_clock; LIST_INSERT_HEAD(&epg_object_updated, eo, up_link); @@ -179,10 +172,8 @@ static void _epg_object_create ( void *o ) else if (eo->id > _epg_object_idx) _epg_object_idx = eo->id; if (!eo->getref) eo->getref = _epg_object_getref; if (!eo->putref) eo->putref = _epg_object_putref; -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] created", - eo, eo->id, eo->type, eo->uri); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] created", + eo, eo->id, eo->type, eo->uri); _epg_object_set_updated(eo); LIST_INSERT_HEAD(&epg_object_unref, eo, un_link); LIST_INSERT_HEAD(&epg_objects[eo->id & EPG_HASH_MASK], eo, id_link); @@ -230,10 +221,8 @@ epg_object_t *epg_object_find_by_id ( uint32_t id, epg_object_type_t type ) static htsmsg_t * _epg_object_serialize ( void *o ) { epg_object_t *eo = o; -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] serialize", - eo, eo->id, eo->type, eo->uri); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] serialize", + eo, eo->id, eo->type, eo->uri); htsmsg_t *m; if ( !eo->id || !eo->type ) return NULL; m = htsmsg_create_map(); @@ -262,10 +251,8 @@ static epg_object_t *_epg_object_deserialize ( htsmsg_t *m, epg_object_t *eo ) _epg_object_set_updated(eo); eo->updated = s64; } -#ifdef EPG_TRACE - tvhlog(LOG_DEBUG, "epg", "eo [%p, %u, %d, %s] deserialize", - eo, eo->id, eo->type, eo->uri); -#endif + tvhtrace("epg", "eo [%p, %u, %d, %s] deserialize", + eo, eo->id, eo->type, eo->uri); return eo; } @@ -459,8 +446,12 @@ int epg_brand_set_summary int epg_brand_set_image ( epg_brand_t *brand, const char *image, epggrab_module_t *src ) { + int save; if (!brand || !image) return 0; - return _epg_object_set_str(brand, &brand->image, image, src); + save = _epg_object_set_str(brand, &brand->image, image, src); + if (save) + imagecache_get_id(image); + return save; } int epg_brand_set_season_count @@ -628,8 +619,12 @@ int epg_season_set_summary int epg_season_set_image ( epg_season_t *season, const char *image, epggrab_module_t *src ) { + int save; if (!season || !image) return 0; - return _epg_object_set_str(season, &season->image, image, src); + save = _epg_object_set_str(season, &season->image, image, src); + if (save) + imagecache_get_id(image); + return save; } int epg_season_set_episode_count @@ -746,13 +741,13 @@ static htsmsg_t *epg_episode_num_serialize ( epg_episode_num_t *num ) if (num->e_cnt) htsmsg_add_u32(m, "e_cnt", num->e_cnt); if (num->s_num) - htsmsg_add_u32(m, "s_num", num->e_num); + htsmsg_add_u32(m, "s_num", num->s_num); if (num->s_cnt) - htsmsg_add_u32(m, "s_cnt", num->e_cnt); + htsmsg_add_u32(m, "s_cnt", num->s_cnt); if (num->p_num) - htsmsg_add_u32(m, "p_num", num->e_num); + htsmsg_add_u32(m, "p_num", num->p_num); if (num->p_cnt) - htsmsg_add_u32(m, "p_cnt", num->e_cnt); + htsmsg_add_u32(m, "p_cnt", num->p_cnt); if (num->text) htsmsg_add_str(m, "text", num->text); return m; @@ -891,8 +886,12 @@ int epg_episode_set_description int epg_episode_set_image ( epg_episode_t *episode, const char *image, epggrab_module_t *src ) { + int save; if (!episode || !image) return 0; - return _epg_object_set_str(episode, &episode->image, image, src); + save = _epg_object_set_str(episode, &episode->image, image, src); + if (save) + imagecache_get_id(image); + return save; } int epg_episode_set_number @@ -990,6 +989,7 @@ int epg_episode_set_genre g2 = LIST_NEXT(g1, link); if (!epg_genre_list_contains(genre, g1, 0)) { LIST_REMOVE(g1, link); + free(g1); save = 1; } g1 = g2; @@ -1077,8 +1077,8 @@ size_t epg_episode_number_format if ( cfmt && num.e_cnt ) i+= snprintf(&buf[i], len-i, cfmt, num.e_cnt); } else if ( num.text ) { - strncpy(buf, num.text, len); - i = strlen(buf); + if (pre) i += snprintf(&buf[i], len-i, "%s", pre); + i += snprintf(&buf[i], len-i, "%s", num.text); } return i; } @@ -1377,8 +1377,8 @@ static void _epg_channel_timer_callback ( void *p ) /* Expire */ if ( ebc->stop <= dispatch_clock ) { - tvhlog(LOG_DEBUG, "epg", "expire event %u from %s", - ebc->id, ch->ch_name); + tvhlog(LOG_DEBUG, "epg", "expire event %u (%s) from %s", + ebc->id, epg_broadcast_get_title(ebc, NULL), ch->ch_name); _epg_channel_rem_broadcast(ch, ebc, NULL); continue; // skip to next @@ -1447,6 +1447,8 @@ static epg_broadcast_t *_epg_channel_add_broadcast _epg_object_create(ret); // Note: sets updated _epg_object_getref(ret); + tvhtrace("epg", "added event %u (%s) on %s @ %"PRItime_t " to %"PRItime_t, + ret->id, epg_broadcast_get_title(ret, NULL), ch->ch_name, ret->start, ret->stop); /* Existing */ } else { @@ -1460,6 +1462,8 @@ static epg_broadcast_t *_epg_channel_add_broadcast } else { ret->stop = (*bcast)->stop; _epg_object_set_updated(ret); + tvhtrace("epg", "updated event %u (%s) on %s @ %"PRItime_t " to %"PRItime_t, + ret->id, epg_broadcast_get_title(ret, NULL), ch->ch_name, ret->start, ret->stop); } } } @@ -1470,12 +1474,16 @@ static epg_broadcast_t *_epg_channel_add_broadcast /* Remove overlapping (before) */ while ( (ebc = RB_PREV(ret, sched_link)) != NULL ) { if ( ebc->stop <= ret->start ) break; + tvhtrace("epg", "remove overlap (b) event %u (%s) on %s @ %"PRItime_t " to %"PRItime_t, + ebc->id, epg_broadcast_get_title(ebc, NULL), ch->ch_name, ebc->start, ebc->stop); _epg_channel_rem_broadcast(ch, ebc, ret); } /* Remove overlapping (after) */ while ( (ebc = RB_NEXT(ret, sched_link)) != NULL ) { if ( ebc->start >= ret->stop ) break; + tvhtrace("epg", "remove overlap (a) event %u (%s) on %s @ %"PRItime_t " to %"PRItime_t, + ebc->id, epg_broadcast_get_title(ebc, NULL), ch->ch_name, ebc->start, ebc->stop); _epg_channel_rem_broadcast(ch, ebc, ret); } @@ -1839,10 +1847,15 @@ epg_broadcast_t *epg_broadcast_deserialize if (!htsmsg_get_u32(m, "is_repeat", &u32)) *save |= epg_broadcast_set_is_repeat(ebc, u32, NULL); - if ((ls = lang_str_deserialize(m, "summary"))) + if ((ls = lang_str_deserialize(m, "summary"))) { *save |= epg_broadcast_set_summary2(ebc, ls, NULL); - if ((ls = lang_str_deserialize(m, "description"))) + lang_str_destroy(ls); + } + + if ((ls = lang_str_deserialize(m, "description"))) { *save |= epg_broadcast_set_description2(ebc, ls, NULL); + lang_str_destroy(ls); + } /* Series link */ if ((str = htsmsg_get_str(m, "serieslink"))) @@ -1866,153 +1879,153 @@ epg_broadcast_t *epg_broadcast_deserialize static const char *_epg_genre_names[16][16] = { { "" }, { - "Movie/Drama", - "detective/thriller", - "adventure/western/war", - "science fiction/fantasy/horror", - "comedy", - "soap/melodrama/folkloric", - "romance", - "serious/classical/religious/historical movie/drama", - "adult movie/drama", - "adult movie/drama", - "adult movie/drama", - "adult movie/drama", - "adult movie/drama", - "adult movie/drama", + "Movie / Drama", + "Detective / Thriller", + "Adventure / Western / War", + "Science fiction / Fantasy / Horror", + "Comedy", + "Soap / Melodrama / Folkloric", + "Romance", + "Serious / Classical / Religious / Historical movie / Drama", + "Adult movie / Drama", + "Adult movie / Drama", + "Adult movie / Drama", + "Adult movie / Drama", + "Adult movie / Drama", + "Adult movie / Drama", }, { - "News/Current affairs", - "news/weather report", - "news magazine", - "documentary", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", - "discussion/interview/debate", + "News / Current affairs", + "News / Weather report", + "News magazine", + "Documentary", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", + "Discussion / Interview / Debate", }, { - "Show/Game show", - "game show/quiz/contest", - "variety show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", - "talk show", + "Show / Game show", + "Game show / Quiz / Contest", + "Variety show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", + "Talk show", }, { "Sports", - "special events (Olympic Games, World Cup, etc.)", - "sports magazines", - "football/soccer", - "tennis/squash", - "team sports (excluding football)", - "athletics", - "motor sport", - "water sport", + "Special events (Olympic Games, World Cup, etc.)", + "Sports magazines", + "Football / Soccer", + "Tennis / Squash", + "Team sports (excluding football)", + "Athletics", + "Motor sport", + "Water sport", }, { - "Children's/Youth programmes", - "pre-school children's programmes", - "entertainment programmes for 6 to14", - "entertainment programmes for 10 to 16", - "informational/educational/school programmes", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", - "cartoons/puppets", + "Children's / Youth programmes", + "Pre-school children's programmes", + "Entertainment programmes for 6 to 14", + "Entertainment programmes for 10 to 16", + "Informational / Educational / School programmes", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", + "Cartoons / Puppets", }, { - "Music/Ballet/Dance", - "rock/pop", - "serious music/classical music", - "folk/traditional music", - "jazz", - "musical/opera", - "musical/opera", - "musical/opera", - "musical/opera", - "musical/opera", - "musical/opera", - "musical/opera", - "musical/opera", + "Music / Ballet / Dance", + "Rock / Pop", + "Serious music / Classical music", + "Folk / Traditional music", + "Jazz", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", + "Musical / Opera", }, { - "Arts/Culture (without music)", - "performing arts", - "fine arts", - "religion", - "popular culture/traditional arts", - "literature", - "film/cinema", - "experimental film/video", - "broadcasting/press", + "Arts / Culture (without music)", + "Performing arts", + "Fine arts", + "Religion", + "Popular culture / Traditional arts", + "Literature", + "Film / Cinema", + "Experimental film / Video", + "Broadcasting / Press", }, { - "Social/Political issues/Economics", - "magazines/reports/documentary", - "economics/social advisory", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", - "remarkable people", + "Social / Political issues / Economics", + "Magazines / Reports / Documentary", + "Economics / Social advisory", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", + "Remarkable people", }, { - "Education/Science/Factual topics", - "nature/animals/environment", - "technology/natural sciences", - "medicine/physiology/psychology", - "foreign countries/expeditions", - "social/spiritual sciences", - "further education", - "languages", - "languages", - "languages", - "languages", - "languages", - "languages", - "languages", + "Education / Science / Factual topics", + "Nature / Animals / Environment", + "Technology / Natural sciences", + "Medicine / Physiology / Psychology", + "Foreign countries / Expeditions", + "Social / Spiritual sciences", + "Further education", + "Languages", + "Languages", + "Languages", + "Languages", + "Languages", + "Languages", + "Languages", }, { "Leisure hobbies", - "tourism/travel", - "handicraft", - "motoring", - "fitness and health", - "cooking", - "advertisement/shopping", - "gardening", - "gardening", - "gardening", - "gardening", - "gardening", - "gardening", - "gardening", + "Tourism / Travel", + "Handicraft", + "Motoring", + "Fitness and health", + "Cooking", + "Advertisement / Shopping", + "Gardening", + "Gardening", + "Gardening", + "Gardening", + "Gardening", + "Gardening", + "Gardening", } }; diff --git a/src/epg.h b/src/epg.h index bff1a5c2..f8724e7b 100644 --- a/src/epg.h +++ b/src/epg.h @@ -556,7 +556,7 @@ void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, * ***********************************************************************/ void epg_init (void); -void epg_save (void); +void epg_save (void*); void epg_updated (void); /* ************************************************************************ diff --git a/src/epgdb.c b/src/epgdb.c index f56e5982..ebc1513b 100644 --- a/src/epgdb.c +++ b/src/epgdb.c @@ -20,6 +20,7 @@ #include #include #include +#include #include "tvheadend.h" #include "queue.h" @@ -241,9 +242,8 @@ static int _epg_write ( int fd, htsmsg_t *m ) int r = htsmsg_binary_serialize(m, &msgdata, &msglen, 0x10000); htsmsg_destroy(m); if (!r) { - ssize_t w = write(fd, msgdata, msglen); + ret = tvh_write(fd, msgdata, msglen); free(msgdata); - if(w == msglen) ret = 0; } } else { ret = 0; @@ -263,13 +263,17 @@ static int _epg_write_sect ( int fd, const char *sect ) return _epg_write(fd, m); } -void epg_save ( void ) +void epg_save ( void *p ) { int fd; epg_object_t *eo; epg_broadcast_t *ebc; channel_t *ch; epggrab_stats_t stats; + extern gtimer_t epggrab_save_timer; + + if (epggrab_epgdb_periodicsave) + gtimer_arm(&epggrab_save_timer, epg_save, NULL, epggrab_epgdb_periodicsave); fd = hts_settings_open_file(1, "epgdb.v%d", EPG_DB_VERSION); diff --git a/src/epggrab.c b/src/epggrab.c index 9d0dcb0f..8d110376 100644 --- a/src/epggrab.c +++ b/src/epggrab.c @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -39,9 +38,9 @@ #include "service.h" /* Thread protection */ -static int epggrab_confver; +static int epggrab_confver; pthread_mutex_t epggrab_mutex; -static pthread_cond_t epggrab_cond; +static pthread_cond_t epggrab_cond; /* Config */ uint32_t epggrab_interval; @@ -50,6 +49,9 @@ epggrab_module_list_t epggrab_modules; uint32_t epggrab_channel_rename; uint32_t epggrab_channel_renumber; uint32_t epggrab_channel_reicon; +uint32_t epggrab_epgdb_periodicsave; + +gtimer_t epggrab_save_timer; /* ************************************************************************** * Internal Grab Thread @@ -139,6 +141,10 @@ static void _epggrab_load ( void ) htsmsg_get_u32(m, "channel_rename", &epggrab_channel_rename); htsmsg_get_u32(m, "channel_renumber", &epggrab_channel_renumber); htsmsg_get_u32(m, "channel_reicon", &epggrab_channel_reicon); + htsmsg_get_u32(m, "epgdb_periodicsave", &epggrab_epgdb_periodicsave); + if (epggrab_epgdb_periodicsave) + gtimer_arm(&epggrab_save_timer, epg_save, NULL, + epggrab_epgdb_periodicsave); if (!htsmsg_get_u32(m, old ? "grab-interval" : "interval", &epggrab_interval)) { if (old) epggrab_interval *= 3600; @@ -213,8 +219,10 @@ static void _epggrab_load ( void ) } /* Load module config (channels) */ +#if ENABLE_LINUXDVB eit_load(); opentv_load(); +#endif pyepg_load(); xmltv_load(); } @@ -233,6 +241,7 @@ void epggrab_save ( void ) htsmsg_add_u32(m, "channel_rename", epggrab_channel_rename); htsmsg_add_u32(m, "channel_renumber", epggrab_channel_renumber); htsmsg_add_u32(m, "channel_reicon", epggrab_channel_reicon); + htsmsg_add_u32(m, "epgdb_periodicsave", epggrab_epgdb_periodicsave); htsmsg_add_u32(m, "interval", epggrab_interval); if ( epggrab_module ) htsmsg_add_str(m, "module", epggrab_module->id); @@ -298,6 +307,25 @@ int epggrab_set_channel_renumber ( uint32_t e ) return save; } +/* + * Config from the webui for period save of db to disk + */ +int epggrab_set_periodicsave ( uint32_t e ) +{ + int save = 0; + if ( e != epggrab_epgdb_periodicsave ) { + epggrab_epgdb_periodicsave = e; + pthread_mutex_lock(&global_lock); + if (!e) + gtimer_disarm(&epggrab_save_timer); + else + epg_save(NULL); // will arm the timer + pthread_mutex_unlock(&global_lock); + save = 1; + } + return save; +} + int epggrab_set_channel_reicon ( uint32_t e ) { int save = 0; @@ -342,22 +370,36 @@ void epggrab_resched ( void ) */ void epggrab_init ( void ) { + /* Defaults */ + epggrab_interval = 0; + epggrab_module = NULL; + epggrab_channel_rename = 0; + epggrab_channel_renumber = 0; + epggrab_channel_reicon = 0; + epggrab_epgdb_periodicsave = 0; + /* Lists */ +#if ENABLE_LINUXDVB extern TAILQ_HEAD(, epggrab_ota_mux) ota_mux_all; TAILQ_INIT(&ota_mux_all); +#endif pthread_mutex_init(&epggrab_mutex, NULL); pthread_cond_init(&epggrab_cond, NULL); /* Initialise modules */ +#if ENABLE_LINUXDVB eit_init(); opentv_init(); +#endif pyepg_init(); xmltv_init(); /* Load config */ _epggrab_load(); +#if ENABLE_LINUXDVB epggrab_ota_load(); +#endif /* Start internal grab thread */ pthread_t tid; diff --git a/src/epggrab.h b/src/epggrab.h index ddb46a41..d9496f58 100644 --- a/src/epggrab.h +++ b/src/epggrab.h @@ -226,6 +226,7 @@ extern epggrab_module_int_t* epggrab_module; extern uint32_t epggrab_channel_rename; extern uint32_t epggrab_channel_renumber; extern uint32_t epggrab_channel_reicon; +extern uint32_t epggrab_epgdb_periodicsave; /* * Set configuration @@ -236,6 +237,7 @@ int epggrab_set_module_by_id ( const char *id ); int epggrab_set_channel_rename ( uint32_t e ); int epggrab_set_channel_renumber ( uint32_t e ); int epggrab_set_channel_reicon ( uint32_t e ); +int epggrab_set_periodicsave ( uint32_t e ); int epggrab_enable_module ( epggrab_module_t *mod, uint8_t e ); int epggrab_enable_module_by_id ( const char *id, uint8_t e ); diff --git a/src/epggrab/channel.c b/src/epggrab/channel.c index db73b288..3fe0381c 100644 --- a/src/epggrab/channel.c +++ b/src/epggrab/channel.c @@ -185,7 +185,7 @@ epggrab_channel_t *epggrab_channel_find htsmsg_t *epggrab_channel_list ( void ) { - char name[100]; + char name[500]; epggrab_module_t *mod; epggrab_channel_t *ec; htsmsg_t *e, *m; @@ -198,9 +198,10 @@ htsmsg_t *epggrab_channel_list ( void ) htsmsg_add_str(e, "id", ec->id); if (ec->name) htsmsg_add_str(e, "name", ec->name); - sprintf(name, "%s|%s", mod->id, ec->id); + snprintf(name, sizeof(name), "%s|%s", mod->id, ec->id); htsmsg_add_str(e, "mod-id", name); - sprintf(name, "%s: %s (%s)", mod->name, ec->name, ec->id); + snprintf(name, sizeof(name), "%s: %s (%s)", + mod->name, ec->name, ec->id); htsmsg_add_str(e, "mod-name", name); htsmsg_add_msg(m, NULL, e); } diff --git a/src/epggrab/module/eit.c b/src/epggrab/module/eit.c index 7efecf38..c20c32d5 100644 --- a/src/epggrab/module/eit.c +++ b/src/epggrab/module/eit.c @@ -228,7 +228,7 @@ static int _eit_desc_short_event { int r; char lang[4]; - char buf[256]; + char buf[512]; if ( len < 5 ) return -1; @@ -242,7 +242,7 @@ static int _eit_desc_short_event if ( (r = _eit_get_string_with_len(mod, buf, sizeof(buf), ptr, len, ev->default_charset)) < 0 ) { return -1; - } else { + } else if ( r > 1 ) { if (!ev->title) ev->title = lang_str_create(); lang_str_add(ev->title, buf, lang, 0); } @@ -255,7 +255,7 @@ static int _eit_desc_short_event if ( (r = _eit_get_string_with_len(mod, buf, sizeof(buf), ptr, len, ev->default_charset)) < 0 ) { return -1; - } else { + } else if ( r > 1 ) { if (!ev->summary) ev->summary = lang_str_create(); lang_str_add(ev->summary, buf, lang, 0); } @@ -269,9 +269,10 @@ static int _eit_desc_short_event static int _eit_desc_ext_event ( epggrab_module_t *mod, uint8_t *ptr, int len, eit_event_t *ev ) { - int r, nitem; - char ikey[256], ival[256]; - char buf[256], lang[4]; + int r, ilen; + char ikey[512], ival[512]; + char buf[512], lang[4]; + uint8_t *iptr; if (len < 6) return -1; @@ -286,28 +287,34 @@ static int _eit_desc_ext_event ptr += 3; /* Key/Value items */ - nitem = *ptr; + ilen = *ptr; len -= 1; ptr += 1; - if (len < (2 * nitem) + 1) return -1; + iptr = ptr; + if (len < ilen) return -1; - while (nitem--) { + /* Skip past */ + ptr += ilen; + len -= ilen; + + /* Process */ + while (ilen) { /* Key */ if ( (r = _eit_get_string_with_len(mod, ikey, sizeof(ikey), - ptr, len, ev->default_charset)) < 0 ) - return -1; - - len -= r; - ptr += r; + iptr, ilen, ev->default_charset)) < 0 ) + break; + + ilen -= r; + iptr += r; /* Value */ if ( (r = _eit_get_string_with_len(mod, ival, sizeof(ival), - ptr, len, ev->default_charset)) < 0 ) - return -1; + iptr, ilen, ev->default_charset)) < 0 ) + break; - len -= r; - ptr += r; + ilen -= r; + iptr += r; /* Store */ // TODO: extend existing? @@ -323,9 +330,7 @@ static int _eit_desc_ext_event if ( _eit_get_string_with_len(mod, buf, sizeof(buf), ptr, len, - ev->default_charset) < 0 ) { - return -1; - } else { + ev->default_charset) > 1 ) { if (!ev->desc) ev->desc = lang_str_create(); lang_str_append(ev->desc, buf, lang); } @@ -441,7 +446,7 @@ static int _eit_desc_crid { int r; uint8_t type; - char buf[257], *crid; + char buf[512], *crid; int clen; while (len > 3) { @@ -455,6 +460,7 @@ static int _eit_desc_crid ptr+1, len-1, ev->default_charset); if (r < 0) return -1; + if (r == 0) continue; /* Episode */ if (type == 0x1 || type == 0x31) { @@ -527,10 +533,8 @@ static int _eit_process_event /* Find broadcast */ ebc = epg_broadcast_find_by_time(svc->s_ch, start, stop, eid, 1, &save2); -#ifdef EPG_EIT_TRACE - tvhlog(LOG_DEBUG, mod->id, "eid=%5d, start=%lu, stop=%lu, ebc=%p", + tvhtrace("eit", "eid=%5d, start=%lu, stop=%lu, ebc=%p", eid, start, stop, ebc); -#endif if (!ebc) return dllen + 12; /* Mark re-schedule detect (only now/next) */ @@ -692,20 +696,17 @@ static int _eit_callback lst = ptr[4]; seg = ptr[9]; ver = (ptr[2] >> 1) & 0x1f; -#ifdef EPG_EIT_TRACE - tvhlog(LOG_DEBUG, mod->id, - "tid=0x%02X, onid=0x%04X, tsid=0x%04X, sid=0x%04X, sec=%3d/%3d, seg=%3d, ver=%2d, cur=%d", - tableid, onid, tsid, sid, sec, lst, seg, ver, ptr[2] & 1); -#endif + tvhtrace("eit", + "tid=0x%02X, onid=0x%04X, tsid=0x%04X, sid=0x%04X" + ", sec=%3d/%3d, seg=%3d, ver=%2d, cur=%d", + tableid, onid, tsid, sid, sec, lst, seg, ver, ptr[2] & 1); /* Don't process */ if((ptr[2] & 1) == 0) return 0; /* Current status */ tsta = eit_status_find(sta, tableid, onid, tsid, sid, sec, lst, seg, ver); -#ifdef EPG_EIT_TRACE - tvhlog(LOG_DEBUG, mod->id, tsta && tsta->state != EIT_STATUS_DONE ? "section process" : "section seen"); -#endif + tvhtrace("eit", tsta && tsta->state != EIT_STATUS_DONE ? "section process" : "section seen"); if (!tsta) return 0; // already seen, no state change if (tsta->state == EIT_STATUS_DONE) goto done; @@ -713,31 +714,23 @@ static int _eit_callback // Note: tableid=0x4f,0x60-0x6f is other TS // so must find the tdmi if(tableid == 0x4f || tableid >= 0x60) { - tda = tdmi->tdmi_adapter; - LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) - if(tdmi->tdmi_transport_stream_id == tsid && - tdmi->tdmi_network_id == onid) - break; + tda = tdmi->tdmi_adapter; + tdmi = dvb_mux_find(tda, NULL, onid, tsid, 1); } else { if (tdmi->tdmi_transport_stream_id != tsid || tdmi->tdmi_network_id != onid) { -#ifdef EPG_EIT_TRACE - tvhlog(LOG_DEBUG, mod->id, - "invalid transport id found tid 0x%02X, onid:tsid %d:%d != %d:%d", - tableid, tdmi->tdmi_network_id, tdmi->tdmi_transport_stream_id, - onid, tsid); -#endif + tvhtrace("eit", + "invalid tsid found tid 0x%02X, onid:tsid %d:%d != %d:%d", + tableid, tdmi->tdmi_network_id, tdmi->tdmi_transport_stream_id, + onid, tsid); tdmi = NULL; } } if(!tdmi) goto done; /* Get service */ - svc = dvb_transport_find(tdmi, sid, 0, NULL); - if (!svc || !svc->s_enabled || !svc->s_ch) goto done; - - /* Ignore (not primary EPG service) */ - if (!service_is_primary_epg(svc)) goto done; + svc = dvb_service_find3(NULL, tdmi, NULL, 0, 0, sid, 1, 1); + if (!svc || !svc->s_ch) goto done; /* Register as interesting */ if (tableid < 0x50) @@ -773,24 +766,25 @@ done: if (!tsta) epggrab_ota_complete(ota); } } -#ifdef EPG_EIT_TRACE +#if ENABLE_TRACE if (ota->state != EPGGRAB_OTA_MUX_COMPLETE) { int total = 0; int finished = 0; - tvhlog(LOG_DEBUG, mod->id, "scan status"); + tvhtrace("eit", "scan status:"); LIST_FOREACH(tsta, &sta->tables, link) { total++; - tvhlog(LOG_DEBUG, mod->id, - " tid=0x%02X, onid=0x%04X, tsid=0x%04X, sid=0x%04X, ver=%02d, done=%d, " - "mask=%08X|%08X|%08X|%08X|%08X|%08X|%08X|%08X", - tsta->tid, tsta->onid, tsta->tsid, tsta->sid, tsta->ver, - tsta->state == EIT_STATUS_DONE, - tsta->sec[7], tsta->sec[6], tsta->sec[5], tsta->sec[4], - tsta->sec[3], tsta->sec[2], tsta->sec[1], tsta->sec[0]); + tvhtrace("eit", + " tid=0x%02X, onid=0x%04X, tsid=0x%04X, sid=0x%04X, ver=%02d" + ", done=%d, " + "mask=%08X|%08X|%08X|%08X|%08X|%08X|%08X|%08X", + tsta->tid, tsta->onid, tsta->tsid, tsta->sid, tsta->ver, + tsta->state == EIT_STATUS_DONE, + tsta->sec[7], tsta->sec[6], tsta->sec[5], tsta->sec[4], + tsta->sec[3], tsta->sec[2], tsta->sec[1], tsta->sec[0]); if (tsta->state == EIT_STATUS_DONE) finished++; } - tvhlog(LOG_DEBUG, mod->id, " completed %d of %d", finished, total); + tvhtrace("eit", " completed %d of %d", finished, total); } #endif @@ -829,6 +823,7 @@ static void _eit_start if (!m->enabled) return; /* Freeview (switch to EIT, ignore if explicitly enabled) */ + // Note: do this as PID is the same if (!strcmp(m->id, "uk_freeview")) { m = (epggrab_module_ota_t*)epggrab_module_find_by_id("eit"); if (m->enabled) return; @@ -841,16 +836,22 @@ static void _eit_start ota->destroy = _eit_ota_destroy; } - /* Add PIDs (freesat uses non-standard) */ + /* Freesat (3002/3003) */ if (!strcmp("uk_freesat", m->id)) { #ifdef IGNORE_TOO_SLOW - tdt_add(tdmi, NULL, dvb_pidx11_callback, m, m->id, TDT_CRC, 3840, NULL); - tdt_add(tdmi, NULL, _eit_callback, m, m->id, TDT_CRC, 3841, NULL); + tdt_add(tdmi, 0, 0, dvb_pidx11_callback, m, m->id, TDT_CRC, 3840, NULL); + tdt_add(tdmi, 0, 0, _eit_callback, m, m->id, TDT_CRC, 3841, NULL); #endif - tdt_add(tdmi, NULL, dvb_pidx11_callback, m, m->id, TDT_CRC, 3002, NULL); - tdt_add(tdmi, NULL, _eit_callback, m, m->id, TDT_CRC, 3003, NULL); + tdt_add(tdmi, 0, 0, dvb_pidx11_callback, m, m->id, TDT_CRC, 3002); + tdt_add(tdmi, 0, 0, _eit_callback, m, m->id, TDT_CRC, 3003); + + /* Viasat Baltic (0x39) */ + } else if (!strcmp("viasat_baltic", m->id)) { + tdt_add(tdmi, 0, 0, _eit_callback, m, m->id, TDT_CRC, 0x39); + + /* Standard (0x12) */ } else { - tdt_add(tdmi, NULL, _eit_callback, m, m->id, TDT_CRC, 0x12, NULL); + tdt_add(tdmi, 0, 0, _eit_callback, m, m->id, TDT_CRC, 0x12); } tvhlog(LOG_DEBUG, m->id, "install table handlers"); } @@ -892,6 +893,8 @@ void eit_init ( void ) _eit_start, _eit_enable, NULL); epggrab_module_ota_create(NULL, "uk_freeview", "UK: Freeview", 5, _eit_start, _eit_enable, NULL); + epggrab_module_ota_create(NULL, "viasat_baltic", "VIASAT: Baltic", 5, + _eit_start, _eit_enable, NULL); } void eit_load ( void ) diff --git a/src/epggrab/module/opentv.c b/src/epggrab/module/opentv.c index 7a1fbba6..0d245d3a 100644 --- a/src/epggrab/module/opentv.c +++ b/src/epggrab/module/opentv.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "tvheadend.h" #include "dvb/dvb.h" @@ -189,23 +190,6 @@ static epggrab_channel_t *_opentv_find_epggrab_channel (epggrab_module_t*)mod); } -static service_t *_opentv_find_service ( int onid, int tsid, int sid ) -{ - th_dvb_adapter_t *tda; - th_dvb_mux_instance_t *tdmi; - service_t *t = NULL; - TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) { - LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) { - if (tdmi->tdmi_transport_stream_id != tsid) continue; - if (tdmi->tdmi_network_id != onid) continue; - LIST_FOREACH(t, &tdmi->tdmi_transports, s_group_link) { - if (t->s_dvb_service_id == sid) return t; - } - } - } - return NULL; -} - /* ************************************************************************ * OpenTV event processing * ***********************************************************************/ @@ -234,6 +218,8 @@ static char *_opentv_parse_string int ok = 0; char *ret, *tmp; + if (len <= 0) return NULL; + // Note: unlikely decoded string will be longer (though its possible) ret = tmp = malloc(2*len); *ret = 0; @@ -265,15 +251,17 @@ static int _opentv_parse_event_record if (rlen+2 <= len) { switch (rtag) { case 0xb5: // title - ev->start = (((int)buf[2] << 9) | (buf[3] << 1)) - + mjd; - ev->stop = (((int)buf[4] << 9) | (buf[5] << 1)) - + ev->start; - ev->cat = buf[6]; - if (prov->genre) - ev->cat = prov->genre->map[ev->cat]; - if (!ev->title) - ev->title = _opentv_parse_string(prov, buf+9, rlen-7); + if (rlen >= 7) { + ev->start = (((int)buf[2] << 9) | (buf[3] << 1)) + + mjd; + ev->stop = (((int)buf[4] << 9) | (buf[5] << 1)) + + ev->start; + ev->cat = buf[6]; + if (prov->genre) + ev->cat = prov->genre->map[ev->cat]; + if (!ev->title) + ev->title = _opentv_parse_string(prov, buf+9, rlen-7); + } break; case 0xb9: // summary if (!ev->summary) @@ -410,6 +398,25 @@ static int _opentv_parse_event_section save |= epg_episode_set_genre(ee, egl, src); epg_genre_list_destroy(egl); } + if (ev.summary) { + regex_t preg; + regmatch_t match[3]; + + /* Parse Series/Episode + * TODO: HACK: this needs doing properly */ + regcomp(&preg, " *\\(S ?([0-9]+),? Ep? ?([0-9]+)\\)$", + REG_ICASE | REG_EXTENDED); + if (!regexec(&preg, ev.summary, 3, match, 0)) { + epg_episode_num_t en; + memset(&en, 0, sizeof(en)); + if (match[1].rm_so != -1) + en.s_num = atoi(ev.summary + match[1].rm_so); + if (match[2].rm_so != -1) + en.e_num = atoi(ev.summary + match[2].rm_so); + save |= epg_episode_set_epnum(ee, &en, src); + } + regfree(&preg); + } } /* Cleanup */ @@ -442,8 +449,8 @@ static void _opentv_parse_channels cnum = ((int)buf[i+5] << 8) | buf[i+6]; /* Find the service */ - svc = _opentv_find_service(onid, tsid, sid); - if (svc && svc->s_ch && service_is_primary_epg(svc)) { + svc = dvb_service_find3(NULL, NULL, NULL, onid, tsid, sid, 1, 1); + if (svc && svc->s_ch) { ec =_opentv_find_epggrab_channel(mod, cid, 1, &save); ecl = LIST_FIRST(&ec->channels); if (!ecl) { @@ -658,7 +665,6 @@ static void _opentv_start ( epggrab_module_ota_t *m, th_dvb_mux_instance_t *tdmi ) { int *t; - struct dmx_sct_filter_params *fp; epggrab_ota_mux_t *ota; opentv_module_t *mod = (opentv_module_t*)m; opentv_status_t *sta; @@ -685,34 +691,25 @@ static void _opentv_start /* Channels */ t = mod->channel; while (*t) { - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0x4a; - fp->filter.mask[0] = 0xff; // TODO: what about 0x46 (service description) - tdt_add(tdmi, fp, _opentv_channel_callback, m, - m->id, TDT_CRC, *t++, NULL); + tdt_add(tdmi, 0x4a, 0xff, _opentv_channel_callback, m, + m->id, TDT_CRC, *t++); } /* Titles */ t = mod->title; while (*t) { - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0xa0; - fp->filter.mask[0] = 0xfc; _opentv_status_get_pid(sta, *t); - tdt_add(tdmi, fp, _opentv_title_callback, m, - m->id, TDT_CRC | TDT_TDT, *t++, NULL); + tdt_add(tdmi, 0xa0, 0xfc, _opentv_title_callback, m, + m->id, TDT_CRC | TDT_TDT, *t++); } /* Summaries */ t = mod->summary; while (*t) { - fp = dvb_fparams_alloc(); - fp->filter.filter[0] = 0xa8; - fp->filter.mask[0] = 0xfc; _opentv_status_get_pid(sta, *t); - tdt_add(tdmi, fp, _opentv_summary_callback, m, - m->id, TDT_CRC | TDT_TDT, *t++, NULL); + tdt_add(tdmi, 0xa8, 0xfc, _opentv_summary_callback, m, + m->id, TDT_CRC | TDT_TDT, *t++); } } diff --git a/src/epggrab/module/xmltv.c b/src/epggrab/module/xmltv.c index 18c53ef8..8c7bc389 100644 --- a/src/epggrab/module/xmltv.c +++ b/src/epggrab/module/xmltv.c @@ -184,8 +184,8 @@ static const char *xmltv_ns_get_parse_num out: - if(ap) *ap = a + 1; - if(bp) *bp = b + 1; + if(ap && a >= 0) *ap = a + 1; + if(bp && b >= 0) *bp = b; return s; } @@ -208,7 +208,10 @@ static void parse_xmltv_dd_progid snprintf(buf, sizeof(buf)-1, "ddprogid://%s/%s", mod->id, s); /* SH - series without episode id so ignore */ - if (strncmp("SH", s, 2)) *uri = strdup(buf); + if (strncmp("SH", s, 2)) + *uri = strdup(buf); + else + *suri = strdup(buf); /* Episode */ if (!strncmp("EP", s, 2)) { @@ -351,14 +354,22 @@ static int _xmltv_parse_previously_shown * Star rating */ static int _xmltv_parse_star_rating - ( epggrab_module_t *mod, epg_episode_t *ee, htsmsg_t *tags ) + ( epggrab_module_t *mod, epg_episode_t *ee, htsmsg_t *body ) { - int a, b; - const char *stars; - if (!mod || !ee || !tags) return 0; - if (!(stars = htsmsg_xml_get_cdata_str(tags, "star-rating"))) return 0; - if (sscanf(stars, "%d/%d", &a, &b) != 2) return 0; - return epg_episode_set_star_rating(ee, (5 * a) / b, mod); + double a, b; + htsmsg_t *stars, *tags; + const char *s1, *s2; + + if (!mod || !ee || !body) return 0; + if (!(stars = htsmsg_get_map(body, "star-rating"))) return 0; + if (!(tags = htsmsg_get_map(stars, "tags"))) return 0; + if (!(s1 = htsmsg_xml_get_cdata_str(tags, "value"))) return 0; + if (!(s2 = strstr(s1, "/"))) return 0; + + a = atof(s1); + b = atof(s2 + 1); + + return epg_episode_set_star_rating(ee, (100 * a) / b, mod); } /* @@ -631,7 +642,7 @@ static void _xmltv_load_grabbers ( void ) size_t i, p, n; char *outbuf; char name[1000]; - char *tmp, *path; + char *tmp, *tmp2 = NULL, *path; /* Load data */ outlen = spawn_and_store_stdout(XMLTV_FIND, NULL, &outbuf); @@ -665,7 +676,7 @@ static void _xmltv_load_grabbers ( void ) NULL }; path = strdup(tmp); - tmp = strtok(path, ":"); + tmp = strtok_r(path, ":", &tmp2); while (tmp) { DIR *dir; struct dirent *de; @@ -674,7 +685,8 @@ static void _xmltv_load_grabbers ( void ) while ((de = readdir(dir))) { if (strstr(de->d_name, XMLTV_GRAB) != de->d_name) continue; snprintf(bin, sizeof(bin), "%s/%s", tmp, de->d_name); - if (lstat(bin, &st)) continue; + if (epggrab_module_find_by_id(bin)) continue; + if (stat(bin, &st)) continue; if (!(st.st_mode & S_IEXEC)) continue; if (!S_ISREG(st.st_mode)) continue; if ((outlen = spawn_and_store_stdout(bin, argv, &outbuf)) > 0) { @@ -687,7 +699,7 @@ static void _xmltv_load_grabbers ( void ) } closedir(dir); } - tmp = strtok(NULL, ":"); + tmp = strtok_r(NULL, ":", &tmp2); } free(path); } diff --git a/src/epggrab/otamux.c b/src/epggrab/otamux.c index 80ace9d2..3669664c 100644 --- a/src/epggrab/otamux.c +++ b/src/epggrab/otamux.c @@ -89,9 +89,10 @@ th_dvb_mux_instance_t *epggrab_mux_next ( th_dvb_adapter_t *tda ) epggrab_ota_mux_t *ota; time(&now); TAILQ_FOREACH(ota, &ota_mux_all, glob_link) { + if (ota->tdmi->tdmi_adapter != tda) continue; if (ota->interval + ota->completed > now) return NULL; if (!ota->is_reg) return NULL; - if (ota->tdmi->tdmi_adapter == tda) break; + break; } return ota ? ota->tdmi : NULL; } @@ -136,6 +137,7 @@ void epggrab_ota_load ( void ) if ((l = htsmsg_get_list_by_field(f))) _epggrab_ota_load_one((epggrab_module_ota_t*)mod, l); } + htsmsg_destroy(m); } } @@ -171,6 +173,7 @@ void epggrab_ota_save ( void ) } hts_settings_save(m, "epggrab/otamux"); + htsmsg_destroy(m); } /* ************************************************************************** @@ -335,7 +338,21 @@ static void _epggrab_ota_finished ( epggrab_ota_mux_t *ota ) /* Reinsert into reg queue */ else { TAILQ_REMOVE(&ota_mux_all, ota, glob_link); - TAILQ_INSERT_SORTED(&ota_mux_all, ota, glob_link, _ota_time_cmp); + // Find the last queue entry that can run before ota + // (i.e _ota_time_cmp(ota, entry)>0) and re-insert ota + // directly after this entry. If no matching entry is + // found (i.e ota can run before any other entry), + // re-insert ota at the queue head. + epggrab_ota_mux_t *entry = NULL; + epggrab_ota_mux_t *tmp; + TAILQ_FOREACH(tmp, &ota_mux_all, glob_link) { + if(_ota_time_cmp(ota, tmp)>0) entry = tmp; + } + if (entry) { + TAILQ_INSERT_AFTER(&ota_mux_all, entry, ota, glob_link); + } else { + TAILQ_INSERT_HEAD(&ota_mux_all, ota, glob_link); + } } } diff --git a/src/filebundle.c b/src/filebundle.c index 82450a37..295ed00f 100644 --- a/src/filebundle.c +++ b/src/filebundle.c @@ -214,12 +214,13 @@ fb_dir *fb_opendir ( const char *path ) /* Bundle */ #if ENABLE_BUNDLE - char *tmp1 = strdup(path); - char *tmp2 = strtok(tmp1, "/"); + char *tmp1, *tmp2, *tmp3 = NULL; + tmp1 = strdup(path); + tmp2 = strtok_r(tmp1, "/", &tmp3); filebundle_entry_t *fb = filebundle_root; while (fb && tmp2) { if (fb->type == FB_DIR && !strcmp(fb->name, tmp2)) { - tmp2 = strtok(NULL, "/"); + tmp2 = strtok_r(NULL, "/", &tmp3); if (tmp2) fb = fb->d.child; } else { fb = fb->next; @@ -383,7 +384,7 @@ fb_file *fb_open2 } else { char path[512]; snprintf(path, sizeof(path), "%s/%s", dir->d.root, name); - FILE *fp = fopen(path, "r"); + FILE *fp = fopen(path, "rb"); if (fp) { struct stat st; lstat(path, &st); @@ -492,7 +493,8 @@ ssize_t fb_read ( fb_file *fp, void *buf, size_t count ) memcpy(buf, fp->buf + fp->pos, count); fp->pos += count; } else if (fp->type == FB_DIRECT) { - fp->pos += fread(buf, 1, count, fp->d.cur); + count = fread(buf, 1, count, fp->d.cur); + fp->pos += count; } else { count = MIN(count, fp->b.root->f.size - fp->pos); memcpy(buf, fp->b.root->f.data + fp->pos, count); diff --git a/src/htsbuf.c b/src/htsbuf.c index ff859c18..045fa071 100644 --- a/src/htsbuf.c +++ b/src/htsbuf.c @@ -1,6 +1,6 @@ /* * Buffer management functions - * Copyright (C) 2008 Andreas Öman + * Copyright (C) 2008 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 @@ -22,17 +22,9 @@ #include #include #include -#include "htsbuf.h" + #include "tvheadend.h" - -#ifndef MIN -#define MIN(a, b) ((a) < (b) ? (a) : (b)) -#endif - -#ifndef MAX -#define MAX(a, b) ((a) > (b) ? (a) : (b)) -#endif - +#include "htsbuf.h" /** * @@ -206,6 +198,7 @@ htsbuf_peek(htsbuf_queue_t *hq, void *buf, size_t len) c = MIN(hd->hd_data_len - hd->hd_data_off, len); memcpy(buf, hd->hd_data + hd->hd_data_off, c); + r += c; buf += c; len -= c; @@ -233,7 +226,7 @@ htsbuf_drop(htsbuf_queue_t *hq, size_t len) len -= c; hd->hd_data_off += c; hq->hq_size -= c; - + r += c; if(hd->hd_data_off == hd->hd_data_len) htsbuf_data_free(hq, hd); } @@ -314,19 +307,162 @@ htsbuf_appendq(htsbuf_queue_t *hq, htsbuf_queue_t *src) } -/** - * - */ -uint32_t -htsbuf_crc32(htsbuf_queue_t *hq, uint32_t crc) +void +htsbuf_dump_raw_stderr(htsbuf_queue_t *hq) { htsbuf_data_t *hd; - - TAILQ_FOREACH(hd, &hq->hq_q, hd_link) - crc = tvh_crc32(hd->hd_data + hd->hd_data_off, - hd->hd_data_len - hd->hd_data_off, - crc); - return crc; + char n = '\n'; + + TAILQ_FOREACH(hd, &hq->hq_q, hd_link) { + if(write(2, hd->hd_data + hd->hd_data_off, + hd->hd_data_len - hd->hd_data_off) + != hd->hd_data_len - hd->hd_data_off) + break; + } + if(write(2, &n, 1) != 1) + return; } +void +htsbuf_hexdump(htsbuf_queue_t *hq, const char *prefix) +{ + void *r = malloc(hq->hq_size); + htsbuf_peek(hq, r, hq->hq_size); + hexdump(prefix, r, hq->hq_size); + free(r); +} + + +/** + * + */ +void +htsbuf_append_and_escape_xml(htsbuf_queue_t *hq, const char *s) +{ + const char *c = s; + const char *e = s + strlen(s); + if(e == s) + return; + + while(1) { + const char *esc; + switch(*c++) { + case '<': esc = "<"; break; + case '>': esc = ">"; break; + case '&': esc = "&"; break; + case '\'': esc = "'"; break; + case '"': esc = """; break; + default: esc = NULL; break; + } + + if(esc != NULL) { + htsbuf_append(hq, s, c - s - 1); + htsbuf_append(hq, esc, strlen(esc)); + s = c; + } + + if(c == e) { + htsbuf_append(hq, s, c - s); + break; + } + } +} + + +/** + * + */ +void +htsbuf_append_and_escape_url(htsbuf_queue_t *hq, const char *s) +{ + const char *c = s; + const char *e = s + strlen(s); + char C; + if(e == s) + return; + + while(1) { + const char *esc; + char buf[4]; + C = *c++; + + if((C >= '0' && C <= '9') || + (C >= 'a' && C <= 'z') || + (C >= 'A' && C <= 'Z') || + C == '_' || + C == '~' || + C == '.' || + C == '-') { + esc = NULL; + } else { + static const char hexchars[16] = "0123456789ABCDEF"; + buf[0] = '%'; + buf[1] = hexchars[(C >> 4) & 0xf]; + buf[2] = hexchars[C & 0xf];; + buf[3] = 0; + esc = buf; + } + + if(esc != NULL) { + htsbuf_append(hq, s, c - s - 1); + htsbuf_append(hq, esc, strlen(esc)); + s = c; + } + + if(c == e) { + htsbuf_append(hq, s, c - s); + break; + } + } +} + + +/** + * + */ +void +htsbuf_append_and_escape_jsonstr(htsbuf_queue_t *hq, const char *str) +{ + const char *s = str; + + htsbuf_append(hq, "\"", 1); + + while(*s != 0) { + if(*s == '"' || *s == '\\' || *s == '\n' || *s == '\r' || *s == '\t') { + htsbuf_append(hq, str, s - str); + + if(*s == '"') + htsbuf_append(hq, "\\\"", 2); + else if(*s == '\n') + htsbuf_append(hq, "\\n", 2); + else if(*s == '\r') + htsbuf_append(hq, "\\r", 2); + else if(*s == '\t') + htsbuf_append(hq, "\\t", 2); + else + htsbuf_append(hq, "\\\\", 2); + s++; + str = s; + } else { + s++; + } + } + htsbuf_append(hq, str, s - str); + htsbuf_append(hq, "\"", 1); +} + + + +/** + * + */ +char * +htsbuf_to_string(htsbuf_queue_t *hq) +{ + char *r = malloc(hq->hq_size + 1); + r[hq->hq_size] = 0; + htsbuf_read(hq, r, hq->hq_size); + return r; +} + diff --git a/src/htsbuf.h b/src/htsbuf.h index 13aaedb5..ae9a1322 100644 --- a/src/htsbuf.h +++ b/src/htsbuf.h @@ -1,6 +1,6 @@ /* * Buffer management functions - * Copyright (C) 2008 Andreas Öman + * Copyright (C) 2008 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 @@ -20,10 +20,11 @@ #define HTSBUF_H__ #include -#include -#include "queue.h" #include +#include "queue.h" + + TAILQ_HEAD(htsbuf_data_queue, htsbuf_data); typedef struct htsbuf_data { @@ -48,15 +49,12 @@ void htsbuf_queue_flush(htsbuf_queue_t *hq); void htsbuf_vqprintf(htsbuf_queue_t *hq, const char *fmt, va_list ap); -void htsbuf_qprintf(htsbuf_queue_t *hq, const char *fmt, ...) - __attribute__((format(printf,2,3))); +void htsbuf_qprintf(htsbuf_queue_t *hq, const char *fmt, ...); void htsbuf_append(htsbuf_queue_t *hq, const void *buf, size_t len); void htsbuf_append_prealloc(htsbuf_queue_t *hq, const void *buf, size_t len); -void htsbuf_appendq(htsbuf_queue_t *hq, htsbuf_queue_t *src); - void htsbuf_data_free(htsbuf_queue_t *hq, htsbuf_data_t *hd); size_t htsbuf_read(htsbuf_queue_t *hq, void *buf, size_t len); @@ -67,6 +65,21 @@ size_t htsbuf_drop(htsbuf_queue_t *hq, size_t len); size_t htsbuf_find(htsbuf_queue_t *hq, uint8_t v); -uint32_t htsbuf_crc32(htsbuf_queue_t *q, uint32_t crc); +void htsbuf_appendq(htsbuf_queue_t *hq, htsbuf_queue_t *src); + +void htsbuf_append_and_escape_xml(htsbuf_queue_t *hq, const char *str); + +void htsbuf_append_and_escape_url(htsbuf_queue_t *hq, const char *s); + +void htsbuf_append_and_escape_jsonstr(htsbuf_queue_t *hq, const char *s); + +void htsbuf_dump_raw_stderr(htsbuf_queue_t *hq); + +char *htsbuf_to_string(htsbuf_queue_t *hq); + +struct rstr; +struct rstr *htsbuf_to_rstr(htsbuf_queue_t *hq, const char *prefix); + +void htsbuf_hexdump(htsbuf_queue_t *hq, const char *prefix); #endif /* HTSBUF_H__ */ diff --git a/src/htsmsg.c b/src/htsmsg.c index 5dd67e3a..62307dac 100644 --- a/src/htsmsg.c +++ b/src/htsmsg.c @@ -1,6 +1,6 @@ /* * Functions for manipulating HTS messages - * Copyright (C) 2007 Andreas Öman + * 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 @@ -27,10 +27,10 @@ static void htsmsg_clear(htsmsg_t *msg); -/* +/** * */ -static void +void htsmsg_field_destroy(htsmsg_t *msg, htsmsg_field_t *f) { TAILQ_REMOVE(&msg->hm_fields, f, hmf_link); @@ -102,7 +102,7 @@ htsmsg_field_add(htsmsg_t *msg, const char *name, int type, int flags) /* * */ -static htsmsg_field_t * +htsmsg_field_t * htsmsg_field_find(htsmsg_t *msg, const char *name) { htsmsg_field_t *f; @@ -186,6 +186,21 @@ htsmsg_add_u32(htsmsg_t *msg, const char *name, uint32_t u32) f->hmf_s64 = u32; } +/* + * + */ +int +htsmsg_set_u32(htsmsg_t *msg, const char *name, uint32_t u32) +{ + htsmsg_field_t *f = htsmsg_field_find(msg, name); + if (!f) + f = htsmsg_field_add(msg, name, HMF_S64, HMF_NAME_ALLOCED); + if (f->hmf_type != HMF_S64) + return 1; + f->hmf_s64 = u32; + return 0; +} + /* * */ @@ -196,16 +211,6 @@ htsmsg_add_s64(htsmsg_t *msg, const char *name, int64_t s64) f->hmf_s64 = s64; } -/* - * - */ -void -htsmsg_add_u64(htsmsg_t *msg, const char *name, uint64_t u64) -{ - htsmsg_field_t *f = htsmsg_field_add(msg, name, HMF_S64, HMF_NAME_ALLOCED); - f->hmf_s64 = u64; -} - /* * */ @@ -217,6 +222,17 @@ htsmsg_add_s32(htsmsg_t *msg, const char *name, int32_t s32) } +/* + * + */ +void +htsmsg_add_dbl(htsmsg_t *msg, const char *name, double dbl) +{ + htsmsg_field_t *f = htsmsg_field_add(msg, name, HMF_DBL, HMF_NAME_ALLOCED); + f->hmf_dbl = dbl; +} + + /* * @@ -267,8 +283,8 @@ htsmsg_add_msg(htsmsg_t *msg, const char *name, htsmsg_t *sub) HMF_NAME_ALLOCED); assert(sub->hm_data == NULL); - TAILQ_MOVE(&f->hmf_msg.hm_fields, &sub->hm_fields, hmf_link); f->hmf_msg.hm_islist = sub->hm_islist; + TAILQ_MOVE(&f->hmf_msg.hm_fields, &sub->hm_fields, hmf_link); free(sub); } @@ -312,10 +328,14 @@ htsmsg_get_s64(htsmsg_t *msg, const char *name, int64_t *s64p) case HMF_S64: *s64p = f->hmf_s64; break; + case HMF_DBL: + *s64p = f->hmf_dbl; + break; } return 0; } + /** * */ @@ -326,30 +346,6 @@ htsmsg_get_s64_or_default(htsmsg_t *msg, const char *name, int64_t def) return htsmsg_get_s64(msg, name, &s64) ? def : s64; } -/** - * - */ -int -htsmsg_get_u64(htsmsg_t *msg, const char *name, uint64_t *u64p) -{ - htsmsg_field_t *f; - - if((f = htsmsg_field_find(msg, name)) == NULL) - return HTSMSG_ERR_FIELD_NOT_FOUND; - - switch(f->hmf_type) { - default: - return HTSMSG_ERR_CONVERSION_IMPOSSIBLE; - case HMF_STR: - *u64p = strtoull(f->hmf_str, NULL, 0); - break; - case HMF_S64: - *u64p = f->hmf_s64; - break; - } - return 0; -} - /* * @@ -373,13 +369,26 @@ htsmsg_get_u32(htsmsg_t *msg, const char *name, uint32_t *u32p) /** * */ -uint32_t +int htsmsg_get_u32_or_default(htsmsg_t *msg, const char *name, uint32_t def) { uint32_t u32; return htsmsg_get_u32(msg, name, &u32) ? def : u32; } + +/** + * + */ +int32_t +htsmsg_get_s32_or_default(htsmsg_t *msg, const char *name, int32_t def) +{ + int32_t s32; + return htsmsg_get_s32(msg, name, &s32) ? def : s32; +} + + + /* * */ @@ -400,6 +409,25 @@ htsmsg_get_s32(htsmsg_t *msg, const char *name, int32_t *s32p) } +/* + * + */ +int +htsmsg_get_dbl(htsmsg_t *msg, const char *name, double *dblp) +{ + htsmsg_field_t *f; + + if((f = htsmsg_field_find(msg, name)) == NULL) + return HTSMSG_ERR_FIELD_NOT_FOUND; + + if(f->hmf_type != HMF_DBL) + return HTSMSG_ERR_CONVERSION_IMPOSSIBLE; + + *dblp = f->hmf_dbl; + return 0; +} + + /* * */ @@ -457,13 +485,6 @@ htsmsg_get_str(htsmsg_t *msg, const char *name) } -const char * -htsmsg_get_str_or_default(htsmsg_t *msg, const char *name, const char *def) -{ - const char *str = htsmsg_get_str(msg, name); - return str ?: def; -} - /* * */ @@ -493,6 +514,32 @@ htsmsg_get_map_multi(htsmsg_t *msg, ...) return msg; } +/** + * + */ +const char * +htsmsg_get_str_multi(htsmsg_t *msg, ...) +{ + va_list ap; + const char *n; + htsmsg_field_t *f; + va_start(ap, msg); + + while((n = va_arg(ap, char *)) != NULL) { + if((f = htsmsg_field_find(msg, n)) == NULL) + return NULL; + else if(f->hmf_type == HMF_STR) + return f->hmf_str; + else if(f->hmf_type == HMF_MAP) + msg = &f->hmf_msg; + else + return NULL; + } + return NULL; +} + + + /* * */ @@ -565,6 +612,10 @@ htsmsg_print0(htsmsg_t *msg, int indent) case HMF_S64: printf("S64) = %" PRId64 "\n", f->hmf_s64); break; + + case HMF_DBL: + printf("DBL) = %f\n", f->hmf_dbl); + break; } } } @@ -611,6 +662,10 @@ htsmsg_copy_i(htsmsg_t *src, htsmsg_t *dst) case HMF_BIN: htsmsg_add_bin(dst, f->hmf_name, f->hmf_bin, f->hmf_binsize); break; + + case HMF_DBL: + htsmsg_add_dbl(dst, f->hmf_name, f->hmf_dbl); + break; } } } @@ -623,13 +678,44 @@ htsmsg_copy(htsmsg_t *src) return dst; } +/** + * + */ +htsmsg_t * +htsmsg_get_map_in_list(htsmsg_t *m, int num) +{ + htsmsg_field_t *f; + + HTSMSG_FOREACH(f, m) { + if(!--num) + return htsmsg_get_map_by_field(f); + } + return NULL; +} + /** * */ -void -htsmsg_dtor(htsmsg_t **mp) +htsmsg_t * +htsmsg_get_map_by_field_if_name(htsmsg_field_t *f, const char *name) { - if(*mp != NULL) - htsmsg_destroy(*mp); + if(f->hmf_type != HMF_MAP) + return NULL; + if(strcmp(f->hmf_name, name)) + return NULL; + return &f->hmf_msg; } + + +/** + * + */ +const char * +htsmsg_get_cdata(htsmsg_t *m, const char *field) +{ + return htsmsg_get_str_multi(m, field, "cdata", NULL); +} + + + diff --git a/src/htsmsg.h b/src/htsmsg.h index d7d2bb05..05beb2cf 100644 --- a/src/htsmsg.h +++ b/src/htsmsg.h @@ -1,6 +1,6 @@ /* * Functions for manipulating HTS messages - * Copyright (C) 2007 Andreas Öman + * 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 @@ -16,9 +16,8 @@ * along with this program. If not, see . */ -#ifndef HTSMSG_H_ -#define HTSMSG_H_ - +#pragma once +#include #include #include "queue.h" @@ -50,6 +49,7 @@ typedef struct htsmsg { #define HMF_STR 3 #define HMF_BIN 4 #define HMF_LIST 5 +#define HMF_DBL 6 typedef struct htsmsg_field { TAILQ_ENTRY(htsmsg_field) hmf_link; @@ -68,6 +68,7 @@ typedef struct htsmsg_field { size_t len; } bin; htsmsg_t msg; + double dbl; } u; } htsmsg_field_t; @@ -76,11 +77,12 @@ typedef struct htsmsg_field { #define hmf_str u.str #define hmf_bin u.bin.data #define hmf_binsize u.bin.len +#define hmf_dbl u.dbl #define htsmsg_get_map_by_field(f) \ ((f)->hmf_type == HMF_MAP ? &(f)->hmf_msg : NULL) #define htsmsg_get_list_by_field(f) \ - ((f)->hmf_type == HMF_LIST ? &(f)->hmf_msg : NULL) + ((f)->hmf_type == HMF_LIST ? &(f)->hmf_msg : NULL) #define HTSMSG_FOREACH(f, msg) TAILQ_FOREACH(f, &(msg)->hm_fields, hmf_link) @@ -94,6 +96,11 @@ htsmsg_t *htsmsg_create_map(void); */ htsmsg_t *htsmsg_create_list(void); +/** + * Remove a given field from a msg + */ +void htsmsg_field_destroy(htsmsg_t *msg, htsmsg_field_t *f); + /** * Destroys a message (map or list) */ @@ -104,16 +111,16 @@ void htsmsg_destroy(htsmsg_t *msg); */ void htsmsg_add_u32(htsmsg_t *msg, const char *name, uint32_t u32); +/** + * Add/update an integer field + */ +int htsmsg_set_u32(htsmsg_t *msg, const char *name, uint32_t u32); + /** * Add an integer field where source is signed 32 bit. */ void htsmsg_add_s32(htsmsg_t *msg, const char *name, int32_t s32); -/** - * Add an integer field where source is unsigned 64 bit. - */ -void htsmsg_add_u64(htsmsg_t *msg, const char *name, uint64_t u64); - /** * Add an integer field where source is signed 64 bit. */ @@ -129,6 +136,11 @@ void htsmsg_add_str(htsmsg_t *msg, const char *name, const char *str); */ void htsmsg_add_msg(htsmsg_t *msg, const char *name, htsmsg_t *sub); +/** + * Add an field where source is a double + */ +void htsmsg_add_dbl(htsmsg_t *msg, const char *name, double dbl); + /** * Add an field where source is a list or map message. * @@ -160,15 +172,6 @@ void htsmsg_add_binptr(htsmsg_t *msg, const char *name, const void *bin, */ int htsmsg_get_u32(htsmsg_t *msg, const char *name, uint32_t *u32p); -/** - * Return the field \p name as an u32. - * - * @return An unsigned 32 bit integer or def if the field can not be found - * or if conversion is not possible. - */ -uint32_t htsmsg_get_u32_or_default - (htsmsg_t *msg, const char *name, uint32_t def); - /** * Get an integer as an signed 32 bit integer. * @@ -187,24 +190,11 @@ int htsmsg_get_s32(htsmsg_t *msg, const char *name, int32_t *s32p); */ int htsmsg_get_s64(htsmsg_t *msg, const char *name, int64_t *s64p); - -/** +/* * Return the field \p name as an s64. - * - * @return A signed 64 bit integer or def if the field can not be found - * or if conversion is not possible. */ -int64_t htsmsg_get_s64_or_default - (htsmsg_t *msg, const char *name, int64_t def); +int64_t htsmsg_get_s64_or_default(htsmsg_t *msg, const char *name, int64_t def); -/** - * Get an integer as an unsigned 64 bit integer. - * - * @return HTSMSG_ERR_FIELD_NOT_FOUND - Field does not exist - * HTSMSG_ERR_CONVERSION_IMPOSSIBLE - Field is not an integer or - * out of range for the requested storage. - */ -int htsmsg_get_u64(htsmsg_t *msg, const char *name, uint64_t *u64p); /** * Get pointer to a binary field. No copying of data is performed. @@ -236,15 +226,6 @@ htsmsg_t *htsmsg_get_list(htsmsg_t *msg, const char *name); */ const char *htsmsg_get_str(htsmsg_t *msg, const char *name); -/** - * Get a field of type 'string'. No copying is done. - * - * @return def if the field can not be found or not of string type. - * Otherwise a pointer to the data is returned. - */ -const char *htsmsg_get_str_or_default - (htsmsg_t *msg, const char *name, const char *def); - /** * Get a field of type 'map'. No copying is done. * @@ -256,7 +237,23 @@ htsmsg_t *htsmsg_get_map(htsmsg_t *msg, const char *name); /** * Traverse a hierarchy of htsmsg's to find a specific child. */ -htsmsg_t *htsmsg_get_map_multi(htsmsg_t *msg, ...); +htsmsg_t *htsmsg_get_map_multi(htsmsg_t *msg, ...) + __attribute__((__sentinel__(0))); + +/** + * Traverse a hierarchy of htsmsg's to find a specific child. + */ +const char *htsmsg_get_str_multi(htsmsg_t *msg, ...) + __attribute__((__sentinel__(0))); + +/** + * Get a field of type 'double'. + * + * @return HTSMSG_ERR_FIELD_NOT_FOUND - Field does not exist + * HTSMSG_ERR_CONVERSION_IMPOSSIBLE - Field is not an integer or + * out of range for the requested storage. + */ +int htsmsg_get_dbl(htsmsg_t *msg, const char *name, double *dblp); /** * Given the field \p f, return a string if it is of type string, otherwise @@ -264,6 +261,23 @@ htsmsg_t *htsmsg_get_map_multi(htsmsg_t *msg, ...); */ const char *htsmsg_field_get_string(htsmsg_field_t *f); +/** + * Return the field \p name as an u32. + * + * @return An unsigned 32 bit integer or NULL if the field can not be found + * or if conversion is not possible. + */ +int htsmsg_get_u32_or_default(htsmsg_t *msg, const char *name, uint32_t def); + +/** + * Return the field \p name as an s32. + * + * @return A signed 32 bit integer or NULL if the field can not be found + * or if conversion is not possible. + */ +int32_t htsmsg_get_s32_or_default(htsmsg_t *msg, const char *name, + int32_t def); + /** * Remove the given field called \p name from the message \p msg. */ @@ -288,6 +302,12 @@ void htsmsg_print(htsmsg_t *msg); htsmsg_field_t *htsmsg_field_add(htsmsg_t *msg, const char *name, int type, int flags); +/** + * Get a field, return NULL if it does not exist + */ +htsmsg_field_t *htsmsg_field_find(htsmsg_t *msg, const char *name); + + /** * Clone a message. */ @@ -296,8 +316,12 @@ htsmsg_t *htsmsg_copy(htsmsg_t *src); #define HTSMSG_FOREACH(f, msg) TAILQ_FOREACH(f, &(msg)->hm_fields, hmf_link) -extern void htsmsg_dtor(htsmsg_t **mp); +/** + * Misc + */ +htsmsg_t *htsmsg_get_map_in_list(htsmsg_t *m, int num); -#define htsmsg_autodtor(n) htsmsg_t *n __attribute__((cleanup(htsmsg_dtor))) +htsmsg_t *htsmsg_get_map_by_field_if_name(htsmsg_field_t *f, const char *name); + +const char *htsmsg_get_cdata(htsmsg_t *m, const char *field); -#endif /* HTSMSG_H_ */ diff --git a/src/htsmsg_json.c b/src/htsmsg_json.c index ff1aa6f7..3fda47c1 100644 --- a/src/htsmsg_json.c +++ b/src/htsmsg_json.c @@ -25,54 +25,19 @@ #include "htsmsg_json.h" #include "htsbuf.h" +#include "misc/json.h" +#include "misc/dbl.h" + /** * */ static void -htsmsg_json_encode_string(const char *str, htsbuf_queue_t *hq) -{ - const char *s = str; - - htsbuf_append(hq, "\"", 1); - - while(*s != 0) { - if(*s == '"' || *s == '\\' || *s == '\n' || *s == '\t' || *s == '\r') { - htsbuf_append(hq, str, s - str); - - if(*s == '"') - htsbuf_append(hq, "\\\"", 2); - else if(*s == '\n') - htsbuf_append(hq, "\\n", 2); - else if(*s == '\t') - htsbuf_append(hq, "\\t", 2); - else if(*s == '\r') - htsbuf_append(hq, "\\r", 2); - else - htsbuf_append(hq, "\\\\", 2); - s++; - str = s; - } else { - s++; - } - } - htsbuf_append(hq, str, s - str); - htsbuf_append(hq, "\"", 1); -} - - -/* - * Note to future: - * If your about to add support for numbers with decimal point, - * remember to always serialize them with '.' as decimal point character - * no matter what current locale says. This is according to the JSON spec. - */ -static void htsmsg_json_write(htsmsg_t *msg, htsbuf_queue_t *hq, int isarray, int indent, int pretty) { htsmsg_field_t *f; - char buf[30]; + char buf[100]; static const char *indentor = "\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; htsbuf_append(hq, isarray ? "[" : "{", 1); @@ -83,7 +48,7 @@ htsmsg_json_write(htsmsg_t *msg, htsbuf_queue_t *hq, int isarray, htsbuf_append(hq, indentor, indent < 16 ? indent : 16); if(!isarray) { - htsmsg_json_encode_string(f->hmf_name ?: "noname", hq); + htsbuf_append_and_escape_jsonstr(hq, f->hmf_name ?: "noname"); htsbuf_append(hq, ": ", 2); } @@ -97,11 +62,11 @@ htsmsg_json_write(htsmsg_t *msg, htsbuf_queue_t *hq, int isarray, break; case HMF_STR: - htsmsg_json_encode_string(f->hmf_str, hq); + htsbuf_append_and_escape_jsonstr(hq, f->hmf_str); break; case HMF_BIN: - htsmsg_json_encode_string("binary", hq); + htsbuf_append_and_escape_jsonstr(hq, "binary"); break; case HMF_S64: @@ -109,6 +74,11 @@ htsmsg_json_write(htsmsg_t *msg, htsbuf_queue_t *hq, int isarray, htsbuf_append(hq, buf, strlen(buf)); break; + case HMF_DBL: + my_double2str(buf, sizeof(buf), f->hmf_dbl); + htsbuf_append(hq, buf, strlen(buf)); + break; + default: abort(); } @@ -125,343 +95,109 @@ htsmsg_json_write(htsmsg_t *msg, htsbuf_queue_t *hq, int isarray, /** * */ -int +void htsmsg_json_serialize(htsmsg_t *msg, htsbuf_queue_t *hq, int pretty) { htsmsg_json_write(msg, hq, msg->hm_islist, 2, pretty); if(pretty) htsbuf_append(hq, "\n", 1); - return 0; -} - - - -static const char *htsmsg_json_parse_value(const char *s, - htsmsg_t *parent, char *name); - -/** - * - */ -static char * -htsmsg_json_parse_string(const char *s, const char **endp) -{ - const char *start; - char *r, *a, *b; - int l, esc = 0; - - while(*s > 0 && *s < 33) - s++; - - if(*s != '"') - return NULL; - - s++; - start = s; - - while(1) { - if(*s == 0) - return NULL; - - if(*s == '\\') { - esc = 1; - /* skip the escape */ - s++; - if (*s == 'u') s += 4; - // Note: we could detect the lack of support here! - } else if(*s == '"') { - - *endp = s + 1; - - /* End */ - l = s - start; - r = malloc(l + 1); - memcpy(r, start, l); - r[l] = 0; - - if(esc) { - /* Do deescaping inplace */ - - a = b = r; - - while(*a) { - if(*a == '\\') { - a++; - if(*a == 'b') - *b++ = '\b'; - else if(*a == '\\') - *b++ = '\\'; - else if(*a == 'f') - *b++ = '\f'; - else if(*a == 'n') - *b++ = '\n'; - else if(*a == 'r') - *b++ = '\r'; - else if(*a == 't') - *b++ = '\t'; - else if(*a == 'u') { - /* 4 hexdigits: Not supported */ - free(r); - return NULL; - } else { - *b++ = *a; - } - a++; - } else { - *b++ = *a++; - } - } - *b = 0; - } - return r; - } - s++; - } } /** * */ -static htsmsg_t * -htsmsg_json_parse_object(const char *s, const char **endp) +char * +htsmsg_json_serialize_to_str(htsmsg_t *msg, int pretty) { - char *name; - const char *s2; - htsmsg_t *r; - - while(*s > 0 && *s < 33) - s++; - - if(*s != '{') - return NULL; - - s++; - - r = htsmsg_create_map(); - - while(*s > 0 && *s < 33) - s++; - - if(*s != '}') while(1) { - - if((name = htsmsg_json_parse_string(s, &s2)) == NULL) { - htsmsg_destroy(r); - return NULL; - } - - s = s2; - - while(*s > 0 && *s < 33) - s++; - - if(*s != ':') { - htsmsg_destroy(r); - free(name); - return NULL; - } - s++; - - s2 = htsmsg_json_parse_value(s, r, name); - free(name); - - if(s2 == NULL) { - htsmsg_destroy(r); - return NULL; - } - - s = s2; - - while(*s > 0 && *s < 33) - s++; - - if(*s == '}') - break; - - if(*s != ',') { - htsmsg_destroy(r); - return NULL; - } - s++; - } - - s++; - *endp = s; - return r; -} - - -/** - * - */ -static htsmsg_t * -htsmsg_json_parse_array(const char *s, const char **endp) -{ - const char *s2; - htsmsg_t *r; - - while(*s > 0 && *s < 33) - s++; - - if(*s != '[') - return NULL; - - s++; - - r = htsmsg_create_list(); - - while(*s > 0 && *s < 33) - s++; - - if(*s != ']') { - - while(1) { - - s2 = htsmsg_json_parse_value(s, r, NULL); - - if(s2 == NULL) { - htsmsg_destroy(r); - return NULL; - } - - s = s2; - - while(*s > 0 && *s < 33) - s++; - - if(*s == ']') - break; - - if(*s != ',') { - htsmsg_destroy(r); - return NULL; - } - s++; - } - } - s++; - *endp = s; - return r; -} - -/* - * locale independent strtod. - * does not support hex floats as the standard strtod - */ -static double -_strntod(const char *s, char decimal_point_char, char **ep) -{ - static char locale_decimal_point_char = 0; - char buf[64]; - const char *c; - double d; - - /* ugly but very portable way to get decimal point char */ - if(locale_decimal_point_char == 0) { - snprintf(buf, sizeof(buf), "%f", 0.0); - locale_decimal_point_char = buf[1]; - assert(locale_decimal_point_char != 0); - } - - for(c = s; - *c != '\0' && - ((*c > 0 && *c < 33) || /* skip whitespace */ - (*c == decimal_point_char || strchr("+-0123456789", *c) != NULL)); c++) - ; - - strncpy(buf, s, c - s); - buf[c - s] = '\0'; - - /* replace if specified char is not same as current locale */ - if(decimal_point_char != locale_decimal_point_char) { - char *r = strchr(buf, decimal_point_char); - if(r != NULL) - *r = locale_decimal_point_char; - } - - d = strtod(buf, ep); - - /* figure out offset in original string */ - if(ep != NULL) - *ep = (char *)s + (*ep - buf); - - return d; -} - -/** - * - */ -static char * -htsmsg_json_parse_number(const char *s, double *dp) -{ - char *ep; - double d = _strntod(s, '.', &ep); - - if(ep == s) - return NULL; - - *dp = d; - return ep; -} - -/** - * - */ -static const char * -htsmsg_json_parse_value(const char *s, htsmsg_t *parent, char *name) -{ - const char *s2; + htsbuf_queue_t hq; char *str; - double d = 0; - htsmsg_t *c; - - if((c = htsmsg_json_parse_object(s, &s2)) != NULL) { - htsmsg_add_msg(parent, name, c); - return s2; - } else if((c = htsmsg_json_parse_array(s, &s2)) != NULL) { - htsmsg_add_msg(parent, name, c); - return s2; - } else if((str = htsmsg_json_parse_string(s, &s2)) != NULL) { - htsmsg_add_str(parent, name, str); - free(str); - return s2; - } else if((s2 = htsmsg_json_parse_number(s, &d)) != NULL) { - htsmsg_add_s64(parent, name, d); - return s2; - } - - if(!strncmp(s, "true", 4)) { - htsmsg_add_u32(parent, name, 1); - return s + 4; - } - - if(!strncmp(s, "false", 5)) { - htsmsg_add_u32(parent, name, 0); - return s + 5; - } - - if(!strncmp(s, "null", 4)) { - /* Don't add anything */ - return s + 4; - } - return NULL; + htsbuf_queue_init(&hq, 0); + htsmsg_json_serialize(msg, &hq, pretty); + str = htsbuf_to_string(&hq); + htsbuf_queue_flush(&hq); + return str; } +/** + * + */ +static void * +create_map(void *opaque) +{ + return htsmsg_create_map(); +} + +static void * +create_list(void *opaque) +{ + return htsmsg_create_list(); +} + +static void +destroy_obj(void *opaque, void *obj) +{ + return htsmsg_destroy(obj); +} + +static void +add_obj(void *opaque, void *parent, const char *name, void *child) +{ + htsmsg_add_msg(parent, name, child); +} + +static void +add_string(void *opaque, void *parent, const char *name, char *str) +{ + htsmsg_add_str(parent, name, str); + free(str); +} + +static void +add_long(void *opaque, void *parent, const char *name, long v) +{ + htsmsg_add_s64(parent, name, v); +} + +static void +add_double(void *opaque, void *parent, const char *name, double v) +{ + htsmsg_add_dbl(parent, name, v); +} + +static void +add_bool(void *opaque, void *parent, const char *name, int v) +{ + htsmsg_add_u32(parent, name, v); +} + +static void +add_null(void *opaque, void *parent, const char *name) +{ +} + +/** + * + */ +static const json_deserializer_t json_to_htsmsg = { + .jd_create_map = create_map, + .jd_create_list = create_list, + .jd_destroy_obj = destroy_obj, + .jd_add_obj = add_obj, + .jd_add_string = add_string, + .jd_add_long = add_long, + .jd_add_double = add_double, + .jd_add_bool = add_bool, + .jd_add_null = add_null, +}; + + /** * */ htsmsg_t * htsmsg_json_deserialize(const char *src) { - const char *end; - htsmsg_t *c; - - if((c = htsmsg_json_parse_object(src, &end)) != NULL) - return c; - - if((c = htsmsg_json_parse_array(src, &end)) != NULL) { - c->hm_islist = 1; - return c; - } - return NULL; + return json_deserialize(src, &json_to_htsmsg, NULL, NULL, 0); } diff --git a/src/htsmsg_json.h b/src/htsmsg_json.h index da6b2504..fae3c457 100644 --- a/src/htsmsg_json.h +++ b/src/htsmsg_json.h @@ -1,6 +1,6 @@ /* * Functions converting HTSMSGs to/from JSON - * Copyright (C) 2008 Andreas Öman + * Copyright (C) 2008 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 @@ -21,12 +21,16 @@ #include "htsmsg.h" #include "htsbuf.h" - +struct rstr; /** * htsmsg_binary_deserialize */ htsmsg_t *htsmsg_json_deserialize(const char *src); -int htsmsg_json_serialize(htsmsg_t *msg, htsbuf_queue_t *hq, int pretty); +void htsmsg_json_serialize(htsmsg_t *msg, htsbuf_queue_t *hq, int pretty); + +char *htsmsg_json_serialize_to_str(htsmsg_t *msg, int pretty); + +struct rstr *htsmsg_json_serialize_to_rstr(htsmsg_t *msg, const char *prefix); #endif /* HTSMSG_JSON_H_ */ diff --git a/src/htsp.c b/src/htsp_server.c similarity index 71% rename from src/htsp.c rename to src/htsp_server.c index 789c036b..88fe3bdd 100644 --- a/src/htsp.c +++ b/src/htsp_server.c @@ -1,6 +1,6 @@ /* * tvheadend, HTSP interface - * Copyright (C) 2007 Andreas Öman + * 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 @@ -28,6 +28,7 @@ #include #include #include +#include #include "tvheadend.h" #include "channels.h" @@ -35,12 +36,19 @@ #include "tcp.h" #include "packet.h" #include "access.h" -#include "htsp.h" +#include "htsp_server.h" #include "streaming.h" #include "psi.h" #include "htsmsg_binary.h" #include "epg.h" - +#include "plumbing/tsfix.h" +#include "imagecache.h" +#if ENABLE_TIMESHIFT +#include "timeshift.h" +#endif +#if ENABLE_LIBAV +#include "plumbing/transcoding.h" +#endif #include #include "settings.h" #include @@ -49,9 +57,9 @@ * Datatypes and variables * *************************************************************************/ -static void *htsp_server; +static void *htsp_server, *htsp_server_2; -#define HTSP_PROTO_VERSION 6 +#define HTSP_PROTO_VERSION 11 #define HTSP_ASYNC_OFF 0x00 #define HTSP_ASYNC_ON 0x01 @@ -63,11 +71,13 @@ extern char *dvr_storage; LIST_HEAD(htsp_connection_list, htsp_connection); LIST_HEAD(htsp_subscription_list, htsp_subscription); +LIST_HEAD(htsp_file_list, htsp_file); TAILQ_HEAD(htsp_msg_queue, htsp_msg); TAILQ_HEAD(htsp_msg_q_queue, htsp_msg_q); static struct htsp_connection_list htsp_async_connections; +static struct htsp_connection_list htsp_connections; static void htsp_streaming_input(void *opaque, streaming_message_t *sm); @@ -104,8 +114,10 @@ typedef struct htsp_msg_q { * */ typedef struct htsp_connection { + LIST_ENTRY(htsp_connection) htsp_link; + int htsp_fd; - struct sockaddr_in *htsp_peer; + struct sockaddr_storage *htsp_peer; uint32_t htsp_version; @@ -137,10 +149,9 @@ typedef struct htsp_connection { htsp_msg_q_t htsp_hmq_epg; htsp_msg_q_t htsp_hmq_qstatus; - /** - * - */ struct htsp_subscription_list htsp_subscriptions; + struct htsp_file_list htsp_files; + int htsp_file_id; uint32_t htsp_granted_access; @@ -162,6 +173,15 @@ typedef struct htsp_subscription { th_subscription_t *hs_s; // Temporary streaming_target_t hs_input; + streaming_target_t *hs_tsfix; + +#if ENABLE_TIMESHIFT + streaming_target_t *hs_tshift; +#endif + +#if ENABLE_LIBAV +streaming_target_t *hs_transcoder; +#endif htsp_msg_q_t hs_q; @@ -169,8 +189,27 @@ typedef struct htsp_subscription { int hs_dropstats[PKT_NTYPES]; + int hs_90khz; + + int hs_queue_depth; + } htsp_subscription_t; + +/** + * + */ +typedef struct htsp_file { + LIST_ENTRY(htsp_file) hf_link; + int hf_id; // ID sent to client + int hf_fd; // Our file descriptor + char *hf_path; // For logging +} htsp_file_t; + + + +#define HTSP_DEFAULT_QUEUE_DEPTH 500000 + /* ************************************************************************** * Support routines * *************************************************************************/ @@ -234,6 +273,10 @@ htsp_flush_queue(htsp_connection_t *htsp, htsp_msg_q_t *hmq) TAILQ_REMOVE(&hmq->hmq_q, hm, hm_link); htsp_msg_destroy(hm); } + + // reset + hmq->hmq_length = 0; + hmq->hmq_payload = 0; pthread_mutex_unlock(&htsp->htsp_out_mutex); } @@ -245,7 +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); } @@ -334,6 +393,72 @@ htsp_generate_challenge(htsp_connection_t *htsp) return n != 32; } +/* ************************************************************************** + * File helpers + * *************************************************************************/ + +/** + * + */ +static htsmsg_t * +htsp_file_open(htsp_connection_t *htsp, const char *path, int fd) +{ + struct stat st; + + if (fd <= 0) { + fd = open(path, O_RDONLY); + tvhlog(LOG_DEBUG, "HTSP", "Opening file %s -- %s", path, fd < 0 ? strerror(errno) : "OK"); + if(fd == -1) + return htsp_error("Unable to open file"); + } + + htsp_file_t *hf = calloc(1, sizeof(htsp_file_t)); + hf->hf_fd = fd; + hf->hf_id = ++htsp->htsp_file_id; + hf->hf_path = strdup(path); + LIST_INSERT_HEAD(&htsp->htsp_files, hf, hf_link); + + htsmsg_t *rep = htsmsg_create_map(); + htsmsg_add_u32(rep, "id", hf->hf_id); + + if(!fstat(hf->hf_fd, &st)) { + htsmsg_add_s64(rep, "size", st.st_size); + htsmsg_add_s64(rep, "mtime", st.st_mtime); + } + + return rep; +} + +/** + * + */ +static htsp_file_t * +htsp_file_find(const htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_file_t *hf; + + int id = htsmsg_get_u32_or_default(in, "id", 0); + + LIST_FOREACH(hf, &htsp->htsp_files, hf_link) { + if(hf->hf_id == id) + return hf; + } + return NULL; +} + +/** + * + */ +static void +htsp_file_destroy(htsp_file_t *hf) +{ + tvhlog(LOG_DEBUG, "HTSP", "Closed opened file %s", hf->hf_path); + free(hf->hf_path); + close(hf->hf_fd); + LIST_REMOVE(hf, hf_link); + free(hf); +} + /* ************************************************************************** * Output message generators * *************************************************************************/ @@ -342,7 +467,7 @@ htsp_generate_challenge(htsp_connection_t *htsp) * */ static htsmsg_t * -htsp_build_channel(channel_t *ch, const char *method) +htsp_build_channel(channel_t *ch, const char *method, htsp_connection_t *htsp) { channel_tag_mapping_t *ctm; channel_tag_t *ct; @@ -357,8 +482,33 @@ htsp_build_channel(channel_t *ch, const char *method) htsmsg_add_u32(out, "channelNumber", ch->ch_number); htsmsg_add_str(out, "channelName", ch->ch_name); - if(ch->ch_icon != NULL) - htsmsg_add_str(out, "channelIcon", ch->ch_icon); + if(ch->ch_icon != NULL) { + uint32_t id; + struct sockaddr_storage addr; + socklen_t addrlen; + if ((id = imagecache_get_id(ch->ch_icon))) { + size_t p = 0; + char url[256]; + char buf[50]; + if (htsp->htsp_version < 8) { + addrlen = sizeof(addr); + getsockname(htsp->htsp_fd, (struct sockaddr*)&addr, &addrlen); + tcp_get_ip_str((struct sockaddr*)&addr, buf, 50); + strcpy(url, "http://"); + p = strlen(url); + p += snprintf(url+p, sizeof(url)-p, "%s%s%s:%hd%s", + (addr.ss_family == AF_INET6)?"[":"", + buf, + (addr.ss_family == AF_INET6)?"]":"", + tvheadend_webui_port, + tvheadend_webroot ?: ""); + } + snprintf(url+p, sizeof(url)-p, "/imagecache/%d", id); + htsmsg_add_str(out, "channelIcon", url); + } else { + htsmsg_add_str(out, "channelIcon", ch->ch_icon); + } + } now = ch->ch_epg_now; next = ch->ch_epg_next; @@ -423,9 +573,12 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) { htsmsg_t *out = htsmsg_create_map(); const char *s = NULL, *error = NULL; + const char *p; + dvr_config_t *cfg; htsmsg_add_u32(out, "id", de->de_id); - htsmsg_add_u32(out, "channel", de->de_channel->ch_id); + if (de->de_channel) + htsmsg_add_u32(out, "channel", de->de_channel->ch_id); htsmsg_add_s64(out, "start", de->de_start); htsmsg_add_s64(out, "stop", de->de_stop); @@ -435,6 +588,13 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) if( de->de_desc && (s = lang_str_get(de->de_desc, NULL))) htsmsg_add_str(out, "description", s); + if( de->de_filename && de->de_config_name ) { + if ((cfg = dvr_config_find_by_name_default(de->de_config_name))) { + if ((p = tvh_strbegins(de->de_filename, cfg->dvr_storage))) + htsmsg_add_str(out, "path", p); + } + } + switch(de->de_sched_state) { case DVR_SCHEDULED: s = "scheduled"; @@ -444,7 +604,9 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) break; case DVR_COMPLETED: s = "completed"; - if(de->de_last_error) + if(dvr_get_filesize(de) == -1) + error = "File missing"; + else if(de->de_last_error) error = streaming_code2txt(de->de_last_error); break; case DVR_MISSED_TIME: @@ -570,7 +732,7 @@ htsp_build_event static htsmsg_t * htsp_method_hello(htsp_connection_t *htsp, htsmsg_t *in) { - htsmsg_t *l, *r = htsmsg_create_map(); + htsmsg_t *r; uint32_t v; const char *name; @@ -580,19 +742,22 @@ htsp_method_hello(htsp_connection_t *htsp, htsmsg_t *in) if((name = htsmsg_get_str(in, "clientname")) == NULL) return htsp_error("Missing argument 'clientname'"); + r = htsmsg_create_map(); + tvh_str_update(&htsp->htsp_clientname, htsmsg_get_str(in, "clientname")); - tvhlog(LOG_INFO, "htsp", "%s: Welcomed client software: %s", - htsp->htsp_logname, name); + tvhlog(LOG_INFO, "htsp", "%s: Welcomed client software: %s (HTSPv%d)", + htsp->htsp_logname, name, v); htsmsg_add_u32(r, "htspversion", HTSP_PROTO_VERSION); htsmsg_add_str(r, "servername", "HTS Tvheadend"); htsmsg_add_str(r, "serverversion", tvheadend_version); htsmsg_add_bin(r, "challenge", htsp->htsp_challenge, 32); + if (tvheadend_webroot) + htsmsg_add_str(r, "webroot", tvheadend_webroot); /* Capabilities */ - l = htsmsg_create_list(); - htsmsg_add_msg(r, "servercapability", l); + htsmsg_add_msg(r, "servercapability", tvheadend_capabilities_list(1)); /* Set version to lowest num */ htsp->htsp_version = MIN(HTSP_PROTO_VERSION, v); @@ -694,7 +859,7 @@ htsp_method_async(htsp_connection_t *htsp, htsmsg_t *in) /* Send all channels */ RB_FOREACH(ch, &channel_name_tree, ch_name_link) - htsp_send_message(htsp, htsp_build_channel(ch, "channelAdd"), NULL); + htsp_send_message(htsp, htsp_build_channel(ch, "channelAdd", htsp), NULL); /* Send all enabled and external tags (now with channel mappings) */ TAILQ_FOREACH(ct, &channel_tags, ct_link) @@ -738,7 +903,7 @@ htsp_method_getEvent(htsp_connection_t *htsp, htsmsg_t *in) if(htsmsg_get_u32(in, "eventId", &eventId)) return htsp_error("Missing argument 'eventId'"); - lang = htsmsg_get_str_or_default(in, "language", htsp->htsp_language); + lang = htsmsg_get_str(in, "language") ?: htsp->htsp_language; if((e = epg_broadcast_find_by_id(eventId, NULL)) == NULL) return htsp_error("Event does not exist"); @@ -772,7 +937,7 @@ htsp_method_getEvents(htsp_connection_t *htsp, htsmsg_t *in) maxTime = htsmsg_get_s64_or_default(in, "maxTime", 0); lang - = htsmsg_get_str_or_default(in, "language", htsp->htsp_language); + = htsmsg_get_str(in, "language") ?: htsp->htsp_language; /* Use event as starting point */ if (e || ch) { @@ -841,7 +1006,7 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in) genre.code = u32; eg = &genre; } - lang = htsmsg_get_str_or_default(in, "language", htsp->htsp_language); + lang = htsmsg_get_str(in, "language") ?: htsp->htsp_language; full = htsmsg_get_u32_or_default(in, "full", 0); //do the query @@ -1099,41 +1264,121 @@ htsp_method_getTicket(htsp_connection_t *htsp, htsmsg_t *in) static htsmsg_t * htsp_method_subscribe(htsp_connection_t *htsp, htsmsg_t *in) { - uint32_t chid, sid, weight; + uint32_t chid, sid, weight, req90khz, normts; +#if ENABLE_TIMESHIFT + uint32_t timeshiftPeriod = 0; +#endif channel_t *ch; htsp_subscription_t *hs; - - if(htsmsg_get_u32(in, "channelId", &chid)) - return htsp_error("Missing argument 'channeId'"); - + const char *str; if(htsmsg_get_u32(in, "subscriptionId", &sid)) return htsp_error("Missing argument 'subscriptionId'"); - if((ch = channel_find_by_identifier(chid)) == NULL) - return htsp_error("Requested channel does not exist"); + if(!htsmsg_get_u32(in, "channelId", &chid)) { + if((ch = channel_find_by_identifier(chid)) == NULL) + return htsp_error("Requested channel does not exist"); + } else if((str = htsmsg_get_str(in, "channelName")) != NULL) { + if((ch = channel_find_by_name(str, 0, 0)) == NULL) + return htsp_error("Requested channel does not exist"); + + } else { + return htsp_error("Missing argument 'channelId' or 'channelName'"); + } weight = htsmsg_get_u32_or_default(in, "weight", 150); + req90khz = htsmsg_get_u32_or_default(in, "90khz", 0); + normts = htsmsg_get_u32_or_default(in, "normts", 0); + +#if ENABLE_TIMESHIFT + if (timeshift_enabled) { + timeshiftPeriod = htsmsg_get_u32_or_default(in, "timeshiftPeriod", 0); + if (!timeshift_unlimited_period) + timeshiftPeriod = MIN(timeshiftPeriod, timeshift_max_period); + } +#endif /* * We send the reply now to avoid the user getting the 'subscriptionStart' * async message before the reply to 'subscribe'. + * + * Send some opiotanl boolean flags back to the subscriber so it can infer + * if we support those + * */ - htsp_reply(htsp, in, htsmsg_create_map()); + htsmsg_t *rep = htsmsg_create_map(); + if(req90khz) + htsmsg_add_u32(rep, "90khz", 1); + if(normts) + htsmsg_add_u32(rep, "normts", 1); + +#if ENABLE_TIMESHIFT + if(timeshiftPeriod) + htsmsg_add_u32(rep, "timeshiftPeriod", timeshiftPeriod); +#endif + + htsp_reply(htsp, in, rep); /* Initialize the HTSP subscription structure */ hs = calloc(1, sizeof(htsp_subscription_t)); hs->hs_htsp = htsp; + hs->hs_90khz = req90khz; + hs->hs_queue_depth = htsmsg_get_u32_or_default(in, "queueDepth", + HTSP_DEFAULT_QUEUE_DEPTH); htsp_init_queue(&hs->hs_q, 0); hs->hs_sid = sid; LIST_INSERT_HEAD(&htsp->htsp_subscriptions, hs, hs_link); streaming_target_init(&hs->hs_input, htsp_streaming_input, hs, 0); + streaming_target_t *st = &hs->hs_input; + +#if ENABLE_TIMESHIFT + if (timeshiftPeriod != 0) { + if (timeshiftPeriod == ~0) + tvhlog(LOG_DEBUG, "htsp", "using timeshift buffer (unlimited)"); + else + tvhlog(LOG_DEBUG, "htsp", "using timeshift buffer (%u mins)", timeshiftPeriod / 60); + st = hs->hs_tshift = timeshift_create(st, timeshiftPeriod); + 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); + hs->hs_s = subscription_create_from_channel(ch, weight, htsp->htsp_logname, - &hs->hs_input, 0); + st, 0, + htsp->htsp_peername, + htsp->htsp_username, + htsp->htsp_clientname); return NULL; } @@ -1193,6 +1438,272 @@ htsp_method_change_weight(htsp_connection_t *htsp, htsmsg_t *in) return NULL; } +/** + * Skip stream + */ +static htsmsg_t * +htsp_method_skip(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_subscription_t *hs; + uint32_t sid, abs; + int64_t s64; + streaming_skip_t skip; + + 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"); + + abs = htsmsg_get_u32_or_default(in, "absolute", 0); + + if(!htsmsg_get_s64(in, "time", &s64)) { + skip.type = abs ? SMT_SKIP_ABS_TIME : SMT_SKIP_REL_TIME; + skip.time = hs->hs_90khz ? s64 : ts_rescale_i(s64, 1000000); + } else if (!htsmsg_get_s64(in, "size", &s64)) { + skip.type = abs ? SMT_SKIP_ABS_SIZE : SMT_SKIP_REL_SIZE; + skip.size = s64; + } else { + return htsp_error("Missing argument 'time' or 'size'"); + } + + subscription_set_skip(hs->hs_s, &skip); + + htsp_reply(htsp, in, htsmsg_create_map()); + return NULL; +} + +/* + * Set stream speed + */ +static htsmsg_t * +htsp_method_speed(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_subscription_t *hs; + uint32_t sid; + int32_t speed; + + if(htsmsg_get_u32(in, "subscriptionId", &sid)) + return htsp_error("Missing argument 'subscriptionId'"); + if(htsmsg_get_s32(in, "speed", &speed)) + return htsp_error("Missing argument 'speed'"); + + 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"); + + subscription_set_speed(hs->hs_s, speed); + + htsp_reply(htsp, in, htsmsg_create_map()); + return NULL; +} + +/* + * Revert to live + */ +static htsmsg_t * +htsp_method_live(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_subscription_t *hs; + uint32_t sid; + streaming_skip_t skip; + + 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"); + + skip.type = SMT_SKIP_LIVE; + subscription_set_skip(hs->hs_s, &skip); + + htsp_reply(htsp, in, htsmsg_create_map()); + return NULL; +} + +/** + * Open file + */ +static htsmsg_t * +htsp_method_file_open(htsp_connection_t *htsp, htsmsg_t *in) +{ + const char *str, *s2; + const char *filename = NULL; + if((str = htsmsg_get_str(in, "file")) == NULL) + return htsp_error("Missing argument 'file'"); + + // optional leading slash + if (*str == '/') + str++; + + if((s2 = tvh_strbegins(str, "dvr/")) != NULL || + (s2 = tvh_strbegins(str, "dvrfile/")) != NULL) { + dvr_entry_t *de = dvr_entry_find_by_id(atoi(s2)); + if(de == NULL) + return htsp_error("DVR entry does not exist"); + + if(de->de_filename == NULL) + return htsp_error("DVR entry does not have a file yet"); + + filename = de->de_filename; + return htsp_file_open(htsp, filename, 0); + + } else if ((s2 = tvh_strbegins(str, "imagecache/")) != NULL) { + int fd = imagecache_open(atoi(s2)); + if (fd <= 0) + return htsp_error("failed to open image"); + return htsp_file_open(htsp, str, fd); + + } else { + return htsp_error("Unknown file"); + } +} + +/** + * + */ +static htsmsg_t * +htsp_method_file_read(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_file_t *hf = htsp_file_find(htsp, in); + int64_t off; + int64_t size; + + if(hf == NULL) + return htsp_error("Unknown file id"); + + if(htsmsg_get_s64(in, "size", &size)) + return htsp_error("Missing field 'size'"); + + /* Seek (optional) */ + if (!htsmsg_get_s64(in, "offset", &off)) + if(lseek(hf->hf_fd, off, SEEK_SET) != off) + return htsp_error("Seek error"); + + /* Read */ + void *m = malloc(size); + if(m == NULL) + return htsp_error("Too big segment"); + + int r = read(hf->hf_fd, m, size); + if(r < 0) { + free(m); + return htsp_error("Read error"); + } + + htsmsg_t *rep = htsmsg_create_map(); + htsmsg_add_bin(rep, "data", m, r); + free(m); + return rep; +} + +/** + * + */ +static htsmsg_t * +htsp_method_file_close(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_file_t *hf = htsp_file_find(htsp, in); + + if(hf == NULL) + return htsp_error("Unknown file id"); + + htsp_file_destroy(hf); + return htsmsg_create_map(); +} + +/** + * + */ +static htsmsg_t * +htsp_method_file_stat(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_file_t *hf = htsp_file_find(htsp, in); + struct stat st; + + if(hf == NULL) + return htsp_error("Unknown file id"); + + htsmsg_t *rep = htsmsg_create_map(); + if(!fstat(hf->hf_fd, &st)) { + htsmsg_add_s64(rep, "size", st.st_size); + htsmsg_add_s64(rep, "mtime", st.st_mtime); + } + return rep; +} + +/** + * + */ +static htsmsg_t * +htsp_method_file_seek(htsp_connection_t *htsp, htsmsg_t *in) +{ + htsp_file_t *hf = htsp_file_find(htsp, in); + htsmsg_t *rep; + const char *str; + int64_t off; + int whence; + + if(hf == NULL) + return htsp_error("Unknown file id"); + + if (htsmsg_get_s64(in, "offset", &off)) + return htsp_error("Missing field 'offset'"); + + if ((str = htsmsg_get_str(in, "whence"))) { + if (!strcmp(str, "SEEK_SET")) + whence = SEEK_SET; + else if (!strcmp(str, "SEEK_CUR")) + whence = SEEK_CUR; + else if (!strcmp(str, "SEEK_END")) + whence = SEEK_END; + else + return htsp_error("Field 'whence' contained invalid value"); + } else { + whence = SEEK_SET; + } + + if ((off = lseek(hf->hf_fd, off, whence)) < 0) + return htsp_error("Seek error"); + + rep = htsmsg_create_map(); + htsmsg_add_s64(rep, "offset", off); + 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 */ @@ -1218,6 +1729,18 @@ struct { { "subscribe", htsp_method_subscribe, ACCESS_STREAMING}, { "unsubscribe", htsp_method_unsubscribe, ACCESS_STREAMING}, { "subscriptionChangeWeight", htsp_method_change_weight, ACCESS_STREAMING}, + { "subscriptionSeek", htsp_method_skip, ACCESS_STREAMING}, + { "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}, + { "fileStat", htsp_method_file_stat, ACCESS_RECORDER}, + { "fileSeek", htsp_method_file_seek, ACCESS_RECORDER}, }; #define NUM_METHODS (sizeof(htsp_methods) / sizeof(htsp_methods[0])) @@ -1388,7 +1911,6 @@ static void * htsp_write_scheduler(void *aux) { htsp_connection_t *htsp = aux; - int r; htsp_msg_q_t *hmq; htsp_msg_t *hm; void *dptr; @@ -1425,26 +1947,25 @@ htsp_write_scheduler(void *aux) pthread_mutex_unlock(&htsp->htsp_out_mutex); - r = htsmsg_binary_serialize(hm->hm_msg, &dptr, &dlen, INT32_MAX); - -#if 0 - if(hm->hm_pktref) { - usleep(hm->hm_payloadsize * 3); + if (htsmsg_binary_serialize(hm->hm_msg, &dptr, &dlen, INT32_MAX) != 0) { + tvhlog(LOG_WARNING, "htsp", "%s: failed to serialize data", + htsp->htsp_logname); } -#endif + htsp_msg_destroy(hm); - - /* ignore return value */ - r = write(htsp->htsp_fd, dptr, dlen); - if(r != dlen) - tvhlog(LOG_INFO, "htsp", "%s: Write error -- %s", - htsp->htsp_logname, strerror(errno)); + + if (tvh_write(htsp->htsp_fd, dptr, dlen)) { + tvhlog(LOG_INFO, "htsp", "%s: Write error -- %s", + htsp->htsp_logname, strerror(errno)); + break; + } + free(dptr); pthread_mutex_lock(&htsp->htsp_out_mutex); - if(r != dlen) - break; } + // Shutdown socket to make receive thread terminate entire HTSP connection + shutdown(htsp->htsp_fd, SHUT_RDWR); pthread_mutex_unlock(&htsp->htsp_out_mutex); return NULL; } @@ -1453,14 +1974,14 @@ htsp_write_scheduler(void *aux) * */ static void -htsp_serve(int fd, void *opaque, struct sockaddr_in *source, - struct sockaddr_in *self) +htsp_serve(int fd, void *opaque, struct sockaddr_storage *source, + struct sockaddr_storage *self) { htsp_connection_t htsp; - char buf[30]; + char buf[50]; htsp_subscription_t *s; - snprintf(buf, sizeof(buf), "%s", inet_ntoa(source->sin_addr)); + tcp_get_ip_str((struct sockaddr*)source, buf, 50); memset(&htsp, 0, sizeof(htsp_connection_t)); @@ -1477,6 +1998,10 @@ htsp_serve(int fd, void *opaque, struct sockaddr_in *source, htsp.htsp_peer = source; htsp.htsp_writer_run = 1; + pthread_mutex_lock(&global_lock); + LIST_INSERT_HEAD(&htsp_connections, &htsp, htsp_link); + pthread_mutex_unlock(&global_lock); + pthread_create(&htsp.htsp_writer_thread, NULL, htsp_write_scheduler, &htsp); /** @@ -1503,12 +2028,9 @@ htsp_serve(int fd, void *opaque, struct sockaddr_in *source, if(htsp.htsp_async_mode) LIST_REMOVE(&htsp, htsp_async_link); - pthread_mutex_unlock(&global_lock); + LIST_REMOVE(&htsp, htsp_link); - free(htsp.htsp_logname); - free(htsp.htsp_peername); - free(htsp.htsp_username); - free(htsp.htsp_clientname); + pthread_mutex_unlock(&global_lock); pthread_mutex_lock(&htsp.htsp_out_mutex); htsp.htsp_writer_run = 0; @@ -1516,16 +2038,39 @@ htsp_serve(int fd, void *opaque, struct sockaddr_in *source, pthread_mutex_unlock(&htsp.htsp_out_mutex); pthread_join(htsp.htsp_writer_thread, NULL); + + free(htsp.htsp_logname); + free(htsp.htsp_peername); + free(htsp.htsp_username); + free(htsp.htsp_clientname); + + htsp_msg_q_t *hmq; + + TAILQ_FOREACH(hmq, &htsp.htsp_active_output_queues, hmq_link) { + htsp_msg_t *hm; + while((hm = TAILQ_FIRST(&hmq->hmq_q)) != NULL) { + TAILQ_REMOVE(&hmq->hmq_q, hm, hm_link); + htsp_msg_destroy(hm); + } + } + + htsp_file_t *hf; + while((hf = LIST_FIRST(&htsp.htsp_files)) != NULL) + htsp_file_destroy(hf); + close(fd); } - + /** * Fire up HTSP server */ void -htsp_init(void) +htsp_init(const char *bindaddr) { - htsp_server = tcp_server_create(htsp_port, htsp_serve, NULL); + extern int tvheadend_htsp_port_extra; + htsp_server = tcp_server_create(bindaddr, tvheadend_htsp_port, htsp_serve, NULL); + if(tvheadend_htsp_port_extra) + htsp_server_2 = tcp_server_create(bindaddr, tvheadend_htsp_port_extra, htsp_serve, NULL); } /* ************************************************************************** @@ -1573,23 +2118,30 @@ htsp_channel_update_current(channel_t *ch) /** * Called from channel.c when a new channel is created */ +static void +_htsp_channel_update(channel_t *ch, const char *msg) +{ + htsp_connection_t *htsp; + LIST_FOREACH(htsp, &htsp_async_connections, htsp_async_link) + if (htsp->htsp_async_mode & HTSP_ASYNC_ON) + htsp_send_message(htsp, htsp_build_channel(ch, msg, htsp), NULL); +} + void htsp_channel_add(channel_t *ch) { - htsp_async_send(htsp_build_channel(ch, "channelAdd"), HTSP_ASYNC_ON); + _htsp_channel_update(ch, "channelAdd"); } - /** * Called from channel.c when a channel is updated */ void htsp_channel_update(channel_t *ch) { - htsp_async_send(htsp_build_channel(ch, "channelUpdate"), HTSP_ASYNC_ON); + _htsp_channel_update(ch, "channelUpdate"); } - /** * Called from channel.c when a channel is deleted */ @@ -1722,15 +2274,15 @@ const static char frametypearray[PKT_NTYPES] = { static void htsp_stream_deliver(htsp_subscription_t *hs, th_pkt_t *pkt) { - htsmsg_t *m, *n; + htsmsg_t *m; htsp_msg_t *hm; htsp_connection_t *htsp = hs->hs_htsp; int64_t ts; int qlen = hs->hs_q.hmq_payload; - if((qlen > 500000 && pkt->pkt_frametype == PKT_B_FRAME) || - (qlen > 750000 && pkt->pkt_frametype == PKT_P_FRAME) || - (qlen > 1500000)) { + 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)) { hs->hs_dropstats[pkt->pkt_frametype]++; @@ -1748,18 +2300,17 @@ htsp_stream_deliver(htsp_subscription_t *hs, th_pkt_t *pkt) htsmsg_add_u32(m, "stream", pkt->pkt_componentindex); htsmsg_add_u32(m, "com", pkt->pkt_commercial); - if(pkt->pkt_pts != PTS_UNSET) { - int64_t pts = ts_rescale(pkt->pkt_pts, 1000000); + int64_t pts = hs->hs_90khz ? pkt->pkt_pts : ts_rescale(pkt->pkt_pts, 1000000); htsmsg_add_s64(m, "pts", pts); } if(pkt->pkt_dts != PTS_UNSET) { - int64_t dts = ts_rescale(pkt->pkt_dts, 1000000); + int64_t dts = hs->hs_90khz ? pkt->pkt_dts : ts_rescale(pkt->pkt_dts, 1000000); htsmsg_add_s64(m, "dts", dts); } - uint32_t dur = ts_rescale(pkt->pkt_duration, 1000000); + uint32_t dur = hs->hs_90khz ? pkt->pkt_duration : ts_rescale(pkt->pkt_duration, 1000000); htsmsg_add_u32(m, "duration", dur); pkt = pkt_merge_header(pkt); @@ -1790,13 +2341,29 @@ htsp_stream_deliver(htsp_subscription_t *hs, th_pkt_t *pkt) pthread_mutex_lock(&htsp->htsp_out_mutex); - if(TAILQ_FIRST(&hs->hs_q.hmq_q) == NULL) { - htsmsg_add_s64(m, "delay", 0); - } else if((hm = TAILQ_FIRST(&hs->hs_q.hmq_q)) != NULL && - (n = hm->hm_msg) != NULL && !htsmsg_get_s64(n, "dts", &ts) && - pkt->pkt_dts != PTS_UNSET && ts != PTS_UNSET) { - htsmsg_add_s64(m, "delay", pkt->pkt_dts - ts); + int64_t min_dts = PTS_UNSET; + int64_t max_dts = PTS_UNSET; + TAILQ_FOREACH(hm, &hs->hs_q.hmq_q, hm_link) { + if(!hm->hm_msg) + continue; + if(htsmsg_get_s64(hm->hm_msg, "dts", &ts)) + continue; + if(ts == PTS_UNSET) + continue; + + if(min_dts == PTS_UNSET) + min_dts = ts; + else + min_dts = MIN(ts, min_dts); + + if(max_dts == PTS_UNSET) + max_dts = ts; + else + max_dts = MAX(ts, max_dts); } + + htsmsg_add_s64(m, "delay", max_dts - min_dts); + pthread_mutex_unlock(&htsp->htsp_out_mutex); htsmsg_add_u32(m, "Bdrops", hs->hs_dropstats[PKT_B_FRAME]); @@ -1840,9 +2407,12 @@ htsp_subscription_start(htsp_subscription_t *hs, const streaming_start_t *ss) if(ssc->ssc_type == SCT_MPEG2VIDEO || ssc->ssc_type == SCT_H264) { if(ssc->ssc_width) - htsmsg_add_u32(c, "width", ssc->ssc_width); + htsmsg_add_u32(c, "width", ssc->ssc_width); if(ssc->ssc_height) - htsmsg_add_u32(c, "height", ssc->ssc_height); + htsmsg_add_u32(c, "height", ssc->ssc_height); + if(ssc->ssc_frameduration) + htsmsg_add_u32(c, "duration", hs->hs_90khz ? ssc->ssc_frameduration : + ts_rescale(ssc->ssc_frameduration, 1000000)); if (ssc->ssc_aspect_num) htsmsg_add_u32(c, "aspect_num", ssc->ssc_aspect_num); if (ssc->ssc_aspect_den) @@ -1851,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) @@ -1941,6 +2512,64 @@ htsp_subscription_signal_status(htsp_subscription_t *hs, signal_status_t *sig) htsp_send_message(hs->hs_htsp, m, &hs->hs_htsp->htsp_hmq_qstatus); } +/** + * + */ +static void +htsp_subscription_speed(htsp_subscription_t *hs, int speed) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "method", "subscriptionSpeed"); + htsmsg_add_u32(m, "subscriptionId", hs->hs_sid); + htsmsg_add_u32(m, "speed", speed); + htsp_send(hs->hs_htsp, m, NULL, &hs->hs_q, 0); +} + +/** + * + */ +static void +htsp_subscription_skip(htsp_subscription_t *hs, streaming_skip_t *skip) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "method", "subscriptionSkip"); + htsmsg_add_u32(m, "subscriptionId", hs->hs_sid); + + /* Flush pkt buffers */ + if (skip->type != SMT_SKIP_ERROR) + htsp_flush_queue(hs->hs_htsp, &hs->hs_q); + + if (skip->type == SMT_SKIP_ABS_TIME || skip->type == SMT_SKIP_ABS_SIZE) + htsmsg_add_u32(m, "absolute", 1); + if (skip->type == SMT_SKIP_ERROR) + htsmsg_add_u32(m, "error", 1); + else if (skip->type == SMT_SKIP_ABS_TIME || skip->type == SMT_SKIP_REL_TIME) + htsmsg_add_s64(m, "time", hs->hs_90khz ? skip->time : ts_rescale(skip->time, 1000000)); + else if (skip->type == SMT_SKIP_ABS_SIZE || skip->type == SMT_SKIP_REL_SIZE) + htsmsg_add_s64(m, "size", skip->size); + htsp_send(hs->hs_htsp, m, NULL, &hs->hs_q, 0); +} + +/** + * + */ +#if ENABLE_TIMESHIFT +static void +htsp_subscription_timeshift_status(htsp_subscription_t *hs, timeshift_status_t *status) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_str(m, "method", "timeshiftStatus"); + htsmsg_add_u32(m, "subscriptionId", hs->hs_sid); + htsmsg_add_u32(m, "full", status->full); + htsmsg_add_s64(m, "shift", hs->hs_90khz ? status->shift : ts_rescale(status->shift, 1000000)); + if (status->pts_start != PTS_UNSET) + htsmsg_add_s64(m, "start", hs->hs_90khz ? status->pts_start : ts_rescale(status->pts_start, 1000000)) ; + if (status->pts_end != PTS_UNSET) + htsmsg_add_s64(m, "end", hs->hs_90khz ? status->pts_end : ts_rescale(status->pts_end, 1000000)) ; + htsp_send(hs->hs_htsp, m, NULL, &hs->hs_q, 0); +} +#endif + /** * */ @@ -1951,7 +2580,8 @@ htsp_streaming_input(void *opaque, streaming_message_t *sm) switch(sm->sm_type) { case SMT_PACKET: - htsp_stream_deliver(hs, sm->sm_data); // reference is transfered + htsp_stream_deliver(hs, sm->sm_data); + // reference is transfered sm->sm_data = NULL; break; @@ -1980,6 +2610,20 @@ htsp_streaming_input(void *opaque, streaming_message_t *sm) case SMT_EXIT: abort(); + + case SMT_SKIP: + htsp_subscription_skip(hs, sm->sm_data); + break; + + case SMT_SPEED: + htsp_subscription_speed(hs, sm->sm_code); + break; + + case SMT_TIMESHIFT_STATUS: +#if ENABLE_TIMESHIFT + htsp_subscription_timeshift_status(hs, sm->sm_data); +#endif + break; } streaming_msg_free(sm); } diff --git a/src/htsp.h b/src/htsp_server.h similarity index 97% rename from src/htsp.h rename to src/htsp_server.h index a82cca12..77002cac 100644 --- a/src/htsp.h +++ b/src/htsp_server.h @@ -22,7 +22,7 @@ #include "epg.h" #include "dvr/dvr.h" -void htsp_init(void); +void htsp_init(const char *bindaddr); void htsp_channel_update_current(channel_t *ch); diff --git a/src/http.c b/src/http.c index 07c88fc6..98562d74 100644 --- a/src/http.c +++ b/src/http.c @@ -173,18 +173,18 @@ http_send_header(http_connection_t *hc, int rc, const char *content, tm = gmtime_r(&t, &tm0); htsbuf_qprintf(&hdrs, - "Last-Modified: %s, %02d %s %d %02d:%02d:%02d GMT\r\n", - cachedays[tm->tm_wday], tm->tm_year + 1900, - cachemonths[tm->tm_mon], tm->tm_mday, - tm->tm_hour, tm->tm_min, tm->tm_sec); + "Last-Modified: %s, %d %s %02d %02d:%02d:%02d GMT\r\n", + cachedays[tm->tm_wday], tm->tm_mday, + cachemonths[tm->tm_mon], tm->tm_year + 1900, + tm->tm_hour, tm->tm_min, tm->tm_sec); t += maxage; tm = gmtime_r(&t, &tm0); htsbuf_qprintf(&hdrs, - "Expires: %s, %02d %s %d %02d:%02d:%02d GMT\r\n", - cachedays[tm->tm_wday], tm->tm_year + 1900, - cachemonths[tm->tm_mon], tm->tm_mday, + "Expires: %s, %d %s %02d %02d:%02d:%02d GMT\r\n", + cachedays[tm->tm_wday], tm->tm_mday, + cachemonths[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); htsbuf_qprintf(&hdrs, "Cache-Control: max-age=%d\r\n", maxage); @@ -247,9 +247,12 @@ void http_error(http_connection_t *hc, int error) { const char *errtxt = http_rc2str(error); + char *addrstr = (char*)malloc(50); + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrstr, 50); tvhlog(LOG_ERR, "HTTP", "%s: %s -- %d", - inet_ntoa(hc->hc_peer->sin_addr), hc->hc_url, error); + addrstr, hc->hc_url, error); + free(addrstr); htsbuf_queue_flush(&hc->hc_reply); @@ -315,10 +318,13 @@ http_access_verify(http_connection_t *hc, int mask) { const char *ticket_id = http_arg_get(&hc->hc_req_args, "ticket"); - if(!access_ticket_verify(ticket_id, hc->hc_url)) { + if(!access_ticket_verify(ticket_id, hc->hc_url)) + { + char *addrstr = (char*)malloc(50); + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrstr, 50); tvhlog(LOG_INFO, "HTTP", "%s: using ticket %s for %s", - inet_ntoa(hc->hc_peer->sin_addr), ticket_id, - hc->hc_url); + addrstr, ticket_id, hc->hc_url); + free(addrstr); return 0; } @@ -504,11 +510,9 @@ process_request(http_connection_t *hc, htsbuf_queue_t *spill) if(hc->hc_username != NULL) { hc->hc_representative = strdup(hc->hc_username); } else { - hc->hc_representative = malloc(30); + hc->hc_representative = malloc(50); /* Not threadsafe ? */ - snprintf(hc->hc_representative, 30, - "%s", inet_ntoa(hc->hc_peer->sin_addr)); - + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, hc->hc_representative, 50); } switch(hc->hc_version) { @@ -607,9 +611,15 @@ http_path_add(const char *path, void *opaque, http_callback_t *callback, uint32_t accessmask) { http_path_t *hp = malloc(sizeof(http_path_t)); + char *tmp; - hp->hp_len = strlen(path); - hp->hp_path = strdup(path); + if (tvheadend_webroot) { + size_t len = strlen(tvheadend_webroot) + strlen(path) + 1; + hp->hp_path = tmp = malloc(len); + sprintf(tmp, "%s%s", tvheadend_webroot, path); + } else + hp->hp_path = strdup(path); + hp->hp_len = strlen(hp->hp_path); hp->hp_opaque = opaque; hp->hp_callback = callback; hp->hp_accessmask = accessmask; @@ -771,8 +781,8 @@ http_serve_requests(http_connection_t *hc, htsbuf_queue_t *spill) * */ static void -http_serve(int fd, void *opaque, struct sockaddr_in *peer, - struct sockaddr_in *self) +http_serve(int fd, void *opaque, struct sockaddr_storage *peer, + struct sockaddr_storage *self) { htsbuf_queue_t spill; http_connection_t hc; @@ -806,7 +816,7 @@ http_serve(int fd, void *opaque, struct sockaddr_in *peer, * Fire up HTTP server */ void -http_server_init(void) +http_server_init(const char *bindaddr) { - http_server = tcp_server_create(webui_port, http_serve, NULL); + http_server = tcp_server_create(bindaddr, tvheadend_webui_port, http_serve, NULL); } diff --git a/src/http.h b/src/http.h index 2a29b8a9..08878b46 100644 --- a/src/http.h +++ b/src/http.h @@ -39,8 +39,8 @@ typedef struct http_arg { typedef struct http_connection { int hc_fd; - struct sockaddr_in *hc_peer; - struct sockaddr_in *hc_self; + struct sockaddr_storage *hc_peer; + struct sockaddr_storage *hc_self; char *hc_representative; char *hc_url; @@ -133,7 +133,7 @@ http_path_t *http_path_add(const char *path, void *opaque, -void http_server_init(void); +void http_server_init(const char *bindaddr); int http_access_verify(http_connection_t *hc, int mask); diff --git a/src/imagecache.c b/src/imagecache.c new file mode 100644 index 00000000..44bd6506 --- /dev/null +++ b/src/imagecache.c @@ -0,0 +1,463 @@ +/* + * Icon file server operations + * Copyright (C) 2012 Andy Brown + * + * 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 "settings.h" +#include "tvheadend.h" +#include "filebundle.h" +#include "imagecache.h" +#include "queue.h" +#include "redblack.h" + +#if ENABLE_IMAGECACHE +#define CURL_STATICLIB +#include +#include +#endif + +// TODO: icon cache flushing? +// TODO: md5 validation? +// TODO: allow cache to be disabled by users + +/* + * Image metadata + */ +typedef struct imagecache_image +{ + int id; ///< Internal ID + const char *url; ///< Upstream URL + int failed; ///< Last update failed + time_t updated; ///< Last time the file was checked + enum { + IDLE, + QUEUED, + FETCHING + } state; ///< fetch status + + TAILQ_ENTRY(imagecache_image) q_link; ///< Fetch Q link + RB_ENTRY(imagecache_image) id_link; ///< Index by ID + RB_ENTRY(imagecache_image) url_link; ///< Index by URL +} imagecache_image_t; + +static int _imagecache_id; +static RB_HEAD(,imagecache_image) _imagecache_by_id; +static RB_HEAD(,imagecache_image) _imagecache_by_url; + +pthread_mutex_t imagecache_mutex; + +static void _imagecache_save ( imagecache_image_t *img ); + +#if ENABLE_IMAGECACHE +uint32_t imagecache_enabled; +uint32_t imagecache_ok_period; +uint32_t imagecache_fail_period; +uint32_t imagecache_ignore_sslcert; + +static pthread_cond_t _imagecache_cond; +static TAILQ_HEAD(, imagecache_image) _imagecache_queue; +static void _imagecache_add ( imagecache_image_t *img ); +static void* _imagecache_thread ( void *p ); +static int _imagecache_fetch ( imagecache_image_t *img ); +#endif + +static int _url_cmp ( void *a, void *b ) +{ + return strcmp(((imagecache_image_t*)a)->url, ((imagecache_image_t*)b)->url); +} + +static int _id_cmp ( void *a, void *b ) +{ + return ((imagecache_image_t*)a)->id - ((imagecache_image_t*)b)->id; +} + +/* + * Initialise + */ +void imagecache_init ( void ) +{ + htsmsg_t *m, *e; + htsmsg_field_t *f; + imagecache_image_t *img, *i; + const char *url; + uint32_t id; + + /* Init vars */ + _imagecache_id = 0; +#if ENABLE_IMAGECACHE + imagecache_enabled = 0; + imagecache_ok_period = 24 * 7; // weekly + imagecache_fail_period = 24; // daily + imagecache_ignore_sslcert = 0; +#endif + + /* Create threads */ + pthread_mutex_init(&imagecache_mutex, NULL); +#if ENABLE_IMAGECACHE + pthread_cond_init(&_imagecache_cond, NULL); + TAILQ_INIT(&_imagecache_queue); +#endif + + /* Load settings */ +#if ENABLE_IMAGECACHE + if ((m = hts_settings_load("imagecache/config"))) { + htsmsg_get_u32(m, "enabled", &imagecache_enabled); + htsmsg_get_u32(m, "ok_period", &imagecache_ok_period); + htsmsg_get_u32(m, "fail_period", &imagecache_fail_period); + htsmsg_get_u32(m, "ignore_sslcert", &imagecache_ignore_sslcert); + htsmsg_destroy(m); + } +#endif + if ((m = hts_settings_load("imagecache/meta"))) { + HTSMSG_FOREACH(f, m) { + if (!(e = htsmsg_get_map_by_field(f))) continue; + if (!(id = atoi(f->hmf_name))) continue; + if (!(url = htsmsg_get_str(e, "url"))) continue; + img = calloc(1, sizeof(imagecache_image_t)); + img->id = id; + img->url = strdup(url); + img->updated = htsmsg_get_s64_or_default(e, "updated", 0); + i = RB_INSERT_SORTED(&_imagecache_by_url, img, url_link, _url_cmp); + if (i) { + hts_settings_remove("imagecache/meta/%d", id); + hts_settings_remove("imagecache/data/%d", id); + free(img); + continue; + } + i = RB_INSERT_SORTED(&_imagecache_by_id, img, id_link, _id_cmp); + assert(!i); + if (id > _imagecache_id) + _imagecache_id = id; +#if ENABLE_IMAGECACHE + if (!img->updated) + _imagecache_add(img); +#endif + } + htsmsg_destroy(m); + } + + /* Start threads */ +#if ENABLE_IMAGECACHE + { + pthread_t tid; + pthread_create(&tid, NULL, _imagecache_thread, NULL); + } +#endif +} + +/* + * Save settings + */ +#if ENABLE_IMAGECACHE +void imagecache_save ( void ) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_u32(m, "enabled", imagecache_enabled); + htsmsg_add_u32(m, "ok_period", imagecache_ok_period); + htsmsg_add_u32(m, "fail_period", imagecache_fail_period); + htsmsg_add_u32(m, "ignore_sslcert", imagecache_ignore_sslcert); + hts_settings_save(m, "imagecache/config"); +} + +/* + * Enable/disable + */ +int imagecache_set_enabled ( uint32_t e ) +{ + if (e == imagecache_enabled) + return 0; + imagecache_enabled = e; + if (e) + pthread_cond_broadcast(&_imagecache_cond); + return 1; +} + +/* + * Set ok period + */ +int imagecache_set_ok_period ( uint32_t p ) +{ + if (p == imagecache_ok_period) + return 0; + imagecache_ok_period = p; + return 1; +} + +/* + * Set fail period + */ +int imagecache_set_fail_period ( uint32_t p ) +{ + if (p == imagecache_fail_period) + return 0; + imagecache_fail_period = p; + return 1; +} + +/* + * Set ignore SSL cert + */ +int imagecache_set_ignore_sslcert ( uint32_t p ) +{ + if (p == imagecache_ignore_sslcert) + return 0; + imagecache_ignore_sslcert = p; + return 1; +} +#endif + +/* + * Fetch a URLs ID + */ +uint32_t imagecache_get_id ( const char *url ) +{ + uint32_t id = 0; + imagecache_image_t *i; + static imagecache_image_t *skel = NULL; + + /* Invalid */ + if (!url) + return 0; + + /* Disabled */ +#if !ENABLE_IMAGECACHE + if (strncasecmp(url, "file://", 7)) + return 0; +#endif + + /* Skeleton */ + if (!skel) + skel = calloc(1, sizeof(imagecache_image_t)); + skel->url = url; + + /* Create/Find */ + pthread_mutex_lock(&imagecache_mutex); + i = RB_INSERT_SORTED(&_imagecache_by_url, skel, url_link, _url_cmp); + if (!i) { + i = skel; + i->url = strdup(url); + i->id = ++_imagecache_id; + skel = RB_INSERT_SORTED(&_imagecache_by_id, i, id_link, _id_cmp); + assert(!skel); +#if ENABLE_IMAGECACHE + _imagecache_add(i); +#endif + _imagecache_save(i); + } +#if ENABLE_IMAGECACHE + if (!strncasecmp(url, "file://", 7) || imagecache_enabled) + id = i->id; +#else + if (!strncasecmp(url, "file://", 7)) + id = i->id; +#endif + pthread_mutex_unlock(&imagecache_mutex); + + return id; +} + +/* + * Get data + */ +int imagecache_open ( uint32_t id ) +{ + imagecache_image_t skel, *i; + int fd = -1; + + pthread_mutex_lock(&imagecache_mutex); + + /* Find */ + skel.id = id; + i = RB_FIND(&_imagecache_by_id, &skel, id_link, _id_cmp); + + /* Invalid */ + if (!i) { + pthread_mutex_unlock(&imagecache_mutex); + return -1; + } + + /* Local file */ + if (!strncasecmp(i->url, "file://", 7)) + fd = open(i->url + 7, O_RDONLY); + + /* Remote file */ +#if ENABLE_IMAGECACHE + else if (imagecache_enabled) { + struct timespec ts; + int err; + if (i->updated) { + // use existing + } else if (i->state == FETCHING) { + ts.tv_nsec = 0; + time(&ts.tv_sec); + ts.tv_sec += 10; // TODO: sensible timeout? + err = pthread_cond_timedwait(&_imagecache_cond, &imagecache_mutex, &ts); + if (err == ETIMEDOUT) { + pthread_mutex_unlock(&imagecache_mutex); + return -1; + } + } else if (i->state == QUEUED) { + i->state = FETCHING; + TAILQ_REMOVE(&_imagecache_queue, i, q_link); + pthread_mutex_unlock(&imagecache_mutex); + if (_imagecache_fetch(i)) + return -1; + pthread_mutex_lock(&imagecache_mutex); + } + fd = hts_settings_open_file(0, "imagecache/data/%d", i->id); + } +#endif + pthread_mutex_unlock(&imagecache_mutex); + + return fd; +} + +static void _imagecache_save ( imagecache_image_t *img ) +{ + htsmsg_t *m = htsmsg_create_map(); + + htsmsg_add_str(m, "url", img->url); + if (img->updated) + htsmsg_add_s64(m, "updated", img->updated); + + hts_settings_save(m, "imagecache/meta/%d", img->id); +} + +#if ENABLE_IMAGECACHE +static void _imagecache_add ( imagecache_image_t *img ) +{ + if (strncasecmp("file://", img->url, 7)) { + img->state = QUEUED; + TAILQ_INSERT_TAIL(&_imagecache_queue, img, q_link); + pthread_cond_broadcast(&_imagecache_cond); + } else { + time(&img->updated); + } +} + +static void *_imagecache_thread ( void *p ) +{ + int err; + imagecache_image_t *img; + struct timespec ts; + ts.tv_nsec = 0; + + while (1) { + + /* Get entry */ + pthread_mutex_lock(&imagecache_mutex); + if (!imagecache_enabled) { + pthread_cond_wait(&_imagecache_cond, &imagecache_mutex); + pthread_mutex_unlock(&imagecache_mutex); + continue; + } + img = TAILQ_FIRST(&_imagecache_queue); + if (!img) { + time(&ts.tv_sec); + ts.tv_sec += 60; + err = pthread_cond_timedwait(&_imagecache_cond, &imagecache_mutex, &ts); + if (err == ETIMEDOUT) { + uint32_t period; + RB_FOREACH(img, &_imagecache_by_url, url_link) { + if (img->state != IDLE) continue; + period = img->failed ? imagecache_fail_period : imagecache_ok_period; + period *= 3600; + if (period && ((ts.tv_sec - img->updated) > period)) + _imagecache_add(img); + } + } + pthread_mutex_unlock(&imagecache_mutex); + continue; + } + img->state = FETCHING; + TAILQ_REMOVE(&_imagecache_queue, img, q_link); + pthread_mutex_unlock(&imagecache_mutex); + + /* Fetch */ + _imagecache_fetch(img); + } + + return NULL; +} + +static int _imagecache_fetch ( imagecache_image_t *img ) +{ + int res; + CURL *curl; + FILE *fp; + char tmp[256], path[256]; + + /* Open file */ + if (hts_settings_buildpath(path, sizeof(path), "imagecache/data/%d", + img->id)) + return 1; + if (hts_settings_makedirs(path)) + return 1; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + if (!(fp = fopen(tmp, "wb"))) + return 1; + + /* Build command */ + pthread_mutex_lock(&imagecache_mutex); + tvhlog(LOG_DEBUG, "imagecache", "fetch %s", img->url); + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, img->url); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "TVHeadend"); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); + if (imagecache_ignore_sslcert) + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + pthread_mutex_unlock(&imagecache_mutex); + + /* Fetch */ + res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + fclose(fp); + + /* Process */ + pthread_mutex_lock(&imagecache_mutex); + img->state = IDLE; + time(&img->updated); // even if failed (possibly request sooner?) + if (res) { + img->failed = 1; + unlink(tmp); + tvhlog(LOG_WARNING, "imagecache", "failed to download %s", img->url); + } else { + img->failed = 0; + unlink(path); + rename(tmp, path); + tvhlog(LOG_DEBUG, "imagecache", "downloaded %s", img->url); + } + _imagecache_save(img); + pthread_cond_broadcast(&_imagecache_cond); + pthread_mutex_unlock(&imagecache_mutex); + + return res; +}; +#endif diff --git a/src/imagecache.h b/src/imagecache.h new file mode 100644 index 00000000..225d717e --- /dev/null +++ b/src/imagecache.h @@ -0,0 +1,61 @@ +/* + * Icon file serve operations + * Copyright (C) 2012 Andy Brown + * + * 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 . + */ + +#ifndef __IMAGE_CACHE_H__ +#define __IMAGE_CACHE_H__ + +#include + +extern uint32_t imagecache_enabled; +extern uint32_t imagecache_ok_period; +extern uint32_t imagecache_fail_period; +extern uint32_t imagecache_ignore_sslcert; + +extern pthread_mutex_t imagecache_mutex; + +void imagecache_init ( void ); + +void imagecache_save ( void ); + +int imagecache_set_enabled ( uint32_t e ) + __attribute__((warn_unused_result)); +int imagecache_set_ok_period ( uint32_t e ) + __attribute__((warn_unused_result)); +int imagecache_set_fail_period ( uint32_t e ) + __attribute__((warn_unused_result)); +int imagecache_set_ignore_sslcert ( uint32_t e ) + __attribute__((warn_unused_result)); + +// Note: will return 0 if invalid (must serve original URL) +uint32_t imagecache_get_id ( const char *url ); + +int imagecache_open ( uint32_t id ); + +#define htsmsg_add_imageurl(_msg, _fld, _fmt, _url)\ + {\ + char _tmp[64];\ + uint32_t _id = imagecache_get_id(_url);\ + if (_id) {\ + snprintf(_tmp, sizeof(_tmp), _fmt, _id);\ + htsmsg_add_str(_msg, _fld, _tmp);\ + } else {\ + htsmsg_add_str(_msg, _fld, _url);\ + }\ + } + +#endif /* __IMAGE_CACHE_H__ */ diff --git a/src/iptv_input.c b/src/iptv_input.c index 2f65398e..6c39a866 100644 --- a/src/iptv_input.c +++ b/src/iptv_input.c @@ -461,6 +461,14 @@ iptv_service_quality(service_t *t) return 100; } +/** + * + */ +static int +iptv_service_is_enabled(service_t *t) +{ + return t->s_enabled; +} /** * Generate a descriptive name for the source @@ -471,6 +479,7 @@ iptv_service_setsourceinfo(service_t *t, struct source_info *si) char straddr[INET6_ADDRSTRLEN]; memset(si, 0, sizeof(struct source_info)); + si->si_type = S_MPEG_TS; si->si_adapter = t->s_iptv_iface ? strdup(t->s_iptv_iface) : NULL; if(t->s_iptv_group.s_addr != 0) { si->si_mux = strdup(inet_ntoa(t->s_iptv_group)); @@ -542,6 +551,7 @@ iptv_service_find(const char *id, int create) t->s_config_save = iptv_service_save; t->s_setsourceinfo = iptv_service_setsourceinfo; t->s_quality_index = iptv_service_quality; + t->s_is_enabled = iptv_service_is_enabled; t->s_grace_period = iptv_grace_period; t->s_dtor = iptv_service_dtor; t->s_iptv_fd = -1; diff --git a/src/iptv_output.c b/src/iptv_output.c deleted file mode 100644 index 9fab8ac4..00000000 --- a/src/iptv_output.c +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Output functions for fixed multicast streaming - * 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 "tvhead.h" -#include "iptv_output.h" -#include "dispatch.h" -#include "channels.h" -#include "subscriptions.h" -#include "tsmux.h" -#include "rtp.h" - -#define MULTICAST_PKT_SIZ (188 * 7) - -typedef struct output_multicast { - ts_muxer_t *om_muxer; - int om_fd; - struct sockaddr_in om_dst; - - int om_corruption; - - int om_inter_drop_rate; - int om_inter_drop_cnt; - - int om_seq; - enum { - OM_RAWUDP, - OM_RTP, - } om_encapsulation; - -} output_multicast_t; - -/** - * Output MPEG TS - */ -static void -iptv_output_ts(void *opaque, th_subscription_t *s, uint8_t *pkt, - int blocks, int64_t pcr) -{ - output_multicast_t *om = opaque; - - om->om_seq++; - - if(om->om_inter_drop_rate && - ++om->om_inter_drop_cnt == om->om_inter_drop_rate) { - om->om_inter_drop_cnt = 0; - return; - } - - switch(om->om_encapsulation) { - case OM_RTP: - rtp_sendmsg(pkt, blocks, pcr, om->om_fd, - (struct sockaddr *)&om->om_dst, sizeof(struct sockaddr_in), - om->om_seq); - break; - - case OM_RAWUDP: - sendto(om->om_fd, pkt, blocks * 188, 0, - (struct sockaddr *)&om->om_dst, sizeof(struct sockaddr_in)); - break; - } -} - - -/** - * Called when a subscription gets/loses access to a transport - */ -static void -iptv_subscription_callback(struct th_subscription *s, - subscription_event_t event, void *opaque) -{ - output_multicast_t *om = opaque; - - switch(event) { - case TRANSPORT_AVAILABLE: - assert(om->om_muxer == NULL); - om->om_muxer = ts_muxer_init(s, iptv_output_ts, om, TS_SEEK, - om->om_corruption); - ts_muxer_play(om->om_muxer, 0); - break; - - case TRANSPORT_UNAVAILABLE: - ts_muxer_deinit(om->om_muxer, s); - om->om_muxer = NULL; - break; - } -} - - - -/** - * Setup IPTV (TS over UDP) output - */ -static void -output_multicast_load(struct config_head *head) -{ - const char *name, *s, *b; - channel_t *ch; - output_multicast_t *om; - int ttl = 32; - struct sockaddr_in sin; - char title[100]; - char title2[100]; - - if((name = config_get_str_sub(head, "channel", NULL)) == NULL) - return; - - ch = channel_find_by_name(name, 1); - - om = calloc(1, sizeof(output_multicast_t)); - - om->om_fd = tvh_socket(AF_INET, SOCK_DGRAM, 0); - - if((b = config_get_str_sub(head, "interface-address", NULL)) != NULL) { - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_port = 0; - sin.sin_addr.s_addr = inet_addr(b); - - if(bind(om->om_fd, (struct sockaddr *)&sin, sizeof(sin))==-1) { - fprintf(stderr, "cannot bind to %s\n", b); - goto err; - } - } - - if((s = config_get_str_sub(head, "group-address", NULL)) == NULL) { - fprintf(stderr, "no group address configured\n"); - goto err; - } - om->om_dst.sin_addr.s_addr = inet_addr(s); - - if((s = config_get_str_sub(head, "port", NULL)) == NULL) { - fprintf(stderr, "no port configured\n"); - goto err; - } - om->om_dst.sin_port = htons(atoi(s)); - - if((s = config_get_str_sub(head, "ttl", NULL)) != NULL) - ttl = atoi(s); - - - if((s = config_get_str_sub(head, "corruption-interval", NULL)) != NULL) - om->om_corruption = atoi(s); - - if((s = config_get_str_sub(head, "inter-drop-rate", NULL)) != NULL) - om->om_inter_drop_rate = atoi(s); - - if((s = config_get_str_sub(head, "encapsulation", NULL)) != NULL) { - if(!strcasecmp(s, "rtp")) - om->om_encapsulation = OM_RTP; - } - - setsockopt(om->om_fd, SOL_IP, IP_MULTICAST_TTL, &ttl, sizeof(int)); - - snprintf(title, sizeof(title), "%s:%d", inet_ntoa(om->om_dst.sin_addr), - ntohs(om->om_dst.sin_port)); - - syslog(LOG_INFO, "Static multicast output: \"%s\" to %s, source %s ", - ch->ch_name, title, inet_ntoa(sin.sin_addr)); - - snprintf(title2, sizeof(title2), "IPTV-OUT: %s", title); - - subscription_create(ch, 900, title2, iptv_subscription_callback, om, 0); - return; - - err: - close(om->om_fd); - free(om); -} - - - -void -output_multicast_setup(void) -{ - config_entry_t *ce; - - TAILQ_FOREACH(ce, &config_list, ce_link) { - if(ce->ce_type == CFG_SUB && !strcasecmp("multicast-output", ce->ce_key)) { - output_multicast_load(&ce->ce_sub); - } - } -} diff --git a/src/lang_codes.c b/src/lang_codes.c index abde7c2a..0e92599b 100644 --- a/src/lang_codes.c +++ b/src/lang_codes.c @@ -180,6 +180,7 @@ const lang_code_t lang_codes[] = { { "gor", NULL, NULL , "Gorontalo" }, { "got", NULL, NULL , "Gothic" }, { "grb", NULL, NULL , "Grebo" }, + { "gre", "el", NULL , "Greek" }, { "grn", "gn", NULL , "Guarani" }, { "gsw", NULL, NULL , "Swiss German; Alemannic; Alsatian" }, { "guj", "gu", NULL , "Gujarati" }, diff --git a/src/libav.c b/src/libav.c new file mode 100644 index 00000000..7934c52e --- /dev/null +++ b/src/libav.c @@ -0,0 +1,153 @@ +#include "libav.h" + +/** + * + */ +static void +libav_log_callback(void *ptr, int level, const char *fmt, va_list vl) +{ + char message[8192]; + char *nl; + char *l; + + memset(message, 0, sizeof(message)); + vsnprintf(message, sizeof(message), fmt, vl); + + l = message; + + if(level == AV_LOG_DEBUG) + level = LOG_DEBUG; + else if(level == AV_LOG_VERBOSE) + level = LOG_INFO; + else if(level == AV_LOG_INFO) + level = LOG_NOTICE; + else if(level == AV_LOG_WARNING) + level = LOG_WARNING; + else if(level == AV_LOG_ERROR) + level = LOG_ERR; + else if(level == AV_LOG_FATAL) + level = LOG_CRIT; + else if(level == AV_LOG_PANIC) + level = LOG_EMERG; + + while(l < message + sizeof(message)) { + nl = strstr(l, "\n"); + if(nl) + *nl = '\0'; + + if(!strlen(l)) + break; + + tvhlog(level, "libav", "%s", l); + + l += strlen(message); + + if(!nl) + break; + } +} + +/** + * Translate a component type to a libavcodec id + */ +enum CodecID +streaming_component_type2codec_id(streaming_component_type_t type) +{ + enum CodecID codec_id = CODEC_ID_NONE; + + switch(type) { + case SCT_H264: + codec_id = CODEC_ID_H264; + break; + case SCT_MPEG2VIDEO: + codec_id = CODEC_ID_MPEG2VIDEO; + break; + case SCT_AC3: + codec_id = CODEC_ID_AC3; + break; + case SCT_EAC3: + codec_id = CODEC_ID_EAC3; + break; + case SCT_AAC: + codec_id = CODEC_ID_AAC; + break; + case SCT_MPEG2AUDIO: + codec_id = CODEC_ID_MP2; + break; + case SCT_DVBSUB: + codec_id = CODEC_ID_DVB_SUBTITLE; + break; + case SCT_TEXTSUB: + codec_id = CODEC_ID_TEXT; + break; + case SCT_TELETEXT: + codec_id = CODEC_ID_DVB_TELETEXT; + break; + default: + codec_id = CODEC_ID_NONE; + break; + } + + 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; +} + + +/** + * + */ +void +libav_init(void) +{ + av_log_set_callback(libav_log_callback); + av_log_set_level(AV_LOG_VERBOSE); + av_register_all(); +} + diff --git a/src/libav.h b/src/libav.h new file mode 100644 index 00000000..98856f90 --- /dev/null +++ b/src/libav.h @@ -0,0 +1,31 @@ +/* + * tvheadend, libav utils + * Copyright (C) 2012 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 . + */ + +#ifndef LIBAV_H_ +#define LIBAV_H_ + + +#include +#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 66d863fc..8a2323f8 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,6 @@ /* * TVheadend - * Copyright (C) 2007 - 2010 Andreas Öman + * Copyright (C) 2007 - 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 @@ -49,7 +49,7 @@ #include "cwc.h" #include "capmt.h" #include "dvr/dvr.h" -#include "htsp.h" +#include "htsp_server.h" #include "rawtsinput.h" #include "avahi.h" #include "iptv_input.h" @@ -60,22 +60,100 @@ #include "ffdecsa/FFdecsa.h" #include "muxes.h" #include "config2.h" +#include "imagecache.h" +#include "timeshift.h" +#if ENABLE_LIBAV +#include "libav.h" +#include "plumbing/transcoding.h" +#endif + +/* Command line option struct */ +typedef struct { + const char sopt; + const char *lopt; + const char *desc; + enum { + OPT_STR, + OPT_INT, + OPT_BOOL + } type; + void *param; +} cmdline_opt_t; + +static cmdline_opt_t* cmdline_opt_find + ( cmdline_opt_t *opts, int num, const char *arg ) +{ + int i; + int isshort = 0; + + if (strlen(arg) < 2 || *arg != '-') + return NULL; + arg++; + + if (strlen(arg) == 1) + isshort = 1; + else if (*arg == '-') + arg++; + else + return NULL; + + for (i = 0; i < num; i++) { + if (!opts[i].lopt) continue; + if (isshort && opts[i].sopt == *arg) + return &opts[i]; + if (!isshort && !strcmp(opts[i].lopt, arg)) + return &opts[i]; + } + + return NULL; +} + +/* + * Globals + */ +int tvheadend_webui_port; +int tvheadend_webui_debug; +int tvheadend_htsp_port; +int tvheadend_htsp_port_extra; +const char *tvheadend_cwd; +const char *tvheadend_webroot; +const tvh_caps_t tvheadend_capabilities[] = { +#if ENABLE_CWC + { "cwc", NULL }, +#endif +#if ENABLE_V4L + { "v4l", NULL }, +#endif +#if ENABLE_LINUXDVB + { "linuxdvb", NULL }, +#endif +#if ENABLE_LIBAV + { "transcoding", &transcoding_enabled }, +#endif +#if ENABLE_IMAGECACHE + { "imagecache", &imagecache_enabled }, +#endif +#if ENABLE_TIMESHIFT + { "timeshift", ×hift_enabled }, +#endif +#if ENABLE_TRACE + { "trace", NULL }, +#endif + { NULL, NULL } +}; -int running; time_t dispatch_clock; -static LIST_HEAD(, gtimer) gtimers; pthread_mutex_t global_lock; pthread_mutex_t ffmpeg_lock; pthread_mutex_t fork_lock; -static int log_stderr; -static int log_decorate; +pthread_mutex_t atomic_lock; -int log_debug_to_syslog; -int log_debug_to_console; - -int webui_port; -int htsp_port; -char *tvheadend_cwd; +/* + * Locals + */ +static int running; +static LIST_HEAD(, gtimer) gtimers; +static pthread_cond_t gtimer_cond; static void handle_sigpipe(int x) @@ -111,31 +189,50 @@ get_user_groups (const struct passwd *pw, gid_t* glist, size_t gmax) static int gtimercmp(gtimer_t *a, gtimer_t *b) { - if(a->gti_expire < b->gti_expire) + if(a->gti_expire.tv_sec < b->gti_expire.tv_sec) return -1; - else if(a->gti_expire > b->gti_expire) + if(a->gti_expire.tv_sec > b->gti_expire.tv_sec) return 1; - return 0; + if(a->gti_expire.tv_nsec < b->gti_expire.tv_nsec) + return -1; + if(a->gti_expire.tv_nsec > b->gti_expire.tv_nsec) + return 1; + return -1; } - /** * */ void -gtimer_arm_abs(gtimer_t *gti, gti_callback_t *callback, void *opaque, - time_t when) +gtimer_arm_abs2 + (gtimer_t *gti, gti_callback_t *callback, void *opaque, struct timespec *when) { lock_assert(&global_lock); - if(gti->gti_callback != NULL) + if (gti->gti_callback != NULL) LIST_REMOVE(gti, gti_link); - + gti->gti_callback = callback; - gti->gti_opaque = opaque; - gti->gti_expire = when; + gti->gti_opaque = opaque; + gti->gti_expire = *when; LIST_INSERT_SORTED(>imers, gti, gti_link, gtimercmp); + + if (LIST_FIRST(>imers) == gti) + pthread_cond_signal(>imer_cond); // force timer re-check +} + +/** + * + */ +void +gtimer_arm_abs + (gtimer_t *gti, gti_callback_t *callback, void *opaque, time_t when) +{ + struct timespec ts; + ts.tv_nsec = 0; + ts.tv_sec = when; + gtimer_arm_abs2(gti, callback, opaque, &ts); } /** @@ -144,10 +241,22 @@ gtimer_arm_abs(gtimer_t *gti, gti_callback_t *callback, void *opaque, void gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque, int delta) { - time_t now; - time(&now); - - gtimer_arm_abs(gti, callback, opaque, now + delta); + gtimer_arm_abs(gti, callback, opaque, dispatch_clock + delta); +} + +/** + * + */ +void +gtimer_arm_ms + (gtimer_t *gti, gti_callback_t *callback, void *opaque, long delta_ms ) +{ + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += (1000000 * delta_ms); + ts.tv_sec += (ts.tv_nsec / 1000000000); + ts.tv_nsec %= 1000000000; + gtimer_arm_abs2(gti, callback, opaque, &ts); } /** @@ -162,47 +271,61 @@ gtimer_disarm(gtimer_t *gti) } } +/** + * Show version info + */ +static void +show_version(const char *argv0) +{ + printf("%s: version %s\n", argv0, tvheadend_version); + exit(0); +} + /** * */ static void -usage(const char *argv0) +show_usage + (const char *argv0, cmdline_opt_t *opts, int num, const char *err, ...) { - printf("HTS Tvheadend %s\n", tvheadend_version); - printf("usage: %s [options]\n", argv0); + int i; + char buf[256]; + printf("Usage: %s [OPTIONS]\n", argv0); + for (i = 0; i < num; i++) { + + /* Section */ + if (!opts[i].lopt) { + printf("\n%s\n\n", + opts[i].desc); + + /* Option */ + } else { + char sopt[4]; + char *desc, *tok; + if (opts[i].sopt) + snprintf(sopt, sizeof(sopt), "-%c,", opts[i].sopt); + else + strcpy(sopt, " "); + snprintf(buf, sizeof(buf), " %s --%s", sopt, opts[i].lopt); + desc = strdup(opts[i].desc); + tok = strtok(desc, "\n"); + while (tok) { + printf("%-30s%s\n", buf, tok); + tok = buf; + while (*tok) { + *tok = ' '; + tok++; + } + tok = strtok(NULL, "\n"); + } + free(desc); + } + } printf("\n"); - printf(" -a Use only DVB adapters specified (csv)\n"); - printf(" -c Alternate configuration path.\n" - " Defaults to [$HOME/.hts/tvheadend]\n"); - printf(" -m Alternate mux configuration directory\n"); - printf(" -f Fork and daemonize\n"); - printf(" -p Write pid to instead of /var/run/tvheadend.pid,\n" - " only works with -f\n"); - printf(" -u Run as user , only works with -f\n"); - printf(" -g Run as group , only works with -f\n"); - printf(" -C If no useraccount exist then create one with\n" - " no username and no password. Use with care as\n" - " it will allow world-wide administrative access\n" - " to your Tvheadend installation until you edit\n" - " the access-control from within the Tvheadend UI\n"); - printf(" -s Log debug to syslog\n"); - printf(" -w WebUI access port [default 9981]\n"); - printf(" -e HTSP access port [default 9982]\n"); - printf("\n"); - printf("Development options\n"); - printf("\n"); - printf(" -d Log debug to console\n"); - printf(" -j Statically join the given transport id\n"); - printf(" -r Read the given transport stream file and present\n" - " found services as channels\n"); - printf(" -A Immediately call abort()\n"); - - printf("\n"); - printf("For more information read the man page or visit\n"); - printf(" http://www.lonelycoder.com/hts/\n"); + printf("For more information please visit the Tvheadend website:\n"); + printf(" https://tvheadend.org\n"); printf("\n"); exit(0); - } @@ -215,28 +338,50 @@ mainloop(void) { gtimer_t *gti; gti_callback_t *cb; + struct timespec ts; while(running) { - sleep(1); - spawn_reaper(); + clock_gettime(CLOCK_REALTIME, &ts); - time(&dispatch_clock); + /* 1sec stuff */ + if (ts.tv_sec > dispatch_clock) { + dispatch_clock = ts.tv_sec; - comet_flush(); /* Flush idle comet mailboxes */ + spawn_reaper(); /* reap spawned processes */ + comet_flush(); /* Flush idle comet mailboxes */ + } + + /* Global timers */ pthread_mutex_lock(&global_lock); + + // TODO: there is a risk that if timers re-insert themselves to + // the top of the list with a 0 offset we could loop indefinitely while((gti = LIST_FIRST(>imers)) != NULL) { - if(gti->gti_expire > dispatch_clock) - break; - + if ((gti->gti_expire.tv_sec > ts.tv_sec) || + ((gti->gti_expire.tv_sec == ts.tv_sec) && + (gti->gti_expire.tv_nsec > ts.tv_nsec))) { + ts = gti->gti_expire; + break; + } + cb = gti->gti_callback; + LIST_REMOVE(gti, gti_link); gti->gti_callback = NULL; cb(gti->gti_opaque); - } + + /* Bound wait */ + if ((LIST_FIRST(>imers) == NULL) || (ts.tv_sec > (dispatch_clock + 1))) { + ts.tv_sec = dispatch_clock + 1; + ts.tv_nsec = 0; + } + + /* Wait */ + pthread_cond_timedwait(>imer_cond, &global_lock, &ts); pthread_mutex_unlock(&global_lock); } } @@ -248,203 +393,346 @@ mainloop(void) int main(int argc, char **argv) { - int c; - int forkaway = 0; - FILE *pidfile; - const char *pidpath = "/var/run/tvheadend.pid"; - struct group *grp; - struct passwd *pw; - const char *usernam = NULL; - const char *groupnam = NULL; - int logfacility = LOG_DAEMON; - int createdefault = 0; + int i; sigset_t set; - const char *homedir; - const char *rawts_input = NULL; - const char *join_transport = NULL; - const char *confpath = NULL; - char *p, *endp; - uint32_t adapter_mask = 0xffffffff; - int crash = 0; - webui_port = 9981; - htsp_port = 9982; +#if ENABLE_LINUXDVB + uint32_t adapter_mask; +#endif + int log_level = LOG_INFO; + int log_options = TVHLOG_OPT_MILLIS | TVHLOG_OPT_STDERR | TVHLOG_OPT_SYSLOG; + const char *log_subsys = NULL; + + /* Defaults */ + tvheadend_webui_port = 9981; + tvheadend_webroot = NULL; + tvheadend_htsp_port = 9982; + tvheadend_htsp_port_extra = 0; + + /* Command line options */ + int opt_help = 0, + opt_version = 0, + opt_fork = 0, + opt_firstrun = 0, + opt_stderr = 0, + opt_syslog = 0, + opt_uidebug = 0, + opt_abort = 0, + opt_noacl = 0, + opt_trace = 0, + opt_fileline = 0, + opt_ipv6 = 0; + const char *opt_config = NULL, + *opt_user = NULL, + *opt_group = NULL, + *opt_logpath = NULL, + *opt_log_subsys = NULL, + *opt_pidpath = "/var/run/tvheadend.pid", +#if ENABLE_LINUXDVB + *opt_dvb_adapters = NULL, + *opt_dvb_raw = NULL, +#endif + *opt_rawts = NULL, + *opt_bindaddr = NULL, + *opt_subscribe = NULL; + cmdline_opt_t cmdline_opts[] = { + { 0, NULL, "Generic Options", OPT_BOOL, NULL }, + { 'h', "help", "Show this page", OPT_BOOL, &opt_help }, + { 'v', "version", "Show version infomation", OPT_BOOL, &opt_version }, + + { 0, NULL, "Service Configuration", OPT_BOOL, NULL }, + { 'c', "config", "Alternate config path", OPT_STR, &opt_config }, + { 'f', "fork", "Fork and run as daemon", OPT_BOOL, &opt_fork }, + { 'u', "user", "Run as user", OPT_STR, &opt_user }, + { 'g', "group", "Run as group", OPT_STR, &opt_group }, + { 'p', "pid", "Alternate pid path", OPT_STR, &opt_pidpath }, + { 'C', "firstrun", "If no user account exists then create one with\n" + "no username and no password. Use with care as\n" + "it will allow world-wide administrative access\n" + "to your Tvheadend installation until you edit\n" + "the access-control from within the Tvheadend UI", + OPT_BOOL, &opt_firstrun }, +#if ENABLE_LINUXDVB + { 'a', "adapters", "Only use specified DVB adapters (comma separated)", + OPT_STR, &opt_dvb_adapters }, +#endif + { 0, NULL, "Server Connectivity", OPT_BOOL, NULL }, + { '6', "ipv6", "Listen on IPv6", OPT_BOOL, &opt_ipv6 }, + { 'b', "bindaddr", "Specify bind address", OPT_STR, &opt_bindaddr}, + { 0, "http_port", "Specify alternative http port", + OPT_INT, &tvheadend_webui_port }, + { 0, "http_root", "Specify alternative http webroot", + OPT_STR, &tvheadend_webroot }, + { 0, "htsp_port", "Specify alternative htsp port", + OPT_INT, &tvheadend_htsp_port }, + { 0, "htsp_port2", "Specify extra htsp port", + OPT_INT, &tvheadend_htsp_port_extra }, + + { 0, NULL, "Debug Options", OPT_BOOL, NULL }, + { 'd', "stderr", "Enable debug on stderr", OPT_BOOL, &opt_stderr }, + { 's', "syslog", "Enable debug to syslog", OPT_BOOL, &opt_syslog }, + { 'l', "logfile", "Enable debug to file", OPT_STR, &opt_logpath }, + { 0, "subsys", "Enable debug subsystems", OPT_STR, &opt_log_subsys }, + { 0, "fileline", "Add file and line numbers to debug", OPT_BOOL, &opt_fileline }, +#if ENABLE_TRACE + { 0, "trace", "Enable low level debug", OPT_BOOL, &opt_trace }, +#endif + { 0, "uidebug", "Enable webUI debug (non-minified JS)", OPT_BOOL, &opt_uidebug }, + { 'A', "abort", "Immediately abort", OPT_BOOL, &opt_abort }, + { 0, "noacl", "Disable all access control checks", + OPT_BOOL, &opt_noacl }, +#if ENABLE_LINUXDVB + { 'R', "dvbraw", "Use rawts file to create virtual adapter", + OPT_STR, &opt_dvb_raw }, +#endif + { 'r', "rawts", "Use rawts file to generate virtual services", + OPT_STR, &opt_rawts }, + { 'j', "join", "Subscribe to a service permanently", + OPT_STR, &opt_subscribe } + }; /* Get current directory */ - tvheadend_cwd = dirname(dirname(strdup(argv[0]))); + tvheadend_cwd = dirname(dirname(tvh_strdupa(argv[0]))); /* Set locale */ setlocale(LC_ALL, ""); + setlocale(LC_NUMERIC, "C"); - // make sure the timezone is set + /* make sure the timezone is set */ tzset(); - while((c = getopt(argc, argv, "Aa:fp:u:g:c:Chdr:j:sw:e:")) != -1) { - switch(c) { - case 'a': - adapter_mask = 0x0; - p = strtok(optarg, ","); - if (p != NULL) { - do { - int adapter = strtol(p, &endp, 10); - if (*endp != 0 || adapter < 0 || adapter > 31) { - fprintf(stderr, "Invalid adapter number '%s'\n", p); - return 1; - } - adapter_mask |= (1 << adapter); - } while ((p = strtok(NULL, ",")) != NULL); - if (adapter_mask == 0x0) { - fprintf(stderr, "No adapters specified!\n"); - return 1; - } - } else { - usage(argv[0]); - } - break; - case 'A': - crash = 1; - break; - case 'f': - forkaway = 1; - break; - case 'p': - pidpath = optarg; - break; - case 'w': - webui_port = atoi(optarg); - break; - case 'e': - htsp_port = atoi(optarg); - break; - case 'u': - usernam = optarg; - break; - case 'g': - groupnam = optarg; - break; - case 'c': - confpath = optarg; - break; - case 'd': - log_debug_to_console = 1; - break; - case 's': - log_debug_to_syslog = 1; - break; - case 'C': - createdefault = 1; - break; - case 'r': - rawts_input = optarg; - break; - case 'j': - join_transport = optarg; - break; - default: - usage(argv[0]); - } + /* Process command line */ + for (i = 1; i < argc; i++) { + + /* Find option */ + cmdline_opt_t *opt + = cmdline_opt_find(cmdline_opts, ARRAY_SIZE(cmdline_opts), argv[i]); + if (!opt) + show_usage(argv[0], cmdline_opts, ARRAY_SIZE(cmdline_opts), + "invalid option specified [%s]", argv[i]); + + /* Process */ + if (opt->type == OPT_BOOL) + *((int*)opt->param) = 1; + else if (++i == argc) + show_usage(argv[0], cmdline_opts, ARRAY_SIZE(cmdline_opts), + "option %s requires a value", opt->lopt); + else if (opt->type == OPT_INT) + *((int*)opt->param) = atoi(argv[i]); + else + *((char**)opt->param) = argv[i]; + + /* Stop processing */ + if (opt_help) + show_usage(argv[0], cmdline_opts, ARRAY_SIZE(cmdline_opts), NULL); + if (opt_version) + show_version(argv[0]); } - signal(SIGPIPE, handle_sigpipe); + /* Additional cmdline processing */ +#if ENABLE_LINUXDVB + if (!opt_dvb_adapters) { + adapter_mask = ~0; + } else { + char *p, *e; + char *r = NULL; + char *dvb_adapters = strdup(opt_dvb_adapters); + adapter_mask = 0x0; + p = strtok_r(dvb_adapters, ",", &r); + while (p) { + int a = strtol(p, &e, 10); + if (*e != 0 || a < 0 || a > 31) { + tvhlog(LOG_ERR, "START", "Invalid adapter number '%s'", p); + free(dvb_adapters); + return 1; + } + adapter_mask |= (1 << a); + p = strtok_r(NULL, ",", &r); + } + free(dvb_adapters); + if (!adapter_mask) { + tvhlog(LOG_ERR, "START", "No adapters specified!"); + return 1; + } + } +#endif + if (tvheadend_webroot) { + char *tmp; + if (*tvheadend_webroot == '/') + tmp = strdup(tvheadend_webroot); + else { + tmp = malloc(strlen(tvheadend_webroot)+1); + *tmp = '/'; + strcpy(tmp+1, tvheadend_webroot); + } + if (tmp[strlen(tmp)-1] == '/') + tmp[strlen(tmp)-1] = '\0'; + tvheadend_webroot = tmp; + } - if(forkaway) { - grp = getgrnam(groupnam ?: "video"); - pw = usernam ? getpwnam(usernam) : NULL; + /* Setup logging */ + if (isatty(2)) + log_options |= TVHLOG_OPT_DECORATE; + if (opt_stderr || opt_syslog || opt_logpath) { + log_subsys = "all"; + log_level = LOG_DEBUG; + if (opt_stderr) + log_options |= TVHLOG_OPT_DBG_STDERR; + if (opt_syslog) + log_options |= TVHLOG_OPT_DBG_SYSLOG; + if (opt_logpath) + log_options |= TVHLOG_OPT_DBG_FILE; + } + if (opt_fileline) + log_options |= TVHLOG_OPT_FILELINE; + if (opt_trace) + log_level = LOG_TRACE; + if (opt_log_subsys) + log_subsys = opt_log_subsys; + + tvhlog_init(log_level, log_options, opt_logpath); + tvhlog_set_subsys(log_subsys); + + signal(SIGPIPE, handle_sigpipe); // will be redundant later + + /* Daemonise */ + if(opt_fork) { + const char *homedir; + gid_t gid; + uid_t uid; + struct group *grp = getgrnam(opt_group ?: "video"); + struct passwd *pw = opt_user ? getpwnam(opt_user) : NULL; + FILE *pidfile = fopen(opt_pidpath, "w+"); + + if(grp != NULL) { + gid = grp->gr_gid; + } else { + gid = 1; + } + + if (pw != NULL) { + if (getuid() != pw->pw_uid) { + gid_t glist[10]; + int gnum; + gnum = get_user_groups(pw, glist, 10); + if (setgroups(gnum, glist)) { + tvhlog(LOG_ALERT, "START", + "setgroups() failed, do you have permission?"); + return 1; + } + } + uid = pw->pw_uid; + homedir = pw->pw_dir; + setenv("HOME", homedir, 1); + } else { + uid = 1; + } + if ((getgid() != gid) && setgid(gid)) { + tvhlog(LOG_ALERT, "START", + "setgid() failed, do you have permission?"); + return 1; + } + if ((getuid() != uid) && setuid(uid)) { + tvhlog(LOG_ALERT, "START", + "setuid() failed, do you have permission?"); + return 1; + } if(daemon(0, 0)) { exit(2); } - pidfile = fopen(pidpath, "w+"); if(pidfile != NULL) { fprintf(pidfile, "%d\n", getpid()); fclose(pidfile); } - if(grp != NULL) { - setgid(grp->gr_gid); - } else { - setgid(1); - } - - if (pw != NULL) { - gid_t glist[10]; - int gnum = get_user_groups(pw, glist, 10); - setgroups(gnum, glist); - setuid(pw->pw_uid); - homedir = pw->pw_dir; - setenv("HOME", homedir, 1); - } else { - setuid(1); - } - umask(0); } - log_stderr = !forkaway; - log_decorate = isatty(2); - - sigfillset(&set); - sigprocmask(SIG_BLOCK, &set, NULL); - - openlog("tvheadend", LOG_PID, logfacility); - - hts_settings_init(confpath); + /* Alter logging */ + if (opt_fork) + tvhlog_options &= ~TVHLOG_OPT_STDERR; + if (!isatty(2)) + tvhlog_options &= ~TVHLOG_OPT_DECORATE; + + /* Initialise configuration */ + hts_settings_init(opt_config); + /* Setup global mutexes */ pthread_mutex_init(&ffmpeg_lock, NULL); pthread_mutex_init(&fork_lock, NULL); pthread_mutex_init(&global_lock, NULL); - + pthread_mutex_init(&atomic_lock, NULL); pthread_mutex_lock(&global_lock); + pthread_cond_init(>imer_cond, NULL); time(&dispatch_clock); + /* Signal handling */ + sigfillset(&set); + sigprocmask(SIG_BLOCK, &set, NULL); trap_init(argv[0]); /** * Initialize subsystems */ +#if ENABLE_LIBAV + libav_init(); +#endif + config_init(); - muxes_init(); + imagecache_init(); service_init(); channels_init(); - access_init(createdefault); + subscription_init(); + + access_init(opt_firstrun, opt_noacl); - tcp_server_init(); #if ENABLE_LINUXDVB - dvb_init(adapter_mask); + muxes_init(); + dvb_init(adapter_mask, opt_dvb_raw); #endif + iptv_input_init(); + #if ENABLE_V4L v4l_init(); #endif - http_server_init(); +#if ENABLE_TIMESHIFT + timeshift_init(); +#endif + + tcp_server_init(opt_ipv6); + http_server_init(opt_bindaddr); webui_init(); serviceprobe_init(); +#if ENABLE_CWC cwc_init(); - capmt_init(); +#if (!ENABLE_DVBCSA) + ffdecsa_init(); +#endif +#endif epggrab_init(); epg_init(); dvr_init(); - htsp_init(); + htsp_init(opt_bindaddr); - ffdecsa_init(); - - if(rawts_input != NULL) - rawts_init(rawts_input); + if(opt_rawts != NULL) + rawts_init(opt_rawts); - if(join_transport != NULL) - subscription_dummy_join(join_transport, 1); + if(opt_subscribe != NULL) + subscription_dummy_join(opt_subscribe, 1); #ifdef CONFIG_AVAHI avahi_init(); @@ -454,7 +742,6 @@ main(int argc, char **argv) pthread_mutex_unlock(&global_lock); - /** * Wait for SIGTERM / SIGINT, but only in this thread */ @@ -474,125 +761,30 @@ main(int argc, char **argv) tvheadend_version, getpid(), getuid(), getgid(), hts_settings_get_root()); - if(crash) + if(opt_abort) abort(); mainloop(); - epg_save(); + // Note: the locking is obviously a bit redundant, but without + // we need to disable the gtimer_arm call in epg_save() + pthread_mutex_lock(&global_lock); + epg_save(NULL); + +#if ENABLE_TIMESHIFT + timeshift_term(); +#endif + pthread_mutex_unlock(&global_lock); tvhlog(LOG_NOTICE, "STOP", "Exiting HTS Tvheadend"); - if(forkaway) - unlink("/var/run/tvheadend.pid"); + if(opt_fork) + unlink(opt_pidpath); return 0; } - -static const char *logtxtmeta[8][2] = { - {"EMERGENCY", "\033[31m"}, - {"ALERT", "\033[31m"}, - {"CRITICAL", "\033[31m"}, - {"ERROR", "\033[31m"}, - {"WARNING", "\033[33m"}, - {"NOTICE", "\033[36m"}, - {"INFO", "\033[32m"}, - {"DEBUG", "\033[32m"}, -}; - -/** - * Internal log function - */ -static void -tvhlogv(int notify, int severity, const char *subsys, const char *fmt, - va_list ap) -{ - char buf[2048]; - char buf2[2048]; - char t[50]; - int l; - struct tm tm; - time_t now; - - l = snprintf(buf, sizeof(buf), "%s: ", subsys); - - vsnprintf(buf + l, sizeof(buf) - l, fmt, ap); - - if(log_debug_to_syslog || severity < LOG_DEBUG) - syslog(severity, "%s", buf); - - /** - * Get time (string) - */ - time(&now); - localtime_r(&now, &tm); - strftime(t, sizeof(t), "%b %d %H:%M:%S", &tm); - - /** - * Send notification to Comet (Push interface to web-clients) - */ - if(notify) { - htsmsg_t *m; - - snprintf(buf2, sizeof(buf2), "%s %s", t, buf); - m = htsmsg_create_map(); - htsmsg_add_str(m, "notificationClass", "logmessage"); - htsmsg_add_str(m, "logtxt", buf2); - comet_mailbox_add_message(m, severity == LOG_DEBUG); - htsmsg_destroy(m); - } - - /** - * Write to stderr - */ - - if(log_stderr && (log_debug_to_console || severity < LOG_DEBUG)) { - const char *leveltxt = logtxtmeta[severity][0]; - const char *sgr = logtxtmeta[severity][1]; - const char *sgroff; - - if(!log_decorate) { - sgr = ""; - sgroff = ""; - } else { - sgroff = "\033[0m"; - } - fprintf(stderr, "%s%s [%s]:%s%s\n", sgr, t, leveltxt, buf, sgroff); - } -} - - -/** - * - */ -void -tvhlog(int severity, const char *subsys, const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - tvhlogv(1, severity, subsys, fmt, ap); - va_end(ap); -} - - -/** - * May be invoked from a forked process so we can't do any notification - * to comet directly. - * - * @todo Perhaps do it via a pipe? - */ -void -tvhlog_spawn(int severity, const char *subsys, const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - tvhlogv(0, severity, subsys, fmt, ap); - va_end(ap); -} - - /** * */ diff --git a/src/misc/dbl.c b/src/misc/dbl.c new file mode 100644 index 00000000..05e5a6dd --- /dev/null +++ b/src/misc/dbl.c @@ -0,0 +1,295 @@ +/* + * Floating point conversion functions. + * Not accurate but should be enough for Showtime's needs + * + * Copyright (C) 2011 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 . + */ + +#define _ISOC99_SOURCE + +#include +#include +#include +#include + +#include "dbl.h" + + +double +my_str2double(const char *str, const char **endp) +{ + double ret = 1.0f; + int n = 0, m = 0, o = 0, e = 0; + + if(*str == '-') { + ret = -1.0f; + str++; + } + + while(*str >= '0' && *str <= '9') + n = n * 10 + *str++ - '0'; + + if(*str != '.') { + ret *= n; + } else { + + str++; + + while(*str >= '0' && *str <= '9') { + o = o * 10 + *str++ - '0'; + m--; + } + + ret *= (n + pow(10, m) * o); + } + + if(*str == 'e' || *str == 'E') { + int esign = 1; + str++; + + if(*str == '+') + str++; + else if(*str == '-') { + str++; + esign = -1; + } + + while(*str >= '0' && *str <= '9') + e = e * 10 + *str++ - '0'; + ret *= pow(10, e * esign); + } + + if(endp != NULL) + *endp = str; + + return ret; +} + + + + +/* +** The code that follow is based on "printf" code that dates from the +** 1980s. It is in the public domain. The original comments are +** included here for completeness. They are very out-of-date but +** might be useful as an historical reference. +** +************************************************************************** +** +** The following modules is an enhanced replacement for the "printf" subroutines +** found in the standard C library. The following enhancements are +** supported: +** +** + Additional functions. The standard set of "printf" functions +** includes printf, fprintf, sprintf, vprintf, vfprintf, and +** vsprintf. This module adds the following: +** +** * snprintf -- Works like sprintf, but has an extra argument +** which is the size of the buffer written to. +** +** * mprintf -- Similar to sprintf. Writes output to memory +** obtained from malloc. +** +** * xprintf -- Calls a function to dispose of output. +** +** * nprintf -- No output, but returns the number of characters +** that would have been output by printf. +** +** * A v- version (ex: vsnprintf) of every function is also +** supplied. +** +** + A few extensions to the formatting notation are supported: +** +** * The "=" flag (similar to "-") causes the output to be +** be centered in the appropriately sized field. +** +** * The %b field outputs an integer in binary notation. +** +** * The %c field now accepts a precision. The character output +** is repeated by the number of times the precision specifies. +** +** * The %' field works like %c, but takes as its character the +** next character of the format string, instead of the next +** argument. For example, printf("%.78'-") prints 78 minus +** signs, the same as printf("%.78c",'-'). +** +** + When compiled using GCC on a SPARC, this version of printf is +** faster than the library printf for SUN OS 4.1. +** +** + All functions are fully reentrant. +** +*/ + + +static char +getdigit(double *val, int *cnt) +{ + int digit; + double d; + if( (*cnt)++ >= 16 ) return '0'; + digit = (int)*val; + d = digit; + digit += '0'; + *val = (*val - d)*10.0; + return (char)digit; +} + +#define xGENERIC 0 +#define xFLOAT 1 +#define xEXP 2 + + +int +my_double2str(char *buf, size_t bufsize, double realvalue) +{ + int precision = -1; + char *bufpt; + char prefix; + char xtype = xGENERIC; + int idx, exp, e2; + double rounder; + char flag_exp; + char flag_rtz; + char flag_dp; + char flag_alternateform = 0; + char flag_altform2 = 0; + int nsd; + + if(bufsize < 8) + return -1; + + if( precision<0 ) precision = 20; /* Set default precision */ + if( precision>bufsize/2-10 ) precision = bufsize/2-10; + if( realvalue<0.0 ){ + realvalue = -realvalue; + prefix = '-'; + }else{ + prefix = 0; + } + if( xtype==xGENERIC && precision>0 ) precision--; + for(idx=precision, rounder=0.5; idx>0; idx--, rounder*=0.1){} + + if( xtype==xFLOAT ) realvalue += rounder; + /* Normalize realvalue to within 10.0 > realvalue >= 1.0 */ + exp = 0; + + if(isnan(realvalue)) { + strcpy(buf, "NaN"); + return 0; + } + + if( realvalue>0.0 ){ + while( realvalue>=1e32 && exp<=350 ){ realvalue *= 1e-32; exp+=32; } + while( realvalue>=1e8 && exp<=350 ){ realvalue *= 1e-8; exp+=8; } + while( realvalue>=10.0 && exp<=350 ){ realvalue *= 0.1; exp++; } + while( realvalue<1e-8 ){ realvalue *= 1e8; exp-=8; } + while( realvalue<1.0 ){ realvalue *= 10.0; exp--; } + if( exp>350 ){ + if( prefix=='-' ){ + strcpy(buf, "-Inf"); + }else{ + strcpy(buf, "Inf"); + } + return 0; + } + } + bufpt = buf; + + /* + ** If the field type is etGENERIC, then convert to either etEXP + ** or etFLOAT, as appropriate. + */ + flag_exp = xtype==xEXP; + if( xtype != xFLOAT ){ + realvalue += rounder; + if( realvalue>=10.0 ){ realvalue *= 0.1; exp++; } + } + if( xtype==xGENERIC ){ + flag_rtz = !flag_alternateform; + if( exp<-4 || exp>precision ){ + xtype = xEXP; + }else{ + precision = precision - exp; + xtype = xFLOAT; + } + }else{ + flag_rtz = 0; + } + if( xtype==xEXP ){ + e2 = 0; + }else{ + e2 = exp; + } + nsd = 0; + flag_dp = (precision>0 ?1:0) | flag_alternateform | flag_altform2; + /* The sign in front of the number */ + if( prefix ){ + *(bufpt++) = prefix; + } + /* Digits prior to the decimal point */ + if( e2<0 ){ + *(bufpt++) = '0'; + }else{ + for(; e2>=0; e2--){ + *(bufpt++) = getdigit(&realvalue,&nsd); + } + } + /* The decimal point */ + if( flag_dp ){ + *(bufpt++) = '.'; + } + /* "0" digits after the decimal point but before the first + ** significant digit of the number */ + for(e2++; e2<0; precision--, e2++){ + assert( precision>0 ); + *(bufpt++) = '0'; + } + /* Significant digits after the decimal point */ + while( (precision--)>0 ){ + *(bufpt++) = getdigit(&realvalue,&nsd); + } + + /* Remove trailing zeros and the "." if no digits follow the "." */ + if( flag_rtz && flag_dp ){ + while( bufpt[-1]=='0' ) *(--bufpt) = 0; + assert( bufpt>buf ); + if( bufpt[-1]=='.' ){ + if( flag_altform2 ){ + *(bufpt++) = '0'; + }else{ + *(--bufpt) = 0; + } + } + } + /* Add the "eNNN" suffix */ + if( flag_exp || xtype==xEXP ){ + *(bufpt++) = 'e'; + if( exp<0 ){ + *(bufpt++) = '-'; exp = -exp; + }else{ + *(bufpt++) = '+'; + } + if( exp>=100 ){ + *(bufpt++) = (char)((exp/100)+'0'); /* 100's digit */ + exp %= 100; + } + *(bufpt++) = (char)(exp/10+'0'); /* 10's digit */ + *(bufpt++) = (char)(exp%10+'0'); /* 1's digit */ + } + *bufpt = 0; + return 0; +} + diff --git a/src/misc/dbl.h b/src/misc/dbl.h new file mode 100644 index 00000000..fc05259c --- /dev/null +++ b/src/misc/dbl.h @@ -0,0 +1,5 @@ +#pragma once + +double my_str2double(const char *str, const char **endp); + +int my_double2str(char *buf, size_t bufsize, double realvalue); diff --git a/src/misc/json.c b/src/misc/json.c new file mode 100644 index 00000000..08f8a74d --- /dev/null +++ b/src/misc/json.c @@ -0,0 +1,438 @@ +/* + * JSON helpers + * Copyright (C) 2011 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 "json.h" +#include "dbl.h" +#include "tvheadend.h" + +#define NOT_THIS_TYPE ((void *)-1) + +static const char *json_parse_value(const char *s, void *parent, + const char *name, + const json_deserializer_t *jd, + void *opaque, + const char **failp, const char **failmsg); + +/** + * Returns a newly allocated string + */ +static char * +json_parse_string(const char *s, const char **endp, + const char **failp, const char **failmsg) +{ + const char *start; + char *r, *a, *b; + int l, esc = 0; + + while(*s > 0 && *s < 33) + s++; + + if(*s != '"') + return NOT_THIS_TYPE; + + s++; + start = s; + + while(1) { + if(*s == 0) { + *failmsg = "Unexpected end of JSON message"; + *failp = s; + return NULL; + } + + if(*s == '\\') { + esc = 1; + /* skip the escape */ + s++; + if (*s == 'u') s += 4; + // Note: we could detect the lack of support here! + } else if(*s == '"') { + + *endp = s + 1; + + /* End */ + l = s - start; + r = malloc(l + 1); + memcpy(r, start, l); + r[l] = 0; + + if(esc) { + /* Do deescaping inplace */ + + a = b = r; + + while(*a) { + if(*a == '\\') { + a++; + if(*a == 'b') + *b++ = '\b'; + else if(*a == '\\') + *b++ = '\\'; + else if(*a == 'f') + *b++ = '\f'; + else if(*a == 'n') + *b++ = '\n'; + else if(*a == 'r') + *b++ = '\r'; + else if(*a == 't') + *b++ = '\t'; + else if(*a == 'u') { + // Unicode character + int i, v = 0; + + a++; + for(i = 0; i < 4; i++) { + v = v << 4; + switch(a[i]) { + case '0' ... '9': + v |= a[i] - '0'; + break; + case 'a' ... 'f': + v |= a[i] - 'a' + 10; + break; + case 'A' ... 'F': + v |= a[i] - 'F' + 10; + break; + default: + free(r); + *failmsg = "Incorrect escape sequence"; + *failp = (a - r) + start; + return NULL; + } + } + a+=3; + b += put_utf8(b, v); + } else { + *b++ = *a; + } + a++; + } else { + *b++ = *a++; + } + } + *b = 0; + } + return r; + } + s++; + } +} + + +/** + * + */ +static void * +json_parse_map(const char *s, const char **endp, const json_deserializer_t *jd, + void *opaque, const char **failp, const char **failmsg) + +{ + char *name; + const char *s2; + void *r; + + while(*s > 0 && *s < 33) + s++; + + if(*s != '{') + return NOT_THIS_TYPE; + + s++; + + r = jd->jd_create_map(opaque); + + while(*s > 0 && *s < 33) + s++; + + if(*s != '}') { + + while(1) { + name = json_parse_string(s, &s2, failp, failmsg); + if(name == NOT_THIS_TYPE) { + *failmsg = "Expected string"; + *failp = s; + return NULL; + } + + if(name == NULL) + return NULL; + + s = s2; + + while(*s > 0 && *s < 33) + s++; + + if(*s != ':') { + jd->jd_destroy_obj(opaque, r); + free(name); + *failmsg = "Expected ':'"; + *failp = s; + return NULL; + } + s++; + + s2 = json_parse_value(s, r, name, jd, opaque, failp, failmsg); + free(name); + + if(s2 == NULL) { + jd->jd_destroy_obj(opaque, r); + return NULL; + } + + s = s2; + + while(*s > 0 && *s < 33) + s++; + + if(*s == '}') + break; + + if(*s != ',') { + jd->jd_destroy_obj(opaque, r); + *failmsg = "Expected ','"; + *failp = s; + return NULL; + } + s++; + } + } + + s++; + *endp = s; + return r; +} + + +/** + * + */ +static void * +json_parse_list(const char *s, const char **endp, const json_deserializer_t *jd, + void *opaque, const char **failp, const char **failmsg) +{ + const char *s2; + void *r; + + while(*s > 0 && *s < 33) + s++; + + if(*s != '[') + return NOT_THIS_TYPE; + + s++; + + r = jd->jd_create_list(opaque); + + while(*s > 0 && *s < 33) + s++; + + if(*s != ']') { + + while(1) { + + s2 = json_parse_value(s, r, NULL, jd, opaque, failp, failmsg); + + if(s2 == NULL) { + jd->jd_destroy_obj(opaque, r); + return NULL; + } + + s = s2; + + while(*s > 0 && *s < 33) + s++; + + if(*s == ']') + break; + + if(*s != ',') { + jd->jd_destroy_obj(opaque, r); + *failmsg = "Expected ','"; + *failp = s; + return NULL; + } + s++; + } + } + s++; + *endp = s; + return r; +} + +/** + * + */ +static const char * +json_parse_double(const char *s, double *dp) +{ + const char *ep; + while(*s > 0 && *s < 33) + s++; + + double d = my_str2double(s, &ep); + + if(ep == s) + return NULL; + + *dp = d; + return ep; +} + + +/** + * + */ +static char * +json_parse_integer(const char *s, long *lp) +{ + char *ep; + while(*s > 0 && *s < 33) + s++; + const char *s2 = s; + if(*s2 == '-') + s2++; + while(*s2 >= '0' && *s2 <= '9') + s2++; + + if(*s2 == 0) + return NULL; + if(s2[0] == '.' || s2[0] == 'e' || s2[0] == 'E') + return NULL; // Is floating point + + long v = strtol(s, &ep, 10); + if(v == LONG_MIN || v == LONG_MAX) + return NULL; + + if(ep == s) + return NULL; + + *lp = v; + return ep; +} + +/** + * + */ +static const char * +json_parse_value(const char *s, void *parent, const char *name, + const json_deserializer_t *jd, void *opaque, + const char **failp, const char **failmsg) +{ + const char *s2; + char *str; + double d = 0; + long l = 0; + void *c; + + if((c = json_parse_map(s, &s2, jd, opaque, failp, failmsg)) == NULL) + return NULL; + + if(c != NOT_THIS_TYPE) { + jd->jd_add_obj(opaque, parent, name, c); + return s2; + } + + if((c = json_parse_list(s, &s2, jd, opaque, failp, failmsg)) == NULL) + return NULL; + + if(c != NOT_THIS_TYPE) { + jd->jd_add_obj(opaque, parent, name, c); + return s2; + } + + if((str = json_parse_string(s, &s2, failp, failmsg)) == NULL) + return NULL; + + if(str != NOT_THIS_TYPE) { + jd->jd_add_string(opaque, parent, name, str); + return s2; + } + + if((s2 = json_parse_integer(s, &l)) != NULL) { + jd->jd_add_long(opaque, parent, name, l); + return s2; + } else if((s2 = json_parse_double(s, &d)) != NULL) { + jd->jd_add_double(opaque, parent, name, d); + return s2; + } + + while(*s > 0 && *s < 33) + s++; + + if(!strncmp(s, "true", 4)) { + jd->jd_add_bool(opaque, parent, name, 1); + return s + 4; + } + + if(!strncmp(s, "false", 5)) { + jd->jd_add_bool(opaque, parent, name, 0); + return s + 5; + } + + if(!strncmp(s, "null", 4)) { + jd->jd_add_null(opaque, parent, name); + return s + 4; + } + + *failmsg = "Unknown token"; + *failp = s; + return NULL; +} + + +/** + * + */ +void * +json_deserialize(const char *src, const json_deserializer_t *jd, void *opaque, + char *errbuf, size_t errlen) +{ + const char *end; + void *c; + const char *errmsg; + const char *errp; + + c = json_parse_map(src, &end, jd, opaque, &errp, &errmsg); + if(c == NOT_THIS_TYPE) + c = json_parse_list(src, &end, jd, opaque, &errp, &errmsg); + + if(c == NOT_THIS_TYPE) { + snprintf(errbuf, errlen, "Invalid JSON, expected '{' or '['"); + return NULL; + } + + if(c == NULL) { + size_t len = strlen(src); + ssize_t offset = errp - src; + if(offset > len || offset < 0) { + snprintf(errbuf, errlen, "%s at (bad) offset %d", errmsg, (int)offset); + } else { + offset -= 10; + if(offset < 0) + offset = 0; + snprintf(errbuf, errlen, "%s at offset %d : '%.20s'", errmsg, (int)offset, + src + offset); + } + } + return c; +} diff --git a/src/misc/json.h b/src/misc/json.h new file mode 100644 index 00000000..8fc1bf26 --- /dev/null +++ b/src/misc/json.h @@ -0,0 +1,31 @@ +#pragma once + +typedef struct json_deserializer { + void *(*jd_create_map)(void *jd_opaque); + void *(*jd_create_list)(void *jd_opaque); + + void (*jd_destroy_obj)(void *jd_opaque, void *obj); + + void (*jd_add_obj)(void *jd_opaque, void *parent, + const char *name, void *child); + + // str must be free'd by callee + void (*jd_add_string)(void *jd_opaque, void *parent, + const char *name, char *str); + + void (*jd_add_long)(void *jd_opaque, void *parent, + const char *name, long v); + + void (*jd_add_double)(void *jd_opaque, void *parent, + const char *name, double d); + + void (*jd_add_bool)(void *jd_opaque, void *parent, + const char *name, int v); + + void (*jd_add_null)(void *jd_opaque, void *parent, + const char *name); + +} json_deserializer_t; + +void *json_deserialize(const char *src, const json_deserializer_t *jd, + void *opaque, char *errbuf, size_t errlen); diff --git a/src/muxer.c b/src/muxer.c index ac69fe92..e252a53b 100644 --- a/src/muxer.c +++ b/src/muxer.c @@ -21,9 +21,11 @@ #include "tvheadend.h" #include "service.h" #include "muxer.h" -#include "muxer_tvh.h" -#include "muxer_pass.h" - +#include "muxer/muxer_tvh.h" +#include "muxer/muxer_pass.h" +#if CONFIG_LIBAV +#include "muxer/muxer_libav.h" +#endif /** * Mime type for containers containing only audio @@ -34,6 +36,7 @@ static struct strtab container_audio_mime[] = { { "audio/x-mpegts", MC_MPEGTS }, { "audio/mpeg", MC_MPEGPS }, { "application/octet-stream", MC_PASS }, + { "application/octet-stream", MC_RAW }, }; @@ -46,6 +49,7 @@ static struct strtab container_video_mime[] = { { "video/x-mpegts", MC_MPEGTS }, { "video/mpeg", MC_MPEGPS }, { "application/octet-stream", MC_PASS }, + { "application/octet-stream", MC_RAW }, }; @@ -58,6 +62,7 @@ static struct strtab container_name[] = { { "mpegts", MC_MPEGTS }, { "mpegps", MC_MPEGPS }, { "pass", MC_PASS }, + { "raw", MC_RAW }, }; @@ -70,6 +75,7 @@ static struct strtab container_audio_file_suffix[] = { { "ts", MC_MPEGTS }, { "mpeg", MC_MPEGPS }, { "bin", MC_PASS }, + { "bin", MC_RAW }, }; @@ -82,6 +88,7 @@ static struct strtab container_video_file_suffix[] = { { "ts", MC_MPEGTS }, { "mpeg", MC_MPEGPS }, { "bin", MC_PASS }, + { "bin", MC_RAW }, }; @@ -89,7 +96,7 @@ static struct strtab container_video_file_suffix[] = { * Get the mime type for a container */ const char* -muxer_container_mimetype(muxer_container_type_t mc, int video) +muxer_container_type2mime(muxer_container_type_t mc, int video) { const char *str; @@ -141,7 +148,46 @@ muxer_container_type2txt(muxer_container_type_t mc) /** - * Convert a string to a container type + * Get a list of supported containers + */ +int +muxer_container_list(htsmsg_t *array) +{ + htsmsg_t *mc; + int c = 0; + + mc = htsmsg_create_map(); + htsmsg_add_str(mc, "name", muxer_container_type2txt(MC_MATROSKA)); + htsmsg_add_str(mc, "description", "Matroska"); + htsmsg_add_msg(array, NULL, mc); + c++; + + mc = htsmsg_create_map(); + htsmsg_add_str(mc, "name", muxer_container_type2txt(MC_PASS)); + htsmsg_add_str(mc, "description", "Same as source (pass through)"); + htsmsg_add_msg(array, NULL, mc); + c++; + +#if ENABLE_LIBAV + mc = htsmsg_create_map(); + htsmsg_add_str(mc, "name", muxer_container_type2txt(MC_MPEGTS)); + htsmsg_add_str(mc, "description", "MPEG-TS"); + htsmsg_add_msg(array, NULL, mc); + c++; + + mc = htsmsg_create_map(); + htsmsg_add_str(mc, "name", muxer_container_type2txt(MC_MPEGPS)); + htsmsg_add_str(mc, "description", "MPEG-PS (DVD)"); + htsmsg_add_msg(array, NULL, mc); + c++; +#endif + + return c; +} + + +/** + * Convert a container name to a container type */ muxer_container_type_t muxer_container_txt2type(const char *str) @@ -159,19 +205,46 @@ muxer_container_txt2type(const char *str) } +/** + * Convert a mime-string to a container type + */ +muxer_container_type_t +muxer_container_mime2type(const char *str) +{ + muxer_container_type_t mc; + + if(!str) + return MC_UNKNOWN; + + mc = str2val(str, container_video_mime); + if(mc == -1) + mc = str2val(str, container_audio_mime); + + if(mc == -1) + return MC_UNKNOWN; + + return mc; +} + + /** * Create a new muxer */ muxer_t* -muxer_create(service_t *s, muxer_container_type_t mc) +muxer_create(muxer_container_type_t mc) { muxer_t *m; - m = pass_muxer_create(s, mc); + m = pass_muxer_create(mc); if(!m) m = tvh_muxer_create(mc); +#if CONFIG_LIBAV + if(!m) + m = lav_muxer_create(mc); +#endif + if(!m) tvhlog(LOG_ERR, "mux", "Can't find a muxer that supports '%s' container", muxer_container_type2txt(mc)); @@ -200,6 +273,7 @@ const char* muxer_suffix(muxer_t *m, const struct streaming_start *ss) { const char *mime; + muxer_container_type_t mc; int video; if(!m || !ss) @@ -207,8 +281,9 @@ muxer_suffix(muxer_t *m, const struct streaming_start *ss) mime = m->m_mime(m, ss); video = memcmp("audio", mime, 5); + mc = muxer_container_mime2type(mime); - return muxer_container_suffix(m->m_container, video); + return muxer_container_suffix(mc, video); } @@ -225,6 +300,31 @@ muxer_init(muxer_t *m, const struct streaming_start *ss, const char *name) } +/** + * sanity wrapper arround m_reconfigure() + */ +int +muxer_reconfigure(muxer_t *m, const struct streaming_start *ss) +{ + if(!m || !ss) + return -1; + + return m->m_reconfigure(m, ss); +} + + +/** + * sanity wrapper arround m_add_marker() + */ +int +muxer_add_marker(muxer_t *m) +{ + if(!m) + return -1; + + return m->m_add_marker(m); +} + /** * sanity wrapper arround m_open_file() */ @@ -295,12 +395,12 @@ muxer_write_meta(muxer_t *m, struct epg_broadcast *eb) * sanity wrapper arround m_write_pkt() */ int -muxer_write_pkt(muxer_t *m, void *data) +muxer_write_pkt(muxer_t *m, streaming_message_type_t smt, void *data) { if(!m || !data) return -1; - return m->m_write_pkt(m, data); + return m->m_write_pkt(m, smt, data); } diff --git a/src/muxer.h b/src/muxer.h index 1f167192..1cfc947a 100644 --- a/src/muxer.h +++ b/src/muxer.h @@ -19,12 +19,15 @@ #ifndef MUXER_H_ #define MUXER_H_ +#include "htsmsg.h" + typedef enum { MC_UNKNOWN = 0, MC_MATROSKA = 1, MC_MPEGTS = 2, MC_MPEGPS = 3, MC_PASS = 4, + MC_RAW = 5, } muxer_container_type_t; @@ -42,33 +45,45 @@ typedef struct muxer { int (*m_init) (struct muxer *, // Init The muxer with streams const struct streaming_start *, const char *); + int (*m_reconfigure)(struct muxer *, // Reconfigure the muxer on + const struct streaming_start *); // stream changes int (*m_close) (struct muxer *); // Close the muxer void (*m_destroy) (struct muxer *); // Free the memory int (*m_write_meta) (struct muxer *, struct epg_broadcast *); // Append epg data - int (*m_write_pkt) (struct muxer *, void *); // Append a media packet + int (*m_write_pkt) (struct muxer *, // Append a media packet + streaming_message_type_t, + void *); + int (*m_add_marker) (struct muxer *); // Add a marker (or chapter) int m_errors; // Number of errors muxer_container_type_t m_container; // The type of the container } muxer_t; -// type <==> txt converters -const char * muxer_container_type2txt(muxer_container_type_t mc); -muxer_container_type_t muxer_container_txt2type(const char *str); -const char* muxer_container_mimetype(muxer_container_type_t mc, int video); -const char* muxer_container_suffix (muxer_container_type_t mc, int video); +// type <==> string converters +const char * muxer_container_type2txt (muxer_container_type_t mc); +const char* muxer_container_type2mime (muxer_container_type_t mc, int video); + +muxer_container_type_t muxer_container_txt2type (const char *str); +muxer_container_type_t muxer_container_mime2type (const char *str); + +const char* muxer_container_suffix(muxer_container_type_t mc, int video); + +int muxer_container_list(htsmsg_t *array); // Muxer factory -muxer_t *muxer_create(struct service *s, muxer_container_type_t mc); +muxer_t *muxer_create(muxer_container_type_t mc); // Wrapper functions int muxer_open_file (muxer_t *m, const char *filename); int muxer_open_stream (muxer_t *m, int fd); int muxer_init (muxer_t *m, const struct streaming_start *ss, const char *name); +int muxer_reconfigure (muxer_t *m, const struct streaming_start *ss); +int muxer_add_marker (muxer_t *m); int muxer_close (muxer_t *m); int muxer_destroy (muxer_t *m); int muxer_write_meta (muxer_t *m, struct epg_broadcast *eb); -int muxer_write_pkt (muxer_t *m, void *data); +int muxer_write_pkt (muxer_t *m, streaming_message_type_t smt, void *data); const char* muxer_mime (muxer_t *m, const struct streaming_start *ss); const char* muxer_suffix (muxer_t *m, const struct streaming_start *ss); diff --git a/src/muxer/muxer_libav.c b/src/muxer/muxer_libav.c new file mode 100644 index 00000000..c8c12470 --- /dev/null +++ b/src/muxer/muxer_libav.c @@ -0,0 +1,536 @@ +/* + * tvheadend, libavformat based muxer + * Copyright (C) 2012 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 "tvheadend.h" +#include "streaming.h" +#include "epg.h" +#include "channels.h" +#include "libav.h" +#include "muxer_libav.h" + +typedef struct lav_muxer { + muxer_t; + AVFormatContext *lm_oc; + AVBitStreamFilterContext *lm_h264_filter; + int lm_fd; + int lm_init; +} lav_muxer_t; + +#define MUX_BUF_SIZE 4096 + +static const AVRational mpeg_tc = {1, 90000}; + + +/** + * Callback function for libavformat + */ +static int +lav_muxer_write(void *opaque, uint8_t *buf, int buf_size) +{ + int r; + lav_muxer_t *lm = (lav_muxer_t*)opaque; + + r = write(lm->lm_fd, buf, buf_size); + lm->m_errors += (r != buf_size); + + return r; +} + + +/** + * Add a stream to the muxer + */ +static int +lav_muxer_add_stream(lav_muxer_t *lm, + const streaming_start_component_t *ssc) +{ + AVStream *st; + AVCodecContext *c; + + st = avformat_new_stream(lm->lm_oc, NULL); + if (!st) + return -1; + + st->id = ssc->ssc_index; + c = st->codec; + c->codec_id = streaming_component_type2codec_id(ssc->ssc_type); + + switch(lm->m_container) { + case MC_MATROSKA: + st->time_base.num = 1000000; + st->time_base.den = 1; + break; + + case MC_MPEGPS: + c->rc_buffer_size = 224*1024*8; + //Fall-through + case MC_MPEGTS: + st->time_base.num = 90000; + st->time_base.den = 1; + break; + + default: + st->time_base = AV_TIME_BASE_Q; + break; + } + + + + if(ssc->ssc_gh) { + c->extradata_size = pktbuf_len(ssc->ssc_gh); + c->extradata = av_malloc(c->extradata_size); + memcpy(c->extradata, pktbuf_ptr(ssc->ssc_gh), + pktbuf_len(ssc->ssc_gh)); + } + + if(SCT_ISAUDIO(ssc->ssc_type)) { + c->codec_type = AVMEDIA_TYPE_AUDIO; + c->sample_fmt = AV_SAMPLE_FMT_S16; + + c->sample_rate = sri_to_rate(ssc->ssc_sri); + c->channels = ssc->ssc_channels; + + c->time_base.num = 1; + c->time_base.den = c->sample_rate; + + av_dict_set(&st->metadata, "language", ssc->ssc_lang, 0); + + } else if(SCT_ISVIDEO(ssc->ssc_type)) { + c->codec_type = AVMEDIA_TYPE_VIDEO; + c->width = ssc->ssc_width; + c->height = ssc->ssc_height; + + c->time_base.num = 1; + c->time_base.den = 25; + + c->sample_aspect_ratio.num = ssc->ssc_aspect_num; + c->sample_aspect_ratio.den = ssc->ssc_aspect_den; + + st->sample_aspect_ratio.num = c->sample_aspect_ratio.num; + st->sample_aspect_ratio.den = c->sample_aspect_ratio.den; + + } else if(SCT_ISSUBTITLE(ssc->ssc_type)) { + c->codec_type = AVMEDIA_TYPE_SUBTITLE; + av_dict_set(&st->metadata, "language", ssc->ssc_lang, 0); + } + + if(lm->lm_oc->oformat->flags & AVFMT_GLOBALHEADER) + c->flags |= CODEC_FLAG_GLOBAL_HEADER; + + return 0; +} + + +/** + * Check if a container supports a given streaming component + */ +static int +lav_muxer_support_stream(muxer_container_type_t mc, + streaming_component_type_t type) +{ + int ret = 0; + + switch(mc) { + case MC_MATROSKA: + ret |= SCT_ISAUDIO(type); + ret |= SCT_ISVIDEO(type); + ret |= SCT_ISSUBTITLE(type); + break; + + case MC_MPEGTS: + ret |= (type == SCT_MPEG2VIDEO); + ret |= (type == SCT_H264); + + ret |= (type == SCT_MPEG2AUDIO); + ret |= (type == SCT_AC3); + ret |= (type == SCT_AAC); + ret |= (type == SCT_MP4A); + ret |= (type == SCT_EAC3); + + //Some pids lack pts, disable for now + //ret |= (type == SCT_TELETEXT); + ret |= (type == SCT_DVBSUB); + break; + + case MC_MPEGPS: + ret |= (type == SCT_MPEG2VIDEO); + ret |= (type == SCT_MPEG2AUDIO); + ret |= (type == SCT_AC3); + + default: + break; + } + + return ret; +} + + +/** + * Figure out the mime-type for the muxed data stream + */ +static const char* +lav_muxer_mime(muxer_t* m, const struct streaming_start *ss) +{ + int i; + int has_audio; + int has_video; + const streaming_start_component_t *ssc; + + has_audio = 0; + has_video = 0; + + for(i=0; i < ss->ss_num_components; i++) { + ssc = &ss->ss_components[i]; + + if(ssc->ssc_disabled) + continue; + + if(!lav_muxer_support_stream(m->m_container, ssc->ssc_type)) + continue; + + has_video |= SCT_ISVIDEO(ssc->ssc_type); + has_audio |= SCT_ISAUDIO(ssc->ssc_type); + } + + if(has_video) + return muxer_container_type2mime(m->m_container, 1); + else if(has_audio) + return muxer_container_type2mime(m->m_container, 0); + else + return muxer_container_type2mime(MC_UNKNOWN, 0); +} + + +/** + * Init the muxer with streams + */ +static int +lav_muxer_init(muxer_t* m, const struct streaming_start *ss, const char *name) +{ + int i; + const streaming_start_component_t *ssc; + AVFormatContext *oc; + lav_muxer_t *lm = (lav_muxer_t*)m; + char app[128]; + + snprintf(app, sizeof(app), "Tvheadend %s", tvheadend_version); + + oc = lm->lm_oc; + + av_dict_set(&oc->metadata, "title", name, 0); + av_dict_set(&oc->metadata, "service_name", name, 0); + av_dict_set(&oc->metadata, "service_provider", app, 0); + + if(lm->m_container == MC_MPEGTS) + lm->lm_h264_filter = av_bitstream_filter_init("h264_mp4toannexb"); + + oc->max_delay = 0.7 * AV_TIME_BASE; + + for(i=0; i < ss->ss_num_components; i++) { + ssc = &ss->ss_components[i]; + + if(ssc->ssc_disabled) + continue; + + if(!lav_muxer_support_stream(lm->m_container, ssc->ssc_type)) { + tvhlog(LOG_WARNING, "libav", "%s is not supported in %s", + streaming_component_type2txt(ssc->ssc_type), + muxer_container_type2txt(lm->m_container)); + continue; + } + + if(lav_muxer_add_stream(lm, ssc)) { + tvhlog(LOG_ERR, "libav", "Failed to add %s stream", + streaming_component_type2txt(ssc->ssc_type)); + continue; + } + } + + if(!lm->lm_oc->nb_streams) { + tvhlog(LOG_ERR, "libav", "No supported streams available"); + lm->m_errors++; + return -1; + } else if(avformat_write_header(lm->lm_oc, NULL) < 0) { + tvhlog(LOG_ERR, "libav", "Failed to write %s header", + muxer_container_type2txt(lm->m_container)); + lm->m_errors++; + return -1; + } + + lm->lm_init = 1; + + return 0; +} + + +/** + * Handle changes to the streams (usually PMT updates) + */ +static int +lav_muxer_reconfigure(muxer_t* m, const struct streaming_start *ss) +{ + lav_muxer_t *lm = (lav_muxer_t*)m; + + lm->m_errors++; + + return -1; +} + + +/** + * Open the muxer and write the header + */ +static int +lav_muxer_open_stream(muxer_t *m, int fd) +{ + uint8_t *buf; + AVIOContext *pb; + lav_muxer_t *lm = (lav_muxer_t*)m; + + buf = av_malloc(MUX_BUF_SIZE); + pb = avio_alloc_context(buf, MUX_BUF_SIZE, 1, lm, NULL, + lav_muxer_write, NULL); + pb->seekable = 0; + lm->lm_oc->pb = pb; + lm->lm_fd = fd; + + return 0; +} + + +static int +lav_muxer_open_file(muxer_t *m, const char *filename) +{ + AVFormatContext *oc; + lav_muxer_t *lm = (lav_muxer_t*)m; + + oc = lm->lm_oc; + snprintf(oc->filename, sizeof(oc->filename), "%s", filename); + + if(avio_open(&oc->pb, filename, AVIO_FLAG_WRITE) < 0) { + tvhlog(LOG_ERR, "libav", "Could not open %s", filename); + lm->m_errors++; + return -1; + } + + return 0; +} + + +/** + * Write a packet to the muxer + */ +static int +lav_muxer_write_pkt(muxer_t *m, streaming_message_type_t smt, void *data) +{ + int i; + AVFormatContext *oc; + AVStream *st; + AVPacket packet; + th_pkt_t *pkt = (th_pkt_t*)data; + lav_muxer_t *lm = (lav_muxer_t*)m; + int rc = 0; + + assert(smt == SMT_PACKET); + + oc = lm->lm_oc; + + if(!oc->nb_streams) { + tvhlog(LOG_ERR, "libav", "No streams to mux"); + rc = -1; + goto ret; + } + + if(!lm->lm_init) { + tvhlog(LOG_ERR, "libav", "Muxer not initialized correctly"); + rc = -1; + goto ret; + } + + for(i=0; inb_streams; i++) { + st = oc->streams[i]; + + if(st->id != pkt->pkt_componentindex) + continue; + + av_init_packet(&packet); + + if(st->codec->codec_id == CODEC_ID_MPEG2VIDEO) + pkt = pkt_merge_header(pkt); + + if(lm->lm_h264_filter && st->codec->codec_id == CODEC_ID_H264) { + if(av_bitstream_filter_filter(lm->lm_h264_filter, + st->codec, + NULL, + &packet.data, + &packet.size, + pktbuf_ptr(pkt->pkt_payload), + pktbuf_len(pkt->pkt_payload), + pkt->pkt_frametype < PKT_P_FRAME) < 0) { + tvhlog(LOG_WARNING, "libav", "Failed to filter bitstream"); + break; + } + } else { + packet.data = pktbuf_ptr(pkt->pkt_payload); + packet.size = pktbuf_len(pkt->pkt_payload); + } + + packet.stream_index = st->index; + + packet.pts = av_rescale_q(pkt->pkt_pts , mpeg_tc, st->time_base); + packet.dts = av_rescale_q(pkt->pkt_dts , mpeg_tc, st->time_base); + packet.duration = av_rescale_q(pkt->pkt_duration, mpeg_tc, st->time_base); + + if(pkt->pkt_frametype < PKT_P_FRAME) + packet.flags |= AV_PKT_FLAG_KEY; + + if((rc = av_interleaved_write_frame(oc, &packet))) + tvhlog(LOG_WARNING, "libav", "Failed to write frame"); + + // h264_mp4toannexb filter might allocate new data. + if(packet.data != pktbuf_ptr(pkt->pkt_payload)) + av_free(packet.data); + + break; + } + + ret: + lm->m_errors += (rc != 0); + pkt_ref_dec(pkt); + + return rc; +} + + +/** + * NOP + */ +static int +lav_muxer_write_meta(muxer_t *m, struct epg_broadcast *eb) +{ + return 0; +} + + +/** + * NOP + */ +static int +lav_muxer_add_marker(muxer_t* m) +{ + return 0; +} + + +/** + * Close the muxer and append trailer to output + */ +static int +lav_muxer_close(muxer_t *m) +{ + int i; + int ret = 0; + lav_muxer_t *lm = (lav_muxer_t*)m; + + if(lm->lm_init && av_write_trailer(lm->lm_oc) < 0) { + tvhlog(LOG_WARNING, "libav", "Failed to write %s trailer", + muxer_container_type2txt(lm->m_container)); + lm->m_errors++; + ret = -1; + } + + if(lm->lm_h264_filter) + av_bitstream_filter_close(lm->lm_h264_filter); + + for(i=0; ilm_oc->nb_streams; i++) + av_freep(&lm->lm_oc->streams[i]->codec->extradata); + + lm->lm_oc->nb_streams = 0; + + return ret; +} + + +/** + * Free all memory associated with the muxer + */ +static void +lav_muxer_destroy(muxer_t *m) +{ + lav_muxer_t *lm = (lav_muxer_t*)m; + + if(lm->lm_oc && lm->lm_oc->pb) + av_free(lm->lm_oc->pb); + + if(lm->lm_oc) + av_free(lm->lm_oc); + + free(lm); +} + + +/** + * Create a new libavformat based muxer + */ +muxer_t* +lav_muxer_create(muxer_container_type_t mc) +{ + const char *mux_name; + lav_muxer_t *lm; + AVOutputFormat *fmt; + + switch(mc) { + case MC_MPEGPS: + mux_name = "dvd"; + break; + default: + mux_name = muxer_container_type2txt(mc); + break; + } + + fmt = av_guess_format(mux_name, NULL, NULL); + if(!fmt) { + tvhlog(LOG_ERR, "libav", "Can't find the '%s' muxer", mux_name); + return NULL; + } + + lm = calloc(1, sizeof(lav_muxer_t)); + lm->m_open_stream = lav_muxer_open_stream; + lm->m_open_file = lav_muxer_open_file; + lm->m_init = lav_muxer_init; + lm->m_reconfigure = lav_muxer_reconfigure; + lm->m_mime = lav_muxer_mime; + lm->m_add_marker = lav_muxer_add_marker; + lm->m_write_meta = lav_muxer_write_meta; + lm->m_write_pkt = lav_muxer_write_pkt; + lm->m_close = lav_muxer_close; + lm->m_destroy = lav_muxer_destroy; + lm->m_container = mc; + lm->lm_oc = avformat_alloc_context(); + lm->lm_oc->oformat = fmt; + lm->lm_fd = -1; + lm->lm_init = 0; + + return (muxer_t*)lm; +} + diff --git a/src/iptv_output.h b/src/muxer/muxer_libav.h similarity index 67% rename from src/iptv_output.h rename to src/muxer/muxer_libav.h index 91895327..a8325b06 100644 --- a/src/iptv_output.h +++ b/src/muxer/muxer_libav.h @@ -1,6 +1,6 @@ /* - * Output functions for fixed multicast streaming - * Copyright (C) 2007 Andreas Öman + * tvheadend, muxing of packets with libavformat + * Copyright (C) 2012 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 @@ -13,12 +13,14 @@ * 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 . + * along with this program. If not, see . */ -#ifndef IPTV_OUTPUT_H_ -#define IPTV_OUTPUT_H_ +#ifndef LAV_MUXER_H_ +#define LAV_MUXER_H_ -void output_multicast_setup(void); +#include "muxer.h" -#endif /* IPTV_OUTPUT_H_ */ +muxer_t* lav_muxer_create(muxer_container_type_t mc); + +#endif diff --git a/src/muxer_pass.c b/src/muxer/muxer_pass.c similarity index 73% rename from src/muxer_pass.c rename to src/muxer/muxer_pass.c index e5866f92..97e9b156 100644 --- a/src/muxer_pass.c +++ b/src/muxer/muxer_pass.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "tvheadend.h" #include "streaming.h" @@ -48,9 +49,10 @@ typedef struct pass_muxer { char *pm_filename; /* TS muxing */ + uint8_t pm_injection; uint8_t *pm_pat; uint8_t *pm_pmt; - uint16_t pm_pmt_pid; + uint16_t pm_pmt_version; uint32_t pm_ic; // Injection counter uint32_t pm_pc; // Packet counter } pass_muxer_t; @@ -65,8 +67,10 @@ pass_muxer_mime(muxer_t* m, const struct streaming_start *ss) int i; int has_audio; int has_video; + muxer_container_type_t mc; const streaming_start_component_t *ssc; - + const source_info_t *si = &ss->ss_si; + has_audio = 0; has_video = 0; @@ -80,12 +84,62 @@ pass_muxer_mime(muxer_t* m, const struct streaming_start *ss) has_audio |= SCT_ISAUDIO(ssc->ssc_type); } - if(has_video) - return muxer_container_mimetype(m->m_container, 1); - else if(has_audio) - return muxer_container_mimetype(m->m_container, 0); + if(si->si_type == S_MPEG_TS) + mc = MC_MPEGTS; + else if(si->si_type == S_MPEG_PS) + mc = MC_MPEGPS; else - return muxer_container_mimetype(MC_UNKNOWN, 0); + mc = MC_UNKNOWN; + + if(has_video) + return muxer_container_type2mime(mc, 1); + else if(has_audio) + return muxer_container_type2mime(mc, 0); + else + return muxer_container_type2mime(MC_UNKNOWN, 0); +} + + +/** + * Generate the pmt and pat from a streaming start message + */ +static int +pass_muxer_reconfigure(muxer_t* m, const struct streaming_start *ss) +{ + pass_muxer_t *pm = (pass_muxer_t*)m; + const source_info_t *si = &ss->ss_si; + + if(si->si_type == S_MPEG_TS && ss->ss_pmt_pid) { + pm->pm_pat = realloc(pm->pm_pat, 188); + memset(pm->pm_pat, 0xff, 188); + pm->pm_pat[0] = 0x47; + pm->pm_pat[1] = 0x40; + pm->pm_pat[2] = 0x00; + pm->pm_pat[3] = 0x10; + pm->pm_pat[4] = 0x00; + if(psi_build_pat(NULL, pm->pm_pat+5, 183, ss->ss_pmt_pid) < 0) { + pm->m_errors++; + tvhlog(LOG_ERR, "pass", "%s: Unable to build pat", pm->pm_filename); + return -1; + } + + pm->pm_pmt = realloc(pm->pm_pmt, 188); + memset(pm->pm_pmt, 0xff, 188); + pm->pm_pmt[0] = 0x47; + pm->pm_pmt[1] = 0x40 | (ss->ss_pmt_pid >> 8); + pm->pm_pmt[2] = 0x00 | (ss->ss_pmt_pid >> 0); + pm->pm_pmt[3] = 0x10; + pm->pm_pmt[4] = 0x00; + if(psi_build_pmt(ss, pm->pm_pmt+5, 183, pm->pm_pmt_version, + ss->ss_pcr_pid) < 0) { + pm->m_errors++; + tvhlog(LOG_ERR, "pass", "%s: Unable to build pmt", pm->pm_filename); + return -1; + } + pm->pm_pmt_version++; + } + + return 0; } @@ -95,36 +149,7 @@ pass_muxer_mime(muxer_t* m, const struct streaming_start *ss) static int pass_muxer_init(muxer_t* m, const struct streaming_start *ss, const char *name) { - pass_muxer_t *pm = (pass_muxer_t*)m; - - if(pm->m_container == MC_MPEGTS) { - - memset(pm->pm_pat, 0xff, 188); - pm->pm_pat[0] = 0x47; - pm->pm_pat[1] = 0x40; - pm->pm_pat[2] = 0x00; - pm->pm_pat[3] = 0x10; - pm->pm_pat[4] = 0x00; - if(psi_build_pat(NULL, pm->pm_pat+5, 183, pm->pm_pmt_pid) < 0) { - pm->m_errors++; - tvhlog(LOG_ERR, "pass", "%s: Unable to build pat", pm->pm_filename); - return -1; - } - - memset(pm->pm_pmt, 0xff, 188); - pm->pm_pmt[0] = 0x47; - pm->pm_pmt[1] = 0x40 | (pm->pm_pmt_pid >> 8); - pm->pm_pmt[2] = 0x00 | (pm->pm_pmt_pid >> 0); - pm->pm_pmt[3] = 0x10; - pm->pm_pmt[4] = 0x00; - if(psi_build_pmt(ss, pm->pm_pmt+5, 183, ss->ss_pcr_pid) < 0) { - pm->m_errors++; - tvhlog(LOG_ERR, "pass", "%s: Unable to build pmt", pm->pm_filename); - return -1; - } - } - - return 0; + return pass_muxer_reconfigure(m, ss); } @@ -170,16 +195,16 @@ pass_muxer_open_file(muxer_t *m, const char *filename) /** - * Write TS packets to the file descriptor + * Write data to the file descriptor */ static void -pass_muxer_write(muxer_t *m, const void *ts, size_t len) +pass_muxer_write(muxer_t *m, const void *data, size_t size) { pass_muxer_t *pm = (pass_muxer_t*)m; if(pm->pm_error) { pm->m_errors++; - } else if(write(pm->pm_fd, ts, len) != len) { + } else if(tvh_write(pm->pm_fd, data, size)) { pm->pm_error = errno; tvhlog(LOG_ERR, "pass", "%s: Write failed -- %s", pm->pm_filename, strerror(errno)); @@ -197,14 +222,18 @@ pass_muxer_write_ts(muxer_t *m, pktbuf_t *pb) pass_muxer_t *pm = (pass_muxer_t*)m; int rem; - // Inject pmt and pat into the stream - rem = pm->pm_pc % TS_INJECTION_RATE; - if(!rem) { - pm->pm_pat[3] = (pm->pm_pat[3] & 0xf0) | (pm->pm_ic & 0x0f); - pm->pm_pmt[3] = (pm->pm_pat[3] & 0xf0) | (pm->pm_ic & 0x0f); - pass_muxer_write(m, pm->pm_pmt, 188); - pass_muxer_write(m, pm->pm_pat, 188); - pm->pm_ic++; + if(pm->pm_pat != NULL && + pm->pm_pmt != NULL && + pm->pm_injection) { + // Inject pmt and pat into the stream + rem = pm->pm_pc % TS_INJECTION_RATE; + if(!rem) { + pm->pm_pat[3] = (pm->pm_pat[3] & 0xf0) | (pm->pm_ic & 0x0f); + pm->pm_pmt[3] = (pm->pm_pmt[3] & 0xf0) | (pm->pm_ic & 0x0f); + pass_muxer_write(m, pm->pm_pat, 188); + pass_muxer_write(m, pm->pm_pmt, 188); + pm->pm_ic++; + } } pass_muxer_write(m, pb->pb_data, pb->pb_size); @@ -217,22 +246,23 @@ pass_muxer_write_ts(muxer_t *m, pktbuf_t *pb) * Write a packet directly to the file descriptor */ static int -pass_muxer_write_pkt(muxer_t *m, void *data) +pass_muxer_write_pkt(muxer_t *m, streaming_message_type_t smt, void *data) { pktbuf_t *pb = (pktbuf_t*)data; pass_muxer_t *pm = (pass_muxer_t*)m; - switch(pm->m_container) { - case MC_MPEGTS: + assert(smt == SMT_MPEGTS); + + switch(smt) { + case SMT_MPEGTS: pass_muxer_write_ts(m, pb); break; default: - //NOP + //TODO: add support for v4l (MPEG-PS) break; } - if(!pm->pm_error) - pktbuf_ref_dec(pb); + pktbuf_ref_dec(pb); return pm->pm_error; } @@ -293,31 +323,25 @@ pass_muxer_destroy(muxer_t *m) * Create a new passthrough muxer */ muxer_t* -pass_muxer_create(service_t *s, muxer_container_type_t mc) +pass_muxer_create(muxer_container_type_t mc) { pass_muxer_t *pm; - if(mc != MC_PASS) + if(mc != MC_PASS && mc != MC_RAW) return NULL; pm = calloc(1, sizeof(pass_muxer_t)); pm->m_open_stream = pass_muxer_open_stream; pm->m_open_file = pass_muxer_open_file; pm->m_init = pass_muxer_init; + pm->m_reconfigure = pass_muxer_reconfigure; pm->m_mime = pass_muxer_mime; pm->m_write_meta = pass_muxer_write_meta; pm->m_write_pkt = pass_muxer_write_pkt; pm->m_close = pass_muxer_close; pm->m_destroy = pass_muxer_destroy; - - if(s->s_type == SERVICE_TYPE_V4L) { - pm->m_container = MC_MPEGPS; - } else { - pm->m_container = MC_MPEGTS; - pm->pm_pmt_pid = s->s_pmt_pid; - pm->pm_pat = malloc(188); - pm->pm_pmt = malloc(188); - } + pm->pm_fd = -1; + pm->pm_injection = (mc == MC_PASS); return (muxer_t *)pm; } diff --git a/src/muxer_pass.h b/src/muxer/muxer_pass.h similarity index 91% rename from src/muxer_pass.h rename to src/muxer/muxer_pass.h index b3dc9c88..919c2104 100644 --- a/src/muxer_pass.h +++ b/src/muxer/muxer_pass.h @@ -21,6 +21,6 @@ #include "muxer.h" -muxer_t* pass_muxer_create(struct service *s, muxer_container_type_t mc); +muxer_t* pass_muxer_create(muxer_container_type_t mc); #endif diff --git a/src/muxer_tvh.c b/src/muxer/muxer_tvh.c similarity index 79% rename from src/muxer_tvh.c rename to src/muxer/muxer_tvh.c index 7708b206..6d46b912 100644 --- a/src/muxer_tvh.c +++ b/src/muxer/muxer_tvh.c @@ -16,12 +16,14 @@ * along with this program. If not, see . */ +#include + #include "tvheadend.h" #include "streaming.h" #include "epg.h" #include "channels.h" -#include "dvr/mkmux.h" #include "muxer_tvh.h" +#include "tvh/mkmux.h" typedef struct tvh_muxer { muxer_t; @@ -54,11 +56,11 @@ tvh_muxer_mime(muxer_t* m, const struct streaming_start *ss) } if(has_video) - return muxer_container_mimetype(m->m_container, 1); + return muxer_container_type2mime(m->m_container, 1); else if(has_audio) - return muxer_container_mimetype(m->m_container, 0); + return muxer_container_type2mime(m->m_container, 0); else - return muxer_container_mimetype(MC_UNKNOWN, 0); + return muxer_container_type2mime(MC_UNKNOWN, 0); } @@ -79,6 +81,38 @@ tvh_muxer_init(muxer_t* m, const struct streaming_start *ss, const char *name) } +/** + * Insert a new chapter at the current location + */ +static int +tvh_muxer_add_marker(muxer_t* m) +{ + tvh_muxer_t *tm = (tvh_muxer_t*)m; + + if(mk_mux_insert_chapter(tm->tm_ref)) { + tm->m_errors++; + return -1; + } + + return 0; +} + + +/** + * Multisegment matroska files do exist but I am not sure if they are supported + * by many media players. For now, we'll treat it as an error. + */ +static int +tvh_muxer_reconfigure(muxer_t* m, const struct streaming_start *ss) +{ + tvh_muxer_t *tm = (tvh_muxer_t*)m; + + tm->m_errors++; + + return -1; +} + + /** * Open the muxer as a stream muxer (using a non-seekable socket) */ @@ -117,11 +151,13 @@ tvh_muxer_open_file(muxer_t *m, const char *filename) * Write a packet to the muxer */ static int -tvh_muxer_write_pkt(muxer_t *m, void *data) +tvh_muxer_write_pkt(muxer_t *m, streaming_message_type_t smt, void *data) { th_pkt_t *pkt = (th_pkt_t*)data; tvh_muxer_t *tm = (tvh_muxer_t*)m; + assert(smt == SMT_PACKET); + if(mk_mux_write_pkt(tm->tm_ref, pkt)) { tm->m_errors++; return -1; @@ -196,6 +232,8 @@ tvh_muxer_create(muxer_container_type_t mc) tm->m_open_file = tvh_muxer_open_file; tm->m_mime = tvh_muxer_mime; tm->m_init = tvh_muxer_init; + tm->m_reconfigure = tvh_muxer_reconfigure; + tm->m_add_marker = tvh_muxer_add_marker; tm->m_write_meta = tvh_muxer_write_meta; tm->m_write_pkt = tvh_muxer_write_pkt; tm->m_close = tvh_muxer_close; diff --git a/src/muxer_tvh.h b/src/muxer/muxer_tvh.h similarity index 100% rename from src/muxer_tvh.h rename to src/muxer/muxer_tvh.h diff --git a/src/dvr/ebml.c b/src/muxer/tvh/ebml.c similarity index 100% rename from src/dvr/ebml.c rename to src/muxer/tvh/ebml.c diff --git a/src/dvr/ebml.h b/src/muxer/tvh/ebml.h similarity index 100% rename from src/dvr/ebml.h rename to src/muxer/tvh/ebml.h diff --git a/src/dvr/mkmux.c b/src/muxer/tvh/mkmux.c similarity index 82% rename from src/dvr/mkmux.c rename to src/muxer/tvh/mkmux.c index 93fa24fc..19880a38 100644 --- a/src/dvr/mkmux.c +++ b/src/muxer/tvh/mkmux.c @@ -27,13 +27,14 @@ #include "tvheadend.h" #include "streaming.h" -#include "dvr.h" +#include "dvr/dvr.h" #include "mkmux.h" #include "ebml.h" extern int dvr_iov_max; TAILQ_HEAD(mk_cue_queue, mk_cue); +TAILQ_HEAD(mk_chapter_queue, mk_chapter); #define MATROSKA_TIMESCALE 1000000 // in nS @@ -49,17 +50,34 @@ typedef struct mk_track { int tracknum; int disabled; int64_t nextpts; -} mk_track; + + uint8_t channels; + uint8_t sri; + + uint16_t aspect_num; + uint16_t aspect_den; + + uint8_t commercial; +} mk_track_t; /** * */ -struct mk_cue { +typedef struct mk_cue { TAILQ_ENTRY(mk_cue) link; int64_t ts; int tracknum; off_t cluster_pos; -}; +} mk_cue_t; + +/** + * + */ +typedef struct mk_chapter { + TAILQ_ENTRY(mk_chapter) link; + int uuid; + int64_t ts; +} mk_chapter_t; /** * @@ -71,7 +89,7 @@ struct mk_mux { off_t fdpos; // Current position in file int seekable; - mk_track *tracks; + mk_track_t *tracks; int ntracks; int has_video; @@ -90,10 +108,12 @@ struct mk_mux { off_t trackinfo_pos; off_t metadata_pos; off_t cue_pos; + off_t chapters_pos; int addcue; struct mk_cue_queue cues; + struct mk_chapter_queue chapters; char uuid[16]; char *title; @@ -165,7 +185,7 @@ mk_build_segment_info(mk_mux_t *mkm) * */ static htsbuf_queue_t * -mk_build_tracks(mk_mux_t *mkm, const struct streaming_start *ss) +mk_build_tracks(mk_mux_t *mkm, const streaming_start_t *ss) { const streaming_start_component_t *ssc; const char *codec_id; @@ -174,7 +194,7 @@ mk_build_tracks(mk_mux_t *mkm, const struct streaming_start *ss) int tracknum = 0; uint8_t buf4[4]; - mkm->tracks = calloc(1, sizeof(mk_track) * ss->ss_num_components); + mkm->tracks = calloc(1, sizeof(mk_track_t) * ss->ss_num_components); mkm->ntracks = ss->ss_num_components; for(i = 0; i < ss->ss_num_components; i++) { @@ -186,7 +206,12 @@ mk_build_tracks(mk_mux_t *mkm, const struct streaming_start *ss) continue; mkm->tracks[i].index = ssc->ssc_index; - mkm->tracks[i].type = ssc->ssc_type; + mkm->tracks[i].type = ssc->ssc_type; + mkm->tracks[i].channels = ssc->ssc_channels; + mkm->tracks[i].aspect_num = ssc->ssc_aspect_num; + mkm->tracks[i].aspect_den = ssc->ssc_aspect_den; + mkm->tracks[i].commercial = COMMERCIAL_UNKNOWN; + mkm->tracks[i].sri = ssc->ssc_sri; mkm->tracks[i].nextpts = PTS_UNSET; switch(ssc->ssc_type) { @@ -412,6 +437,70 @@ mk_write_segment_header(mk_mux_t *mkm, int64_t size) } +/** + * + */ +static htsbuf_queue_t * +mk_build_one_chapter(mk_chapter_t *ch) +{ + htsbuf_queue_t *q = htsbuf_queue_alloc(0); + + ebml_append_uint(q, 0x73C4, ch->uuid); + ebml_append_uint(q, 0x91, ch->ts * MATROSKA_TIMESCALE); + ebml_append_uint(q, 0x98, 0); //ChapterFlagHidden + ebml_append_uint(q, 0x4598, 1); //ChapterFlagEnabled + + return q; +} + + +/** + * + */ +static htsbuf_queue_t * +mk_build_edition_entry(mk_mux_t *mkm) +{ + mk_chapter_t *ch; + htsbuf_queue_t *q = htsbuf_queue_alloc(0); + + ebml_append_uint(q, 0x45bd, 0); //EditionFlagHidden + ebml_append_uint(q, 0x45db, 1); //EditionFlagDefault + + TAILQ_FOREACH(ch, &mkm->chapters, link) { + ebml_append_master(q, 0xB6, mk_build_one_chapter(ch)); + } + + return q; +} + + +/** + * + */ +static htsbuf_queue_t * +mk_build_chapters(mk_mux_t *mkm) +{ + htsbuf_queue_t *q = htsbuf_queue_alloc(0); + + ebml_append_master(q, 0x45b9, mk_build_edition_entry(mkm)); + + return q; +} + +/** + * + */ +static void +mk_write_chapters(mk_mux_t *mkm) +{ + if(TAILQ_FIRST(&mkm->chapters) == NULL) + return; + + mkm->chapters_pos = mkm->fdpos; + mk_write_master(mkm, 0x1043a770, mk_build_chapters(mkm)); +} + + /** * */ @@ -581,6 +670,11 @@ mk_build_metaseek(mk_mux_t *mkm) ebml_append_master(q, 0x4dbb, mk_build_one_metaseek(mkm, 0x1c53bb6b, mkm->cue_pos)); + + if(mkm->chapters_pos) + ebml_append_master(q, 0x4dbb, + mk_build_one_metaseek(mkm, 0x1043a770, + mkm->chapters_pos)); return q; } @@ -623,7 +717,7 @@ mk_write_metaseek(mk_mux_t *mkm, int first) */ static htsbuf_queue_t * mk_build_segment(mk_mux_t *mkm, - const struct streaming_start *ss) + const streaming_start_t *ss) { htsbuf_queue_t *p = htsbuf_queue_alloc(0); @@ -649,7 +743,7 @@ mk_build_segment(mk_mux_t *mkm, static void addcue(mk_mux_t *mkm, int64_t pts, int tracknum) { - struct mk_cue *mc = malloc(sizeof(struct mk_cue)); + mk_cue_t *mc = malloc(sizeof(mk_cue_t)); mc->ts = pts; mc->tracknum = tracknum; mc->cluster_pos = mkm->cluster_pos; @@ -657,6 +751,36 @@ addcue(mk_mux_t *mkm, int64_t pts, int tracknum) } +/** + * + */ +static void +mk_add_chapter(mk_mux_t *mkm, int64_t ts) +{ + mk_chapter_t *ch; + int uuid; + + ch = TAILQ_LAST(&mkm->chapters, mk_chapter_queue); + if(ch) { + // don't add a new chapter if the previous one was + // added less than 10s ago + if(ts - ch->ts < 10000) + return; + + uuid = ch->uuid + 1; + } + else { + uuid = 1; + } + + ch = malloc(sizeof(mk_chapter_t)); + + ch->uuid = uuid; + ch->ts = ts; + + TAILQ_INSERT_TAIL(&mkm->chapters, ch, link); +} + /** * */ @@ -673,7 +797,7 @@ mk_close_cluster(mk_mux_t *mkm) * */ static void -mk_write_frame_i(mk_mux_t *mkm, mk_track *t, th_pkt_t *pkt) +mk_write_frame_i(mk_mux_t *mkm, mk_track_t *t, th_pkt_t *pkt) { int64_t pts = pkt->pkt_pts, delta, nxt; unsigned char c_delta_flags[3]; @@ -766,7 +890,7 @@ mk_write_frame_i(mk_mux_t *mkm, mk_track *t, th_pkt_t *pkt) static void mk_write_cues(mk_mux_t *mkm) { - struct mk_cue *mc; + mk_cue_t *mc; htsbuf_queue_t *q, *c, *p; if(TAILQ_FIRST(&mkm->cues) == NULL) @@ -800,7 +924,10 @@ mk_write_cues(mk_mux_t *mkm) */ mk_mux_t *mk_mux_create(void) { - mk_mux_t *mkm = calloc(1, sizeof(struct mk_mux)); + mk_mux_t *mkm = calloc(1, sizeof(mk_mux_t)); + + mkm->fd = -1; + return mkm; } @@ -848,7 +975,7 @@ mk_mux_open_file(mk_mux_t *mkm, const char *filename) * Init the muxer with a title and some tracks */ int -mk_mux_init(mk_mux_t *mkm, const char *title, const struct streaming_start *ss) +mk_mux_init(mk_mux_t *mkm, const char *title, const streaming_start_t *ss) { htsbuf_queue_t q; @@ -860,6 +987,7 @@ mk_mux_init(mk_mux_t *mkm, const char *title, const struct streaming_start *ss) mkm->title = strdup(mkm->filename); TAILQ_INIT(&mkm->cues); + TAILQ_INIT(&mkm->chapters); htsbuf_queue_init(&q, 0); @@ -873,6 +1001,8 @@ mk_mux_init(mk_mux_t *mkm, const char *title, const struct streaming_start *ss) mk_write_queue(mkm, &q); + htsbuf_queue_flush(&q); + return mkm->error; } @@ -881,10 +1011,10 @@ mk_mux_init(mk_mux_t *mkm, const char *title, const struct streaming_start *ss) * Append a packet to the muxer */ int -mk_mux_write_pkt(mk_mux_t *mkm, struct th_pkt *pkt) +mk_mux_write_pkt(mk_mux_t *mkm, th_pkt_t *pkt) { - int i; - mk_track *t = NULL; + int i, mark; + mk_track_t *t = NULL; for(i = 0; i < mkm->ntracks;i++) { if(mkm->tracks[i].index == pkt->pkt_componentindex && mkm->tracks[i].enabled) { @@ -893,14 +1023,47 @@ mk_mux_write_pkt(mk_mux_t *mkm, struct th_pkt *pkt) } } - if(t != NULL && !t->disabled) { - if(t->merge) - pkt = pkt_merge_header(pkt); - mk_write_frame_i(mkm, t, pkt); - } - - if(!mkm->error) + if(t == NULL || t->disabled) { pkt_ref_dec(pkt); + return mkm->error; + } + + mark = 0; + if(pkt->pkt_channels != t->channels && + pkt->pkt_channels) { + mark = 1; + t->channels = pkt->pkt_channels; + } + if(pkt->pkt_aspect_num != t->aspect_num && + pkt->pkt_aspect_num) { + mark = 1; + t->aspect_num = pkt->pkt_aspect_num; + } + if(pkt->pkt_aspect_den != t->aspect_den && + pkt->pkt_aspect_den) { + mark = 1; + t->aspect_den = pkt->pkt_aspect_den; + } + if(pkt->pkt_sri != t->sri && + pkt->pkt_sri) { + mark = 1; + t->sri = pkt->pkt_sri; + } + if(pkt->pkt_commercial != t->commercial && + pkt->pkt_commercial != COMMERCIAL_UNKNOWN) { + mark = 1; + t->commercial = pkt->pkt_commercial; + } + + if(mark) + mk_mux_insert_chapter(mkm); + + if(t->merge) + pkt = pkt_merge_header(pkt); + + mk_write_frame_i(mkm, t, pkt); + + pkt_ref_dec(pkt); return mkm->error; } @@ -926,6 +1089,19 @@ mk_mux_write_meta(mk_mux_t *mkm, const dvr_entry_t *de, } +/** + * Insert a new chapter at the current location + */ +int +mk_mux_insert_chapter(mk_mux_t *mkm) +{ + if(mkm->totduration != PTS_UNSET) + mk_add_chapter(mkm, mkm->totduration); + + return mkm->error; +} + + /** * Close the muxer */ @@ -935,6 +1111,7 @@ mk_mux_close(mk_mux_t *mkm) int64_t totsize; mk_close_cluster(mkm); mk_write_cues(mkm); + mk_write_chapters(mkm); mk_write_metaseek(mkm, 0); totsize = mkm->fdpos; @@ -975,6 +1152,13 @@ mk_mux_close(mk_mux_t *mkm) void mk_mux_destroy(mk_mux_t *mkm) { + mk_chapter_t *ch; + + while((ch = TAILQ_FIRST(&mkm->chapters)) != NULL) { + TAILQ_REMOVE(&mkm->chapters, ch, link); + free(ch); + } + free(mkm->filename); free(mkm->tracks); free(mkm->title); diff --git a/src/dvr/mkmux.h b/src/muxer/tvh/mkmux.h similarity index 97% rename from src/dvr/mkmux.h rename to src/muxer/tvh/mkmux.h index ddb4a866..8b44145d 100644 --- a/src/dvr/mkmux.h +++ b/src/muxer/tvh/mkmux.h @@ -40,6 +40,8 @@ int mk_mux_write_pkt (mk_mux_t *mkm, struct th_pkt *pkt); int mk_mux_write_meta(mk_mux_t *mkm, const struct dvr_entry *de, const struct epg_broadcast *eb); +int mk_mux_insert_chapter(mk_mux_t *mkm); + int mk_mux_close (mk_mux_t *mkm); void mk_mux_destroy(mk_mux_t *mkm); diff --git a/src/muxes.c b/src/muxes.c index 8629d264..6783fd85 100644 --- a/src/muxes.c +++ b/src/muxes.c @@ -289,7 +289,10 @@ static void _muxes_load_file str++; } } - if (!reg) return; + if (!reg) { + fb_close(fp); + return; + } /* Network */ str = buf; @@ -338,19 +341,22 @@ static void _muxes_load_file * * Note: should we follow symlinks? */ -static void _muxes_load_dir ( const char *path, const char *type ) +static void _muxes_load_dir + ( const char *path, const char *type, int lvl ) { char p[256]; fb_dir *dir; fb_dirent *de; + if (lvl >= 3) return; if (!(dir = fb_opendir(path))) return; + lvl++; while ((de = fb_readdir(dir))) { if (*de->name == '.') continue; if (de->type == FB_DIR) { snprintf(p, sizeof(p), "%s/%s", path, de->name); - _muxes_load_dir(p, de->name); + _muxes_load_dir(p, de->name, lvl+1); } else if (type) { _muxes_load_file(type, dir, de->name); } @@ -371,5 +377,5 @@ void muxes_init ( void ) #else path = "/usr/share/dvb"; #endif - _muxes_load_dir(path, NULL); + _muxes_load_dir(path, NULL, 0); } diff --git a/src/notify.c b/src/notify.c index bdad26ef..b2d8701a 100644 --- a/src/notify.c +++ b/src/notify.c @@ -33,3 +33,12 @@ notify_by_msg(const char *class, htsmsg_t *m) comet_mailbox_add_message(m, 0); htsmsg_destroy(m); } + + +void +notify_reload(const char *class) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_u32(m, "reload", 1); + notify_by_msg(class, m); +} diff --git a/src/notify.h b/src/notify.h index cf278393..1a41a43a 100644 --- a/src/notify.h +++ b/src/notify.h @@ -23,4 +23,6 @@ void notify_by_msg(const char *class, htsmsg_t *m); +void notify_reload(const char *class); + #endif /* NOTIFY_H_ */ diff --git a/src/packet.h b/src/packet.h index 836c2d55..2aecce7b 100644 --- a/src/packet.h +++ b/src/packet.h @@ -36,6 +36,14 @@ typedef struct pktbuf { #define PKT_B_FRAME 3 #define PKT_NTYPES 4 +static inline char pkt_frametype_to_char ( int frametype ) +{ + if (frametype == PKT_I_FRAME) return 'I'; + if (frametype == PKT_P_FRAME) return 'P'; + if (frametype == PKT_B_FRAME) return 'B'; + return ' '; +} + typedef struct th_pkt { int64_t pkt_dts; int64_t pkt_pts; @@ -49,6 +57,7 @@ typedef struct th_pkt { uint8_t pkt_channels; uint8_t pkt_sri; + uint8_t pkt_err; uint16_t pkt_aspect_num; uint16_t pkt_aspect_den; diff --git a/src/parser_h264.c b/src/parser_h264.c index 07bed03b..0653227f 100644 --- a/src/parser_h264.c +++ b/src/parser_h264.c @@ -352,10 +352,10 @@ h264_decode_pic_parameter_set(elementary_stream_t *st, bitstream_t *bs) int h264_decode_slice_header(elementary_stream_t *st, bitstream_t *bs, int *pkttype, - int *duration, int *isfield) + int *isfield) { h264_private_t *p; - int slice_type, pps_id, sps_id; + unsigned int slice_type, pps_id, sps_id; if((p = st->es_priv) == NULL) return -1; @@ -402,22 +402,20 @@ h264_decode_slice_header(elementary_stream_t *st, bitstream_t *bs, int *pkttype, *isfield = field; - if(p->sps[sps_id].time_scale != 0) { - int d = timebase * p->sps[sps_id].units_in_tick / p->sps[sps_id].time_scale; - *duration = d; - } else { - *duration = 0; - } + int d = 0; + if(p->sps[sps_id].time_scale != 0) + d = timebase * p->sps[sps_id].units_in_tick / p->sps[sps_id].time_scale; if(p->sps[sps_id].cbpsize != 0) st->es_vbv_size = p->sps[sps_id].cbpsize; st->es_vbv_delay = -1; - if(p->sps[sps_id].width && p->sps[sps_id].height && !st->es_buf.sb_err) - parser_set_stream_vsize(st, p->sps[sps_id].width, - p->sps[sps_id].height * - (2 - p->sps[sps_id].mbs_only_flag)); + if(p->sps[sps_id].width && p->sps[sps_id].height && d && !st->es_buf.sb_err) + parser_set_stream_vparam(st, p->sps[sps_id].width, + p->sps[sps_id].height * + (2 - p->sps[sps_id].mbs_only_flag), + d); if(p->sps[sps_id].aspect_num && p->sps[sps_id].aspect_den) { diff --git a/src/parser_h264.h b/src/parser_h264.h index af92a569..5e78b436 100644 --- a/src/parser_h264.h +++ b/src/parser_h264.h @@ -28,6 +28,6 @@ int h264_decode_seq_parameter_set(struct elementary_stream *st, bitstream_t *bs) int h264_decode_pic_parameter_set(struct elementary_stream *st, bitstream_t *bs); int h264_decode_slice_header(struct elementary_stream *st, bitstream_t *bs, - int *pkttype, int *duration, int *isfield); + int *pkttype, int *isfield); #endif /* PARSER_H264_H_ */ diff --git a/src/parsers.c b/src/parsers.c index d72b0f0f..9adc8b20 100644 --- a/src/parsers.c +++ b/src/parsers.c @@ -119,7 +119,8 @@ static int parse_eac3(service_t *t, elementary_stream_t *st, size_t len, static int parse_mp4a(service_t *t, elementary_stream_t *st, size_t ilen, uint32_t next_startcode, int sc_offset); -static void parser_deliver(service_t *t, elementary_stream_t *st, th_pkt_t *pkt); +static void parser_deliver(service_t *t, elementary_stream_t *st, th_pkt_t *pkt, + int errors); static int parse_pes_header(service_t *t, elementary_stream_t *st, const uint8_t *buf, size_t len); @@ -266,7 +267,7 @@ parse_aac(service_t *t, elementary_stream_t *st, const uint8_t *data, pkt = parse_latm_audio_mux_element(t, st, d + 3, muxlen); if(pkt != NULL) - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, st->es_buf.sb_err); p += muxlen + 3; } else { @@ -413,6 +414,8 @@ depacketize(service_t *t, elementary_stream_t *st, size_t len, buf += hlen; len -= hlen; + st->es_buf_a.sb_err = st->es_buf.sb_err; + sbuf_append(&st->es_buf_a, buf, len); return 0; } @@ -425,7 +428,8 @@ depacketize(service_t *t, elementary_stream_t *st, size_t len, */ static void makeapkt(service_t *t, elementary_stream_t *st, const void *buf, - int len, int64_t dts, int duration, int channels, int sri) + int len, int64_t dts, int duration, int channels, int sri, + int errors) { th_pkt_t *pkt = pkt_alloc(buf, len, dts, dts); @@ -435,7 +439,7 @@ makeapkt(service_t *t, elementary_stream_t *st, const void *buf, pkt->pkt_channels = channels; pkt->pkt_sri = sri; - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, errors); st->es_curdts = PTS_UNSET; st->es_nextdts = dts + duration; @@ -471,7 +475,7 @@ mp4a_valid_frame(const uint8_t *buf) } static int parse_mp4a(service_t *t, elementary_stream_t *st, size_t ilen, - uint32_t next_startcode, int sc_offset) + uint32_t next_startcode, int sc_offset) { int i, len; const uint8_t *buf; @@ -505,7 +509,9 @@ static int parse_mp4a(service_t *t, elementary_stream_t *st, size_t ilen, int channels = ((p[2] & 0x01) << 2) | ((p[3] & 0xc0) >> 6); - makeapkt(t, st, p, fsize, dts, duration, channels, sri); + makeapkt(t, st, p, fsize, dts, duration, channels, sri, + st->es_buf_a.sb_err); + st->es_buf_a.sb_err = 0; sbuf_cut(&st->es_buf_a, i + fsize); goto again; } @@ -565,7 +571,9 @@ parse_mpa2(service_t *t, elementary_stream_t *st) mpa_valid_frame(buf + i + fsize)) { makeapkt(t, st, buf + i, fsize, dts, duration, - channels, mpa_sri[(buf[i+2] >> 2) & 3]); + channels, mpa_sri[(buf[i+2] >> 2) & 3], + st->es_buf_a.sb_err); + st->es_buf_a.sb_err = 0; sbuf_cut(&st->es_buf_a, i + fsize); goto again; } @@ -705,7 +713,9 @@ parse_ac3(service_t *t, elementary_stream_t *st, size_t ilen, int lfeon = read_bits(&bs, 1); int channels = acmodtab[acmod] + lfeon; - makeapkt(t, st, p, fsize, dts, duration, channels, sri); + makeapkt(t, st, p, fsize, dts, duration, channels, sri, + st->es_buf_a.sb_err); + st->es_buf_a.sb_err = 0; sbuf_cut(&st->es_buf_a, i + fsize); goto again; } @@ -775,7 +785,9 @@ parse_eac3(service_t *t, elementary_stream_t *st, size_t ilen, if(dts != PTS_UNSET && len >= i + fsize + 6 && eac3_valid_frame(p + fsize)) { - makeapkt(t, st, p, fsize, dts, duration, channels, sri); + makeapkt(t, st, p, fsize, dts, duration, channels, sri, + st->es_buf_a.sb_err); + st->es_buf_a.sb_err = 0; sbuf_cut(&st->es_buf_a, i + fsize); goto again; } @@ -892,16 +904,18 @@ parse_mpeg2video_pic_start(service_t *t, elementary_stream_t *st, int *frametype * */ void -parser_set_stream_vsize(elementary_stream_t *st, int width, int height) +parser_set_stream_vparam(elementary_stream_t *st, int width, int height, + int duration) { int need_save = 0; - if(st->es_width == 0 && st->es_height == 0) { + if(st->es_width == 0 && st->es_height == 0 && st->es_frame_duration == 0) { need_save = 1; st->es_meta_change = 0; - } else if(st->es_width != width || st->es_height != height) { - + } else if(st->es_width != width || st->es_height != height || + st->es_frame_duration != duration) { + st->es_meta_change++; if(st->es_meta_change == 2) need_save = 1; @@ -913,6 +927,7 @@ parser_set_stream_vsize(elementary_stream_t *st, int width, int height) if(need_save) { st->es_width = width; st->es_height = height; + st->es_frame_duration = duration; service_request_save(st->es_service, 1); } } @@ -957,7 +972,7 @@ parse_mpeg2video_seq_start(service_t *t, elementary_stream_t *st, st->es_aspect_num = mpeg2_aspect[aspect][0]; st->es_aspect_den = mpeg2_aspect[aspect][1]; - st->es_frame_duration = mpeg2video_framedurations[read_bits(bs, 4)]; + int duration = mpeg2video_framedurations[read_bits(bs, 4)]; v = read_bits(bs, 18) * 400; skip_bits(bs, 1); @@ -965,7 +980,7 @@ parse_mpeg2video_seq_start(service_t *t, elementary_stream_t *st, v = read_bits(bs, 10) * 16 * 1024 / 8; st->es_vbv_size = v; - parser_set_stream_vsize(st, width, height); + parser_set_stream_vparam(st, width, height, duration); return 0; } @@ -1104,7 +1119,7 @@ parse_mpeg2video(service_t *t, elementary_stream_t *st, size_t len, st->es_buf.sb_ptr - 4); pkt->pkt_duration = st->es_frame_duration; - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, st->es_buf.sb_err); st->es_curpkt = NULL; st->es_buf.sb_data = malloc(st->es_buf.sb_size); @@ -1145,8 +1160,7 @@ parse_h264(service_t *t, elementary_stream_t *st, size_t len, { const uint8_t *buf = st->es_buf.sb_data + sc_offset; uint32_t sc = st->es_startcode; - int64_t d; - int l2, pkttype, duration, isfield; + int l2, pkttype, isfield; bitstream_t bs; int ret = 0; @@ -1157,12 +1171,6 @@ parse_h264(service_t *t, elementary_stream_t *st, size_t len, if(plen >= 0xffe9) st->es_incomplete =1; parse_pes_header(t, st, buf + 6, len - 6); } - if(st->es_prevdts != PTS_UNSET && st->es_curdts != PTS_UNSET) { - d = (st->es_curdts - st->es_prevdts) & 0x1ffffffffLL; - - if(d < 90000) - st->es_frame_duration = d; - } st->es_prevdts = st->es_curdts; return 1; } @@ -1199,23 +1207,23 @@ parse_h264(service_t *t, elementary_stream_t *st, size_t len, case 5: /* IDR+SLICE */ case 1: - if(st->es_curpkt != NULL || st->es_frame_duration == 0) - break; - l2 = len - 3 > 64 ? 64 : len - 3; void *f = h264_nal_deescape(&bs, buf + 3, l2); /* we just want the first stuff */ - if(h264_decode_slice_header(st, &bs, &pkttype, &duration, &isfield)) { + if(h264_decode_slice_header(st, &bs, &pkttype, &isfield)) { free(f); return 1; } free(f); + if(st->es_curpkt != NULL || st->es_frame_duration == 0) + break; + st->es_curpkt = pkt_alloc(NULL, 0, st->es_curpts, st->es_curdts); st->es_curpkt->pkt_frametype = pkttype; st->es_curpkt->pkt_field = isfield; - st->es_curpkt->pkt_duration = duration ?: st->es_frame_duration; + st->es_curpkt->pkt_duration = st->es_frame_duration; st->es_curpkt->pkt_commercial = t->s_tt_commercial_advice; break; @@ -1240,7 +1248,7 @@ parse_h264(service_t *t, elementary_stream_t *st, size_t len, pkt->pkt_payload = pktbuf_make(st->es_buf.sb_data, st->es_buf.sb_ptr - 4); - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, st->es_buf.sb_err); st->es_curpkt = NULL; st->es_buf.sb_data = malloc(st->es_buf.sb_size); @@ -1268,8 +1276,7 @@ parse_subtitles(service_t *t, elementary_stream_t *st, const uint8_t *data, if(start) { /* Payload unit start */ st->es_parser_state = 1; - st->es_buf.sb_err = 0; - st->es_buf.sb_ptr = 0; + sbuf_reset(&st->es_buf); } if(st->es_parser_state == 0) @@ -1313,7 +1320,7 @@ parse_subtitles(service_t *t, elementary_stream_t *st, const uint8_t *data, if(buf[psize - 1] == 0xff) { pkt = pkt_alloc(buf, psize - 1, st->es_curpts, st->es_curdts); pkt->pkt_commercial = t->s_tt_commercial_advice; - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, st->es_buf.sb_err); } } } @@ -1331,7 +1338,6 @@ parse_teletext(service_t *t, elementary_stream_t *st, const uint8_t *data, const uint8_t *d; if(start) { st->es_parser_state = 1; - st->es_buf.sb_err = 0; st->es_parser_ptr = 0; sbuf_reset(&st->es_buf); } @@ -1363,7 +1369,7 @@ parse_teletext(service_t *t, elementary_stream_t *st, const uint8_t *data, pkt = pkt_alloc(buf, psize, st->es_curpts, st->es_curdts); pkt->pkt_commercial = t->s_tt_commercial_advice; - parser_deliver(t, st, pkt); + parser_deliver(t, st, pkt, st->es_buf.sb_err); } } @@ -1371,8 +1377,10 @@ parse_teletext(service_t *t, elementary_stream_t *st, const uint8_t *data, * */ static void -parser_deliver(service_t *t, elementary_stream_t *st, th_pkt_t *pkt) +parser_deliver(service_t *t, elementary_stream_t *st, th_pkt_t *pkt, int error) { + pkt->pkt_err = error; + if(SCT_ISAUDIO(st->es_type) && pkt->pkt_pts != PTS_UNSET && (t->s_current_pts == PTS_UNSET || pkt->pkt_pts > t->s_current_pts || diff --git a/src/parsers.h b/src/parsers.h index 947b6fd6..826daaed 100644 --- a/src/parsers.h +++ b/src/parsers.h @@ -31,7 +31,8 @@ void parse_mpeg_ps(struct service *t, struct elementary_stream *st, void parser_enqueue_packet(struct service *t, struct elementary_stream *st, th_pkt_t *pkt); -void parser_set_stream_vsize(struct elementary_stream *st, int width, int height); +void parser_set_stream_vparam(struct elementary_stream *st, int width, int height, + int duration); extern const unsigned int mpeg2video_framedurations[16]; diff --git a/src/plumbing/globalheaders.c b/src/plumbing/globalheaders.c index d776f34c..7eda3b02 100644 --- a/src/plumbing/globalheaders.c +++ b/src/plumbing/globalheaders.c @@ -255,6 +255,9 @@ gh_hold(globalheaders_t *gh, streaming_message_t *sm) case SMT_SIGNAL_STATUS: case SMT_NOSTART: case SMT_MPEGTS: + case SMT_SPEED: + case SMT_SKIP: + case SMT_TIMESHIFT_STATUS: streaming_target_deliver2(gh->gh_output, sm); break; } @@ -283,6 +286,9 @@ gh_pass(globalheaders_t *gh, streaming_message_t *sm) case SMT_SIGNAL_STATUS: case SMT_NOSTART: case SMT_MPEGTS: + case SMT_SKIP: + case SMT_SPEED: + case SMT_TIMESHIFT_STATUS: streaming_target_deliver2(gh->gh_output, sm); break; 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/plumbing/tsfix.c b/src/plumbing/tsfix.c index d04e610d..858e59ed 100644 --- a/src/plumbing/tsfix.c +++ b/src/plumbing/tsfix.c @@ -55,6 +55,7 @@ typedef struct tsfix { struct tfstream_list tf_streams; int tf_hasvideo; int64_t tf_tsref; + time_t tf_start_time; struct th_pktref_queue tf_ptsq; @@ -153,6 +154,9 @@ normalize_ts(tsfix_t *tf, tfstream_t *tfs, th_pkt_t *pkt) return; } + pkt->pkt_dts &= PTS_MASK; + pkt->pkt_pts &= PTS_MASK; + /* Subtract the transport wide start offset */ dts = pkt->pkt_dts - tf->tf_tsref; @@ -306,23 +310,21 @@ tsfix_input_packet(tsfix_t *tf, streaming_message_t *sm) tfstream_t *tfs = tfs_find(tf, pkt); streaming_msg_free(sm); - if(tfs == NULL) { + if(tfs == NULL || dispatch_clock < tf->tf_start_time) { pkt_ref_dec(pkt); return; } - if(tf->tf_tsref == PTS_UNSET && (!tf->tf_hasvideo || (SCT_ISVIDEO(tfs->tfs_type) && pkt->pkt_frametype == PKT_I_FRAME))) { - tf->tf_tsref = pkt->pkt_dts; + tf->tf_tsref = pkt->pkt_dts & PTS_MASK; tsfixprintf("reference clock set to %"PRId64"\n", tf->tf_tsref); } + int pdur = pkt->pkt_duration >> pkt->pkt_field; + if(pkt->pkt_dts == PTS_UNSET) { - - int pdur = pkt->pkt_duration >> pkt->pkt_field; - if(tfs->tfs_last_dts_in == PTS_UNSET) { pkt_ref_dec(pkt); return; @@ -334,6 +336,7 @@ tsfix_input_packet(tsfix_t *tf, streaming_message_t *sm) streaming_component_type2txt(tfs->tfs_type), tfs->tfs_last_dts_in, pdur, pkt->pkt_dts); } + tfs->tfs_last_dts_in = pkt->pkt_dts; compute_pts(tf, tfs, pkt); @@ -366,6 +369,9 @@ tsfix_input(void *opaque, streaming_message_t *sm) case SMT_SIGNAL_STATUS: case SMT_NOSTART: case SMT_MPEGTS: + case SMT_SPEED: + case SMT_SKIP: + case SMT_TIMESHIFT_STATUS: break; } @@ -384,10 +390,22 @@ tsfix_create(streaming_target_t *output) TAILQ_INIT(&tf->tf_ptsq); tf->tf_output = output; + tf->tf_start_time = dispatch_clock; + streaming_target_init(&tf->tf_input, tsfix_input, tf, 0); return &tf->tf_input; } +/** + * + */ +void tsfix_set_start_time(streaming_target_t *pad, time_t start) +{ + tsfix_t *tf = (tsfix_t *)pad; + + tf->tf_start_time = start; +} + /** * diff --git a/src/plumbing/tsfix.h b/src/plumbing/tsfix.h index 7e6f7bd0..156284fe 100644 --- a/src/plumbing/tsfix.h +++ b/src/plumbing/tsfix.h @@ -23,6 +23,8 @@ streaming_target_t *tsfix_create(streaming_target_t *output); +void tsfix_set_start_time(streaming_target_t *pad, time_t start); + void tsfix_destroy(streaming_target_t *gh); diff --git a/src/psi.c b/src/psi.c index 7960ae2b..378cc4c4 100644 --- a/src/psi.c +++ b/src/psi.c @@ -63,8 +63,8 @@ psi_section_reassemble0(psi_section_t *ps, const uint8_t *data, if(crc && tvh_crc32(ps->ps_data, tsize, 0xffffffff)) return -1; - cb(ps->ps_data, tsize - (crc ? 4 : 0), opaque); ps->ps_offset = 0; + cb(ps->ps_data, tsize - (crc ? 4 : 0), opaque); return len - excess; } @@ -89,15 +89,15 @@ psi_section_reassemble(psi_section_t *ps, const uint8_t *tsb, int crc, int len = tsb[off++]; if(len > 0) { if(len > 188 - off) { - ps->ps_lock = 0; - return; + ps->ps_lock = 0; + return; } psi_section_reassemble0(ps, tsb + off, len, 0, crc, cb, opaque); off += len; } } - while(off < 188 && tsb[off] != 0xff) { + while(off < 188) { r = psi_section_reassemble0(ps, tsb + off, 188 - off, pusi, crc, cb, opaque); if(r < 0) { @@ -110,44 +110,6 @@ psi_section_reassemble(psi_section_t *ps, const uint8_t *tsb, int crc, } -/** - * PAT parser, from ISO 13818-1 - */ -int -psi_parse_pat(service_t *t, uint8_t *ptr, int len, - pid_section_callback_t *pmt_callback) -{ - uint16_t prognum; - uint16_t pid; - elementary_stream_t *st; - - lock_assert(&t->s_stream_mutex); - - if(len < 5) - return -1; - - ptr += 5; - len -= 5; - - while(len >= 4) { - - prognum = ptr[0] << 8 | ptr[1]; - pid = (ptr[2] & 0x1f) << 8 | ptr[3]; - - if(prognum != 0) { - if(service_stream_find(t, pid) == NULL) { - st = service_stream_create(t, pid, SCT_PMT); - st->es_section_docrc = 1; - st->es_got_section = pmt_callback; - } - } - ptr += 4; - len -= 4; - } - return 0; -} - - /** * Append CRC */ @@ -210,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 @@ -347,9 +310,9 @@ psi_desc_teletext(service_t *t, const uint8_t *ptr, int size, if((st = service_stream_find(t, pid)) == NULL) { r |= PMT_UPDATE_NEW_STREAM; st = service_stream_create(t, pid, SCT_TEXTSUB); + st->es_delete_me = 1; } - st->es_delete_me = 0; lang = lang_code_get2((const char*)ptr, 3); if(memcmp(st->es_lang,lang,3)) { @@ -362,10 +325,12 @@ psi_desc_teletext(service_t *t, const uint8_t *ptr, int size, st->es_parent_pid = parent_pid; } - if(st->es_position != *position) { + // Check es_delete_me so we only compute position once per PMT update + if(st->es_position != *position && st->es_delete_me) { st->es_position = *position; r |= PMT_REORDERED; } + st->es_delete_me = 0; (*position)++; } ptr += 5; @@ -433,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; @@ -468,6 +434,10 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, /* Mark all streams for deletion */ if(delete) { TAILQ_FOREACH(st, &t->s_components, es_link) { + + if(st->es_type == SCT_PMT) + continue; + st->es_delete_me = 1; LIST_FOREACH(c, &st->es_caids, link) @@ -561,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: @@ -633,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; @@ -690,7 +666,7 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, service_request_save(t, 0); // Only restart if something that our clients worry about did change - if(update & !(PMT_UPDATE_NEW_CA_STREAM | + if(update & ~(PMT_UPDATE_NEW_CA_STREAM | PMT_UPDATE_NEW_CAID | PMT_UPDATE_CA_PROVIDER_CHANGE | PMT_UPDATE_CAID_DELETED)) { @@ -706,7 +682,8 @@ psi_parse_pmt(service_t *t, const uint8_t *ptr, int len, int chksvcid, * PMT generator */ int -psi_build_pmt(const streaming_start_t *ss, uint8_t *buf0, int maxlen, int pcrpid) +psi_build_pmt(const streaming_start_t *ss, uint8_t *buf0, int maxlen, + int version, int pcrpid) { int c, tlen, dlen, l, i; uint8_t *buf, *buf1; @@ -722,8 +699,10 @@ psi_build_pmt(const streaming_start_t *ss, uint8_t *buf0, int maxlen, int pcrpid buf[4] = 0x01; buf[5] = 0xc1; /* current_next_indicator + version */ - buf[6] = 0; - buf[7] = 0; + buf[5] |= (version & 0x1F) << 1; + + buf[6] = 0; /* section number */ + buf[7] = 0; /* last section number */ buf[8] = 0xe0 | (pcrpid >> 8); buf[9] = pcrpid; @@ -785,7 +764,7 @@ psi_build_pmt(const streaming_start_t *ss, uint8_t *buf0, int maxlen, int pcrpid 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: @@ -848,14 +827,27 @@ static struct strtab caidnametab[] = { { "Viaccess", 0x0500 }, { "Irdeto", 0x0600 }, { "Irdeto", 0x0602 }, - { "Irdeto", 0x0604 }, + { "Irdeto", 0x0603 }, + { "Irdeto", 0x0604 }, + { "Irdeto", 0x0622 }, { "Irdeto", 0x0624 }, + { "Irdeto", 0x0648 }, { "Irdeto", 0x0666 }, { "Jerroldgi", 0x0700 }, { "Matra", 0x0800 }, - { "NDS", 0x0900 }, + { "NDS", 0x0900 }, + { "NDS", 0x0919 }, + { "NDS", 0x091F }, + { "NDS", 0x092B }, + { "NDS", 0x09AF }, + { "NDS", 0x09C4 }, + { "NDS", 0x0960 }, + { "NDS", 0x0963 }, { "Nokia", 0x0A00 }, - { "Conax", 0x0B00 }, + { "Conax", 0x0B00 }, + { "Conax", 0x0B01 }, + { "Conax", 0x0B02 }, + { "Conax", 0x0BAA }, { "NTL", 0x0C00 }, { "CryptoWorks", 0x0D00 }, { "CryptoWorks", 0x0D01 }, @@ -864,9 +856,11 @@ static struct strtab caidnametab[] = { { "CryptoWorks", 0x0D05 }, { "CryptoWorks", 0x0D0F }, { "CryptoWorks", 0x0D70 }, + { "CryptoWorks ICE", 0x0D95 }, { "CryptoWorks ICE", 0x0D96 }, { "CryptoWorks ICE", 0x0D97 }, { "PowerVu", 0x0E00 }, + { "PowerVu", 0x0E11 }, { "Sony", 0x0F00 }, { "Tandberg", 0x1000 }, { "Thompson", 0x1100 }, @@ -879,7 +873,16 @@ static struct strtab caidnametab[] = { { "BetaCrypt", 0x1702 }, { "BetaCrypt", 0x1722 }, { "BetaCrypt", 0x1762 }, - { "NagraVision", 0x1800 }, + { "NagraVision", 0x1800 }, + { "NagraVision", 0x1803 }, + { "Nagra Media Access", 0x1813 }, + { "NagraVision", 0x1810 }, + { "NagraVision", 0x1815 }, + { "NagraVision", 0x1830 }, + { "NagraVision", 0x1833 }, + { "NagraVision", 0x1834 }, + { "NagraVision", 0x183D }, + { "NagraVision", 0x1861 }, { "Titan", 0x1900 }, { "Telefonica", 0x2000 }, { "Stentor", 0x2100 }, @@ -894,6 +897,7 @@ static struct strtab caidnametab[] = { { "DRECrypt2", 0x4ae1 }, { "Bulcrypt", 0x4aee }, { "Bulcrypt", 0x5581 }, + { "Verimatrix", 0x5601 }, }; const char * @@ -908,10 +912,26 @@ 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"; +} + /** * */ static struct strtab streamtypetab[] = { + { "NONE", SCT_NONE }, + { "UNKNOWN", SCT_UNKNOWN }, { "MPEG2VIDEO", SCT_MPEG2VIDEO }, { "MPEG2AUDIO", SCT_MPEG2AUDIO }, { "H264", SCT_H264 }, @@ -920,12 +940,11 @@ static struct strtab streamtypetab[] = { { "DVBSUB", SCT_DVBSUB }, { "CA", SCT_CA }, { "PMT", SCT_PMT }, - { "PAT", SCT_PAT }, { "AAC", SCT_AAC }, { "MPEGTS", SCT_MPEGTS }, { "TEXTSUB", SCT_TEXTSUB }, { "EAC3", SCT_EAC3 }, - { "AAC", SCT_MP4A }, + { "AAC", SCT_MP4A }, }; @@ -938,6 +957,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 @@ -964,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; @@ -994,6 +1024,8 @@ psi_save_service_settings(htsmsg_t *m, service_t *t) htsmsg_add_u32(sub, "width", st->es_width); htsmsg_add_u32(sub, "height", st->es_height); } + if(st->es_frame_duration) + htsmsg_add_u32(sub, "duration", st->es_frame_duration); } htsmsg_add_msg(m, "stream", sub); @@ -1112,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; @@ -1137,6 +1174,9 @@ psi_load_service_settings(htsmsg_t *m, service_t *t) if(!htsmsg_get_u32(c, "height", &u32)) st->es_height = u32; + + if(!htsmsg_get_u32(c, "duration", &u32)) + st->es_frame_duration = u32; } } diff --git a/src/psi.h b/src/psi.h index 270c4335..5dfc98b5 100644 --- a/src/psi.h +++ b/src/psi.h @@ -37,17 +37,16 @@ typedef struct psi_section { void psi_section_reassemble(psi_section_t *ps, const uint8_t *tsb, int crc, section_handler_t *cb, void *opaque); -int psi_parse_pat(struct service *t, uint8_t *ptr, int len, - pid_section_callback_t *pmt_callback); - int psi_parse_pmt(struct service *t, const uint8_t *ptr, int len, int chksvcid, int delete); int psi_build_pat(struct service *t, uint8_t *buf, int maxlen, int pmtpid); -int psi_build_pmt(const streaming_start_t *ss, uint8_t *buf, int maxlen, int pcrpid); +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/rawtsinput.c b/src/rawtsinput.c index e22ac146..134fcad5 100644 --- a/src/rawtsinput.c +++ b/src/rawtsinput.c @@ -72,13 +72,12 @@ static void rawts_service_save(service_t *t) { htsmsg_t *m = htsmsg_create_map(); - printf("SAVE %s\n", service_nicename(t)); pthread_mutex_lock(&t->s_stream_mutex); psi_save_service_settings(m, t); pthread_mutex_unlock(&t->s_stream_mutex); - htsmsg_print(m); + //htsmsg_print(m); htsmsg_destroy(m); } @@ -93,6 +92,15 @@ rawts_service_quality(service_t *t) return 100; } +/** + * + */ +static int +rawts_service_is_enabled(service_t *t) +{ + return 1; +} + /** * Generate a descriptive name for the source @@ -134,6 +142,7 @@ rawts_service_add(rawts_t *rt, uint16_t sid, int pmt_pid) t->s_config_save = rawts_service_save; t->s_setsourceinfo = rawts_service_setsourceinfo; t->s_quality_index = rawts_service_quality; + t->s_is_enabled = rawts_service_is_enabled; t->s_svcname = strdup(tmp); @@ -164,7 +173,7 @@ got_pmt(struct service *t, elementary_stream_t *st, return; pthread_mutex_lock(&global_lock); - psi_parse_pmt(t, table + 3, table_len - 3, 1, 0); + psi_parse_pmt(t, table + 3, table_len - 3, 1, 1); pthread_mutex_unlock(&global_lock); } @@ -267,7 +276,7 @@ process_ts_packet(rawts_t *rt, uint8_t *tsb) slp.tv_sec = d / 1000000; slp.tv_nsec = (d % 1000000) * 1000; - clock_nanosleep(CLOCK_MONOTONIC_COARSE, TIMER_ABSTIME, &slp, NULL); + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &slp, NULL); didsleep = 1; } t->s_pcr_last = pcr; diff --git a/src/service.c b/src/service.c index 4c3e8fdc..c6666b5f 100644 --- a/src/service.c +++ b/src/service.c @@ -46,7 +46,7 @@ #include "serviceprobe.h" #include "atomic.h" #include "dvb/dvb.h" -#include "htsp.h" +#include "htsp_server.h" #include "lang_codes.h" #define SERVICE_HASH_WIDTH 101 @@ -110,18 +110,21 @@ stream_clean(elementary_stream_t *st) st->es_global_data_len = 0; } - /** * */ void -service_stream_destroy(service_t *t, elementary_stream_t *st) +service_stream_destroy(service_t *t, elementary_stream_t *es) { if(t->s_status == SERVICE_RUNNING) - stream_clean(st); - TAILQ_REMOVE(&t->s_components, st, es_link); - free(st->es_nicename); - free(st); + 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); } /** @@ -153,6 +156,8 @@ service_stop(service_t *t) 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); @@ -203,8 +208,10 @@ service_start(service_t *t, unsigned int weight, int force_start) if((r = t->s_start_feed(t, weight, force_start))) return r; +#if ENABLE_CWC cwc_service_start(t); capmt_service_start(t); +#endif pthread_mutex_lock(&t->s_stream_mutex); @@ -328,7 +335,7 @@ service_find(channel_t *ch, unsigned int weight, const char *loginfo, cnt = 0; LIST_FOREACH(t, &ch->ch_services, s_ch_link) { - if(!t->s_enabled) { + if(!t->s_is_enabled(t)) { if(loginfo != NULL) { tvhlog(LOG_NOTICE, "Service", "%s: Skipping \"%s\" -- not enabled", loginfo, service_nicename(t)); @@ -456,7 +463,6 @@ 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); @@ -487,23 +493,18 @@ service_destroy(service_t *t) free(t->s_provider); free(t->s_dvb_charset); - while((st = TAILQ_FIRST(&t->s_components)) != NULL) { - TAILQ_REMOVE(&t->s_components, st, es_link); - free(st->es_nicename); - free(st); - } + 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); - service_unref(t); + avgstat_flush(&t->s_cc_errors); + avgstat_flush(&t->s_rate); - if(ch != NULL) { - if(LIST_FIRST(&ch->ch_services) == NULL) - channel_delete(ch); - } + service_unref(t); } @@ -761,8 +762,9 @@ static struct strtab stypetab[] = { { "SDTV", ST_DN_SDTV }, { "HDTV", ST_DN_HDTV }, { "SDTV", ST_SK_SDTV }, - { "SDTV-AC", ST_AC_SDTV }, - { "HDTV-AC", ST_AC_HDTV }, + { "SDTV", ST_NE_SDTV }, + { "SDTV", ST_AC_SDTV }, + { "HDTV", ST_AC_HDTV }, }; const char * @@ -775,29 +777,40 @@ service_servicetype_txt(service_t *t) * */ int -service_is_tv(service_t *t) +servicetype_is_tv(int servicetype) { 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_AC_SDTV || - t->s_servicetype == ST_AC_HDTV; + servicetype == ST_SDTV || + servicetype == ST_HDTV || + servicetype == ST_EX_HDTV || + servicetype == ST_EX_SDTV || + servicetype == ST_EP_HDTV || + servicetype == ST_ET_HDTV || + servicetype == ST_DN_SDTV || + servicetype == ST_DN_HDTV || + servicetype == ST_SK_SDTV || + servicetype == ST_NE_SDTV || + servicetype == ST_AC_SDTV || + servicetype == ST_AC_HDTV; +} +int +service_is_tv(service_t *t) +{ + return servicetype_is_tv(t->s_servicetype); } /** * */ int +servicetype_is_radio(int servicetype) +{ + return servicetype == ST_RADIO; +} +int service_is_radio(service_t *t) { - return t->s_servicetype == ST_RADIO; + return servicetype_is_radio(t->s_servicetype); } /** @@ -895,17 +908,20 @@ 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; 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; } @@ -924,6 +940,15 @@ service_set_enable(service_t *t, int enabled) subscription_reschedule(); } +void +service_set_prefcapid(service_t *t, uint32_t prefcapid) +{ + if(t->s_prefcapid == prefcapid) + return; + + t->s_prefcapid = prefcapid; + t->s_config_save(t); +} static pthread_mutex_t pending_save_mutex; static pthread_cond_t pending_save_cond; @@ -1165,8 +1190,7 @@ 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_dvb_mux_instance) continue; - if (!t->s_enabled || !t->s_dvb_eit_enable) continue; + if (!t->s_is_enabled(t) || !t->s_dvb_eit_enable) continue; if (!ret || service_get_prio(t) < service_get_prio(ret)) ret = t; } diff --git a/src/service.h b/src/service.h index ad357226..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; @@ -226,6 +228,7 @@ typedef struct service { */ enum { S_MPEG_TS, + S_MPEG_PS, S_OTHER, } s_source_type; @@ -248,6 +251,7 @@ typedef struct service { * subscription scheduling. */ int s_enabled; + int (*s_is_enabled)(struct service *t); /** * Last PCR seen, we use it for a simple clock for rawtsinput.c @@ -323,6 +327,7 @@ typedef struct service { ST_HDTV = 0x11, /* HDTV (MPEG2) */ ST_AC_SDTV = 0x16, /* Advanced codec SDTV */ ST_AC_HDTV = 0x19, /* Advanced codec HDTV */ + ST_NE_SDTV = 0x80, /* NET POA - Cabo SDTV */ ST_EX_HDTV = 0x91, /* Bell TV HDTV */ ST_EX_SDTV = 0x96, /* Bell TV SDTV */ ST_EP_HDTV = 0xA0, /* Bell TV tiered HDTV */ @@ -337,7 +342,6 @@ typedef struct service { * Teletext... */ th_commercial_advice_t s_tt_commercial_advice; - int s_tt_rundown_content_length; time_t s_tt_clock; /* Network clock as determined by teletext decoder */ /** @@ -472,6 +476,7 @@ typedef struct service { int s_scrambled; int s_scrambled_seen; int s_caid; + uint16_t s_prefcapid; /** * PCR drift compensation. This should really be per-packet. @@ -548,6 +553,10 @@ int service_is_tv(service_t *t); int service_is_radio(service_t *t); +int servicetype_is_tv(int st); + +int servicetype_is_radio(int st); + void service_destroy(service_t *t); void service_remove_subscriber(service_t *t, struct th_subscription *s, @@ -595,6 +604,8 @@ void service_set_dvb_charset(service_t *t, const char *dvb_charset); void service_set_dvb_eit_enable(service_t *t, int dvb_eit_enable); +void service_set_prefcapid(service_t *t, uint32_t prefcapid); + int service_is_primary_epg (service_t *t); htsmsg_t *servicetype_list (void); diff --git a/src/serviceprobe.c b/src/serviceprobe.c index 7cc6282a..f542ca33 100644 --- a/src/serviceprobe.c +++ b/src/serviceprobe.c @@ -108,13 +108,17 @@ serviceprobe_thread(void *aux) was_doing_work = 1; } - checksubscr = !t->s_dvb_mux_instance->tdmi_adapter->tda_skip_checksubscr; + if (t->s_dvb_mux_instance) + checksubscr = !t->s_dvb_mux_instance->tdmi_adapter->tda_skip_checksubscr; + else + checksubscr = 1; if (checksubscr) { tvhlog(LOG_INFO, "serviceprobe", "%20s: checking...", t->s_svcname); - s = subscription_create_from_service(t, "serviceprobe", &sq.sq_st, 0); + s = subscription_create_from_service(t, "serviceprobe", &sq.sq_st, 0, + NULL, NULL, "serviceprobe"); if(s == NULL) { t->s_sp_onqueue = 0; TAILQ_REMOVE(&serviceprobe_queue, t, s_sp_link); @@ -198,6 +202,7 @@ serviceprobe_thread(void *aux) case ST_EX_SDTV: case ST_DN_SDTV: case ST_SK_SDTV: + case ST_NE_SDTV: str = "SDTV"; break; case ST_HDTV: diff --git a/src/settings.c b/src/settings.c index 8f0de39b..da46c455 100644 --- a/src/settings.c +++ b/src/settings.c @@ -83,30 +83,25 @@ hts_settings_init(const char *confpath) int hts_settings_makedirs ( const char *inpath ) { - size_t x; - struct stat st; + size_t x = strlen(inpath) - 1; char path[512]; - size_t l = strlen(inpath); strcpy(path, inpath); - for(x = 0; x < l; x++) { - if(path[x] == '/' && x != 0) { + + while (x) { + if (path[x] == '/') { path[x] = 0; - if(stat(path, &st) && mkdir(path, 0700)) { - tvhlog(LOG_ALERT, "settings", "Unable to create dir \"%s\": %s", - path, strerror(errno)); - return -1; - } - path[x] = '/'; + break; } + x--; } - return 0; + return makedirs(path, 0700); } /** * */ static void -hts_settings_buildpath +_hts_settings_buildpath (char *dst, size_t dstsize, const char *fmt, va_list ap, const char *prefix) { char tmp[256]; @@ -125,6 +120,18 @@ hts_settings_buildpath } } +int +hts_settings_buildpath + (char *dst, size_t dstsize, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + if (!settingspath) + return 1; + _hts_settings_buildpath(dst, dstsize, fmt, va, settingspath); + return 0; +} + /** * */ @@ -144,7 +151,7 @@ hts_settings_save(htsmsg_t *record, const char *pathfmt, ...) /* Clean the path */ va_start(ap, pathfmt); - hts_settings_buildpath(path, sizeof(path), pathfmt, ap, settingspath); + _hts_settings_buildpath(path, sizeof(path), pathfmt, ap, settingspath); va_end(ap); /* Create directories */ @@ -163,8 +170,7 @@ hts_settings_save(htsmsg_t *record, const char *pathfmt, ...) htsbuf_queue_init(&hq, 0); htsmsg_json_serialize(record, &hq, 1); TAILQ_FOREACH(hd, &hq.hq_q, hd_link) - if(write(fd, hd->hd_data + hd->hd_data_off, hd->hd_data_len) != - hd->hd_data_len) { + if(tvh_write(fd, hd->hd_data + hd->hd_data_off, hd->hd_data_len)) { tvhlog(LOG_ALERT, "settings", "Failed to write file \"%s\" - %s", tmppath, strerror(errno)); ok = 0; @@ -200,11 +206,13 @@ hts_settings_load_one(const char *filename) mem = malloc(fb_size(fp)+1); n = fb_read(fp, mem, fb_size(fp)); if (n >= 0) mem[n] = 0; - fb_close(fp); /* Decode */ if(n == fb_size(fp)) r = htsmsg_json_deserialize(mem); + + /* Close */ + fb_close(fp); free(mem); return r; @@ -265,16 +273,16 @@ hts_settings_load(const char *pathfmt, ...) /* Try normal path */ va_start(ap, pathfmt); - hts_settings_buildpath(fullpath, sizeof(fullpath), - pathfmt, ap, settingspath); + _hts_settings_buildpath(fullpath, sizeof(fullpath), + pathfmt, ap, settingspath); va_end(ap); ret = _hts_settings_load(fullpath); /* Try bundle path */ if (!ret && *pathfmt != '/') { va_start(ap, pathfmt); - hts_settings_buildpath(fullpath, sizeof(fullpath), - pathfmt, ap, "data/conf"); + _hts_settings_buildpath(fullpath, sizeof(fullpath), + pathfmt, ap, "data/conf"); va_end(ap); ret = _hts_settings_load(fullpath); } @@ -293,7 +301,7 @@ hts_settings_remove(const char *pathfmt, ...) struct stat st; va_start(ap, pathfmt); - hts_settings_buildpath(fullpath, sizeof(fullpath), + _hts_settings_buildpath(fullpath, sizeof(fullpath), pathfmt, ap, settingspath); va_end(ap); if (stat(fullpath, &st) == 0) { @@ -315,7 +323,7 @@ hts_settings_open_file(int for_write, const char *pathfmt, ...) /* Build path */ va_start(ap, pathfmt); - hts_settings_buildpath(path, sizeof(path), pathfmt, ap, settingspath); + _hts_settings_buildpath(path, sizeof(path), pathfmt, ap, settingspath); va_end(ap); /* Create directories */ diff --git a/src/settings.h b/src/settings.h index 7f0bb8d0..a1db839c 100644 --- a/src/settings.h +++ b/src/settings.h @@ -34,6 +34,8 @@ const char *hts_settings_get_root(void); int hts_settings_open_file(int for_write, const char *pathfmt, ...); +int hts_settings_buildpath(char *dst, size_t dstsize, const char *pathfmt, ...); + int hts_settings_makedirs ( const char *path ); #endif /* HTSSETTINGS_H__ */ diff --git a/src/spawn.c b/src/spawn.c index 1ae74891..534e5af7 100644 --- a/src/spawn.c +++ b/src/spawn.c @@ -53,13 +53,13 @@ find_exec ( const char *name, char *out, size_t len ) { int ret = 0; char bin[512]; - char *path, *tmp; + char *path, *tmp, *tmp2 = NULL; DIR *dir; struct dirent *de; struct stat st; if (!(path = getenv("PATH"))) return 0; path = strdup(path); - tmp = strtok(path, ":"); + tmp = strtok_r(path, ":", &tmp2); while (tmp && !ret) { if ((dir = opendir(tmp))) { while ((de = readdir(dir))) { @@ -73,7 +73,7 @@ find_exec ( const char *name, char *out, size_t len ) } closedir(dir); } - tmp = strtok(NULL, ":"); + tmp = strtok_r(NULL, ":", &tmp2); } free(path); return ret; diff --git a/src/streaming.c b/src/streaming.c old mode 100755 new mode 100644 index 7b4559fe..2e0cace3 --- a/src/streaming.c +++ b/src/streaming.c @@ -23,6 +23,7 @@ #include "packet.h" #include "atomic.h" #include "service.h" +#include "timeshift.h" void streaming_pad_init(streaming_pad_t *sp) @@ -52,7 +53,16 @@ streaming_queue_deliver(void *opauqe, streaming_message_t *sm) streaming_queue_t *sq = opauqe; pthread_mutex_lock(&sq->sq_mutex); - TAILQ_INSERT_TAIL(&sq->sq_queue, sm, sm_link); + + /* queue size protection */ + // TODO: would be better to update size as we go, but this would + // require updates elsewhere to ensure all removals from the queue + // are covered (new function) + if (sq->sq_maxsize && streaming_queue_size(&sq->sq_queue) >= sq->sq_maxsize) + streaming_msg_free(sm); + else + TAILQ_INSERT_TAIL(&sq->sq_queue, sm, sm_link); + pthread_cond_signal(&sq->sq_cond); pthread_mutex_unlock(&sq->sq_mutex); } @@ -62,13 +72,24 @@ streaming_queue_deliver(void *opauqe, streaming_message_t *sm) * */ void -streaming_queue_init(streaming_queue_t *sq, int reject_filter) +streaming_queue_init2(streaming_queue_t *sq, int reject_filter, size_t maxsize) { streaming_target_init(&sq->sq_st, streaming_queue_deliver, sq, reject_filter); pthread_mutex_init(&sq->sq_mutex, NULL); pthread_cond_init(&sq->sq_cond, NULL); TAILQ_INIT(&sq->sq_queue); + + sq->sq_maxsize = maxsize; +} + +/** + * + */ +void +streaming_queue_init(streaming_queue_t *sq, int reject_filter) +{ + streaming_queue_init2(sq, reject_filter, 0); // 0 = unlimited } @@ -117,6 +138,9 @@ streaming_msg_create(streaming_message_type_t type) { streaming_message_t *sm = malloc(sizeof(streaming_message_t)); sm->sm_type = type; +#if ENABLE_TIMESHIFT + sm->sm_time = 0; +#endif return sm; } @@ -168,7 +192,10 @@ streaming_msg_clone(streaming_message_t *src) streaming_message_t *dst = malloc(sizeof(streaming_message_t)); streaming_start_t *ss; - dst->sm_type = src->sm_type; + dst->sm_type = src->sm_type; +#if ENABLE_TIMESHIFT + dst->sm_time = src->sm_time; +#endif switch(src->sm_type) { @@ -182,11 +209,22 @@ streaming_msg_clone(streaming_message_t *src) atomic_add(&ss->ss_refcount, 1); break; + case SMT_SKIP: + dst->sm_data = malloc(sizeof(streaming_skip_t)); + memcpy(dst->sm_data, src->sm_data, sizeof(streaming_skip_t)); + break; + case SMT_SIGNAL_STATUS: dst->sm_data = malloc(sizeof(signal_status_t)); memcpy(dst->sm_data, src->sm_data, sizeof(signal_status_t)); break; + case SMT_TIMESHIFT_STATUS: + dst->sm_data = malloc(sizeof(timeshift_status_t)); + memcpy(dst->sm_data, src->sm_data, sizeof(timeshift_status_t)); + break; + + case SMT_SPEED: case SMT_STOP: case SMT_SERVICE_STATUS: case SMT_NOSTART: @@ -244,18 +282,17 @@ streaming_msg_free(streaming_message_t *sm) break; case SMT_STOP: - break; - case SMT_EXIT: - break; - case SMT_SERVICE_STATUS: - break; - case SMT_NOSTART: + case SMT_SPEED: break; + case SMT_SKIP: case SMT_SIGNAL_STATUS: +#if ENABLE_TIMESHIFT + case SMT_TIMESHIFT_STATUS: +#endif free(sm->sm_data); break; @@ -331,6 +368,36 @@ streaming_queue_clear(struct streaming_message_queue *q) } +/** + * + */ +size_t streaming_queue_size(struct streaming_message_queue *q) +{ + streaming_message_t *sm; + int size = 0; + + TAILQ_FOREACH(sm, q, sm_link) { + if (sm->sm_type == SMT_PACKET) + { + th_pkt_t *pkt = sm->sm_data; + if (pkt && pkt->pkt_payload) + { + size += pkt->pkt_payload->pb_size; + } + } + else if (sm->sm_type == SMT_MPEGTS) + { + pktbuf_t *pkt_payload = sm->sm_data; + if (pkt_payload) + { + size += pkt_payload->pb_size; + } + } + } + return size; +} + + /** * */ diff --git a/src/streaming.h b/src/streaming.h index eb2919c9..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; @@ -54,6 +55,7 @@ typedef struct streaming_start { source_info_t ss_si; uint16_t ss_pcr_pid; + uint16_t ss_pmt_pid; streaming_start_component_t ss_components[0]; @@ -71,8 +73,13 @@ void streaming_target_init(streaming_target_t *st, void streaming_queue_init(streaming_queue_t *sq, int reject_filter); +void streaming_queue_init2 + (streaming_queue_t *sq, int reject_filter, size_t maxsize); + void streaming_queue_clear(struct streaming_message_queue *q); +size_t streaming_queue_size(struct streaming_message_queue *q); + void streaming_queue_deinit(streaming_queue_t *sq); void streaming_target_connect(streaming_pad_t *sp, streaming_target_t *st); diff --git a/src/subscriptions.c b/src/subscriptions.c index 4249a382..548fa7e9 100644 --- a/src/subscriptions.c +++ b/src/subscriptions.c @@ -36,6 +36,10 @@ #include "streaming.h" #include "channels.h" #include "service.h" +#include "htsmsg.h" +#include "notify.h" +#include "atomic.h" +#include "dvb/dvb.h" struct th_subscription_list subscriptions; static gtimer_t subscription_reschedule_timer; @@ -75,9 +79,14 @@ subscription_link_service(th_subscription_t *s, service_t *t) pthread_mutex_lock(&t->s_stream_mutex); - if(TAILQ_FIRST(&t->s_components) != NULL) + if(TAILQ_FIRST(&t->s_components) != NULL) { + + if(s->ths_start_message != NULL) + streaming_msg_free(s->ths_start_message); + s->ths_start_message = streaming_msg_create_data(SMT_START, service_build_stream_start(t)); + } // Link to service output streaming_target_connect(&t->s_streaming_pad, &s->ths_input); @@ -129,12 +138,16 @@ subscription_unlink_service(th_subscription_t *s, int reason) } +/** + * + */ static void subscription_reschedule_cb(void *aux) { subscription_reschedule(); } + /** * */ @@ -208,13 +221,25 @@ subscription_unsubscribe(th_subscription_t *s) if(t != NULL) service_remove_subscriber(t, s, SM_CODE_OK); + if(s->ths_tdmi != NULL) { + LIST_REMOVE(s, ths_tdmi_link); + th_dvb_adapter_t *tda = s->ths_tdmi->tdmi_adapter; + pthread_mutex_lock(&tda->tda_delivery_mutex); + streaming_target_disconnect(&tda->tda_streaming_pad, &s->ths_input); + pthread_mutex_unlock(&tda->tda_delivery_mutex); + } + if(s->ths_start_message != NULL) streaming_msg_free(s->ths_start_message); free(s->ths_title); + free(s->ths_hostname); + free(s->ths_username); + free(s->ths_client); free(s); subscription_reschedule(); + notify_reload("subscriptions"); } @@ -261,6 +286,17 @@ subscription_input(void *opauqe, streaming_message_t *sm) streaming_msg_free(sm); return; } + + if(sm->sm_type == SMT_PACKET) { + th_pkt_t *pkt = sm->sm_data; + if(pkt->pkt_err) + s->ths_total_err++; + s->ths_bytes += pkt->pkt_payload->pb_size; + } else if(sm->sm_type == SMT_MPEGTS) { + pktbuf_t *pb = sm->sm_data; + s->ths_bytes += pb->pb_size; + } + streaming_target_deliver(s->ths_output, sm); } @@ -272,6 +308,17 @@ static void subscription_input_direct(void *opauqe, streaming_message_t *sm) { th_subscription_t *s = opauqe; + + if(sm->sm_type == SMT_PACKET) { + th_pkt_t *pkt = sm->sm_data; + if(pkt->pkt_err) + s->ths_total_err++; + s->ths_bytes += pkt->pkt_payload->pb_size; + } else if(sm->sm_type == SMT_MPEGTS) { + pktbuf_t *pb = sm->sm_data; + s->ths_bytes += pb->pb_size; + } + streaming_target_deliver(s->ths_output, sm); } @@ -280,29 +327,36 @@ subscription_input_direct(void *opauqe, streaming_message_t *sm) /** * */ -static th_subscription_t * +th_subscription_t * subscription_create(int weight, const char *name, streaming_target_t *st, - int flags, int direct) + int flags, st_callback_t *cb, const char *hostname, + const char *username, const char *client) { th_subscription_t *s = calloc(1, sizeof(th_subscription_t)); int reject = 0; + static int tally; if(flags & SUBSCRIPTION_RAW_MPEGTS) reject |= SMT_TO_MASK(SMT_PACKET); // Reject parsed frames else reject |= SMT_TO_MASK(SMT_MPEGTS); // Reject raw mpegts - - streaming_target_init(&s->ths_input, direct ? subscription_input_direct : - subscription_input, s, reject); + streaming_target_init(&s->ths_input, + cb ?: subscription_input_direct, s, reject); s->ths_weight = weight; s->ths_title = strdup(name); + s->ths_hostname = hostname ? strdup(hostname) : NULL; + s->ths_username = username ? strdup(username) : NULL; + s->ths_client = client ? strdup(client) : NULL; s->ths_total_err = 0; s->ths_output = st; s->ths_flags = flags; time(&s->ths_start); + + s->ths_id = ++tally; + LIST_INSERT_SORTED(&subscriptions, s, ths_global_link, subscription_sort); return s; @@ -315,9 +369,13 @@ subscription_create(int weight, const char *name, streaming_target_t *st, th_subscription_t * subscription_create_from_channel(channel_t *ch, unsigned int weight, const char *name, streaming_target_t *st, - int flags) + int flags, const char *hostname, + const char *username, const char *client) { - th_subscription_t *s = subscription_create(weight, name, st, flags, 0); + th_subscription_t *s; + + s = subscription_create(weight, name, st, flags, subscription_input, + hostname, username, client); s->ths_channel = ch; LIST_INSERT_HEAD(&ch->ch_subscriptions, s, ths_channel_link); @@ -349,6 +407,7 @@ subscription_create_from_channel(channel_t *ch, unsigned int weight, service_source_info_free(&si); } + notify_reload("subscriptions"); return s; } @@ -358,12 +417,18 @@ subscription_create_from_channel(channel_t *ch, unsigned int weight, */ th_subscription_t * subscription_create_from_service(service_t *t, const char *name, - streaming_target_t *st, int flags) + streaming_target_t *st, int flags, + const char *hostname, const char *username, + const char *client) { - th_subscription_t *s = subscription_create(INT32_MAX, name, st, flags, 1); + th_subscription_t *s; source_info_t si; int r; + s = subscription_create(INT32_MAX, name, st, flags, + subscription_input_direct, + hostname, username, client); + if(t->s_status != SERVICE_RUNNING) { if((r = service_start(t, INT32_MAX, 1)) != 0) { subscription_unsubscribe(s); @@ -391,6 +456,7 @@ subscription_create_from_service(service_t *t, const char *name, service_source_info_free(&si); subscription_link_service(s, t); + notify_reload("subscriptions"); return s; } @@ -473,8 +539,139 @@ subscription_dummy_join(const char *id, int first) st = calloc(1, sizeof(streaming_target_t)); streaming_target_init(st, dummy_callback, NULL, 0); - subscription_create_from_service(t, "dummy", st, 0); + subscription_create_from_service(t, "dummy", st, 0, NULL, NULL, "dummy"); tvhlog(LOG_NOTICE, "subscription", "Dummy join %s ok", id); } + +/** + * + */ +htsmsg_t * +subscription_create_msg(th_subscription_t *s) +{ + htsmsg_t *m = htsmsg_create_map(); + + htsmsg_add_u32(m, "id", s->ths_id); + htsmsg_add_u32(m, "start", s->ths_start); + htsmsg_add_u32(m, "errors", s->ths_total_err); + + const char *state; + switch(s->ths_state) { + default: + state = "Idle"; + break; + + case SUBSCRIPTION_TESTING_SERVICE: + state = "Testing"; + break; + + case SUBSCRIPTION_GOT_SERVICE: + state = "Running"; + break; + + case SUBSCRIPTION_BAD_SERVICE: + state = "Bad"; + break; + } + + + htsmsg_add_str(m, "state", state); + + if(s->ths_hostname != NULL) + htsmsg_add_str(m, "hostname", s->ths_hostname); + + if(s->ths_username != NULL) + htsmsg_add_str(m, "username", s->ths_username); + + if(s->ths_client != NULL) + htsmsg_add_str(m, "title", s->ths_client); + else if(s->ths_title != NULL) + htsmsg_add_str(m, "title", s->ths_title); + + if(s->ths_channel != NULL) + htsmsg_add_str(m, "channel", s->ths_channel->ch_name); + + if(s->ths_service != NULL) + htsmsg_add_str(m, "service", s->ths_service->s_nicename); + + return m; +} + + +static gtimer_t every_sec; + +/** + * + */ +static void +every_sec_cb(void *aux) +{ + th_subscription_t *s; + gtimer_arm(&every_sec, every_sec_cb, NULL, 1); + + LIST_FOREACH(s, &subscriptions, ths_global_link) { + int errors = s->ths_total_err; + int bw = atomic_exchange(&s->ths_bytes, 0); + + htsmsg_t *m = subscription_create_msg(s); + htsmsg_delete_field(m, "errors"); + htsmsg_add_u32(m, "errors", errors); + htsmsg_add_u32(m, "bw", bw); + htsmsg_add_u32(m, "updateEntry", 1); + notify_by_msg("subscriptions", m); + } +} + + +/** + * + */ +void +subscription_init(void) +{ + gtimer_arm(&every_sec, every_sec_cb, NULL, 1); +} + +/** + * Set speed + */ +void +subscription_set_speed ( th_subscription_t *s, int speed ) +{ + streaming_message_t *sm; + service_t *t = s->ths_service; + + if (!t) return; + + pthread_mutex_lock(&t->s_stream_mutex); + + sm = streaming_msg_create_code(SMT_SPEED, speed); + + streaming_target_deliver(s->ths_output, sm); + + pthread_mutex_unlock(&t->s_stream_mutex); +} + +/** + * Set skip + */ +void +subscription_set_skip ( th_subscription_t *s, const streaming_skip_t *skip ) +{ + streaming_message_t *sm; + service_t *t = s->ths_service; + + if (!t) return; + + pthread_mutex_lock(&t->s_stream_mutex); + + sm = streaming_msg_create(SMT_SKIP); + sm->sm_data = malloc(sizeof(streaming_skip_t)); + memcpy(sm->sm_data, skip, sizeof(streaming_skip_t)); + + streaming_target_deliver(s->ths_output, sm); + + pthread_mutex_unlock(&t->s_stream_mutex); +} diff --git a/src/subscriptions.h b/src/subscriptions.h index 52738041..28b84af8 100644 --- a/src/subscriptions.h +++ b/src/subscriptions.h @@ -19,9 +19,14 @@ #ifndef SUBSCRIPTIONS_H #define SUBSCRIPTIONS_H +extern struct th_subscription_list subscriptions; + #define SUBSCRIPTION_RAW_MPEGTS 0x1 typedef struct th_subscription { + + int ths_id; + LIST_ENTRY(th_subscription) ths_global_link; int ths_weight; @@ -46,6 +51,7 @@ typedef struct th_subscription { char *ths_title; /* display title */ time_t ths_start; /* time when subscription started */ int ths_total_err; /* total errors during entire subscription */ + int ths_bytes; // Reset every second to get aprox. bandwidth streaming_target_t ths_input; @@ -55,12 +61,23 @@ typedef struct th_subscription { streaming_message_t *ths_start_message; + char *ths_hostname; + char *ths_username; + char *ths_client; + + // Ugly ugly ugly to refer DVB code here + + LIST_ENTRY(th_subscription) ths_tdmi_link; + struct th_dvb_mux_instance *ths_tdmi; + } th_subscription_t; /** * Prototypes */ +void subscription_init(void); + void subscription_unsubscribe(th_subscription_t *s); void subscription_set_weight(th_subscription_t *s, unsigned int weight); @@ -71,16 +88,35 @@ th_subscription_t *subscription_create_from_channel(struct channel *ch, unsigned int weight, const char *name, streaming_target_t *st, - int flags); + int flags, + const char *hostname, + const char *username, + const char *client); th_subscription_t *subscription_create_from_service(struct service *t, const char *name, streaming_target_t *st, - int flags); + int flags, + const char *hostname, + const char *username, + const char *client); + +th_subscription_t *subscription_create(int weight, const char *name, + streaming_target_t *st, + int flags, st_callback_t *cb, + const char *hostname, + const char *username, + const char *client); void subscription_change_weight(th_subscription_t *s, int weight); +void subscription_set_speed + (th_subscription_t *s, int32_t speed ); + +void subscription_set_skip + (th_subscription_t *s, const streaming_skip_t *skip); + void subscription_stop(th_subscription_t *s); void subscription_unlink_service(th_subscription_t *s, int reason); @@ -89,4 +125,7 @@ void subscription_dummy_join(const char *id, int first); int subscriptions_active(void); +struct htsmsg; +struct htsmsg *subscription_create_msg(th_subscription_t *s); + #endif /* SUBSCRIPTIONS_H */ diff --git a/src/tcp.c b/src/tcp.c index cab3c5ff..fc134e4f 100644 --- a/src/tcp.c +++ b/src/tcp.c @@ -35,6 +35,7 @@ #include "tcp.h" #include "tvheadend.h" +int tcp_preferred_address_family = AF_INET; /** * @@ -182,14 +183,15 @@ tcp_write_queue(int fd, htsbuf_queue_t *q) { htsbuf_data_t *hd; int l, r = 0; + void *p; while((hd = TAILQ_FIRST(&q->hq_q)) != NULL) { - TAILQ_REMOVE(&q->hq_q, hd, hd_link); - - l = hd->hd_data_len - hd->hd_data_off; - r |= !!write(fd, hd->hd_data + hd->hd_data_off, l); - free(hd->hd_data); - free(hd); + if (!r) { + l = hd->hd_data_len - hd->hd_data_off; + p = hd->hd_data + hd->hd_data_off; + r = tvh_write(fd, p, l); + } + htsbuf_data_free(q, hd); } q->hq_size = 0; return r; @@ -343,6 +345,31 @@ tcp_read_timeout(int fd, void *buf, size_t len, int timeout) } +/** + * + */ +char * +tcp_get_ip_str(const struct sockaddr *sa, char *s, size_t maxlen) +{ + if(sa == NULL || s == NULL) + return NULL; + + switch(sa->sa_family) + { + case AF_INET: + inet_ntop(AF_INET, &(((struct sockaddr_in*)sa)->sin_addr), s, maxlen); + break; + case AF_INET6: + inet_ntop(AF_INET6, &(((struct sockaddr_in6*)sa)->sin6_addr), s, maxlen); + break; + default: + strncpy(s, "Unknown AF", maxlen); + return NULL; + } + + return s; +} + /** * */ @@ -358,8 +385,8 @@ typedef struct tcp_server_launch_t { tcp_server_callback_t *start; void *opaque; int fd; - struct sockaddr_in peer; - struct sockaddr_in self; + struct sockaddr_storage peer; + struct sockaddr_storage self; } tcp_server_launch_t; @@ -442,7 +469,7 @@ tcp_server_loop(void *aux) tsl = malloc(sizeof(tcp_server_launch_t)); tsl->start = ts->start; tsl->opaque = ts->opaque; - slen = sizeof(struct sockaddr_in); + slen = sizeof(struct sockaddr_storage); tsl->fd = accept(ts->serverfd, (struct sockaddr *)&tsl->peer, &slen); @@ -454,7 +481,7 @@ tcp_server_loop(void *aux) } - slen = sizeof(struct sockaddr_in); + slen = sizeof(struct sockaddr_storage); if(getsockname(tsl->fd, (struct sockaddr *)&tsl->self, &slen)) { close(tsl->fd); free(tsl); @@ -472,26 +499,66 @@ tcp_server_loop(void *aux) * */ void * -tcp_server_create(int port, tcp_server_callback_t *start, void *opaque) +tcp_server_create(const char *bindaddr, int port, tcp_server_callback_t *start, void *opaque) { int fd, x; struct epoll_event e; tcp_server_t *ts; - struct sockaddr_in s; + struct addrinfo hints, *res, *ressave, *use = NULL; + char *portBuf = (char*)malloc(6); int one = 1; + int zero = 0; + memset(&e, 0, sizeof(e)); - fd = tvh_socket(AF_INET, SOCK_STREAM, 0); + + snprintf(portBuf, 6, "%d", port); + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_flags = AI_PASSIVE; + if (bindaddr != NULL) + hints.ai_flags |= AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + x = getaddrinfo(bindaddr, portBuf, &hints, &res); + free(portBuf); + + if(x != 0) { + tvhlog(LOG_ERR, "tcp", "getaddrinfo: %s: %s", bindaddr != NULL ? bindaddr : "*", + x == EAI_SYSTEM ? strerror(errno) : gai_strerror(x)); + return NULL; + } + + ressave = res; + while(res) + { + if(res->ai_family == tcp_preferred_address_family) + { + use = res; + break; + } + else if(use == NULL) + { + use = res; + } + res = res->ai_next; + } + + fd = tvh_socket(use->ai_family, use->ai_socktype, use->ai_protocol); if(fd == -1) return NULL; - setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + if(use->ai_family == AF_INET6) + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(int)); - memset(&s, 0, sizeof(s)); - s.sin_family = AF_INET; - s.sin_port = htons(port); + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); - x = bind(fd, (struct sockaddr *)&s, sizeof(s)); - if(x < 0) { + x = bind(fd, use->ai_addr, use->ai_addrlen); + freeaddrinfo(ressave); + + if(x != 0) + { + tvhlog(LOG_ERR, "tcp", "bind: %s: %s", bindaddr != NULL ? bindaddr : "*", strerror(errno)); close(fd); return NULL; } @@ -503,11 +570,10 @@ tcp_server_create(int port, tcp_server_callback_t *start, void *opaque) ts->start = start; ts->opaque = opaque; - e.events = EPOLLIN; e.data.ptr = ts; - epoll_ctl(tcp_server_epoll_fd, EPOLL_CTL_ADD, fd, &e); + return ts; } @@ -515,10 +581,13 @@ tcp_server_create(int port, tcp_server_callback_t *start, void *opaque) * */ void -tcp_server_init(void) +tcp_server_init(int opt_ipv6) { pthread_t tid; + if(opt_ipv6) + tcp_preferred_address_family = AF_INET6; + tcp_server_epoll_fd = epoll_create(10); pthread_create(&tid, NULL, tcp_server_loop, NULL); } diff --git a/src/tcp.h b/src/tcp.h index e9d9534c..98649f0b 100644 --- a/src/tcp.h +++ b/src/tcp.h @@ -21,16 +21,18 @@ #include "htsbuf.h" -void tcp_server_init(void); +extern int tcp_preferred_address_family; + +void tcp_server_init(int opt_ipv6); int tcp_connect(const char *hostname, int port, char *errbuf, size_t errbufsize, int timeout); typedef void (tcp_server_callback_t)(int fd, void *opaque, - struct sockaddr_in *peer, - struct sockaddr_in *self); + struct sockaddr_storage *peer, + struct sockaddr_storage *self); -void *tcp_server_create(int port, tcp_server_callback_t *start, void *opaque); +void *tcp_server_create(const char *bindaddr, int port, tcp_server_callback_t *start, void *opaque); int tcp_read(int fd, void *buf, size_t len); @@ -44,4 +46,6 @@ int tcp_write_queue(int fd, htsbuf_queue_t *q); int tcp_read_timeout(int fd, void *buf, size_t len, int timeout); +char *tcp_get_ip_str(const struct sockaddr *sa, char *s, size_t maxlen); + #endif /* TCP_H_ */ diff --git a/src/teletext.c b/src/teletext.c index c559cac1..4b736cf2 100644 --- a/src/teletext.c +++ b/src/teletext.c @@ -469,30 +469,29 @@ teletext_input(service_t *t, elementary_stream_t *st, const uint8_t *tsb) * Swedish TV4 rundown dump (page 192) * - Starttid Titel L{ngd | 20 83 53 - ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 94 2c 2c - 23:47:05 Reklam block 00:04:15 | 8d 83 32 + Starttid Titel L{ngd | 3 3 53 + ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c + 19:00:00 TV4Nyheterna 00:14:00 | d 7 31 + | 20 20 20 + ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c + 19:14:00 Lokala nyheter 00:03:00 | 1 1 31 + 19:17:00 TV4Nyheterna 19.00 2 00:02:02 | 7 7 31 + 19:19:02 Vinjett 00:00:03 | 6 6 31 + 19:19:05 Reklam 00:02:30 | 3 3 31 + 19:21:35 Vinjett 00:00:06 | 6 6 31 + 19:21:41 LOKAL BILLBOARD 00:00:16 | 1 1 31 + 19:21:57 Lokalt v{der 00:01:00 | 1 1 31 + 19:22:57 S4NVMT1553 00:00:15 | 6 6 31 + 19:23:12 V{der 00:02:00 | 7 7 31 + 19:25:12 Trailer 00:01:00 | 2 2 31 + 19:26:12 Vinjett 00:00:03 | 6 6 31 + 19:26:15 Reklam 00:02:20 | 3 3 31 + 19:28:35 Vinjett 00:00:07 | 6 6 31 + 19:28:42 Hall} 16:9 00:00:30 | 7 7 31 + ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 14 2c 2c + | 20 20 20 +TV4 | 54 56 34 | 20 20 20 - ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 94 2c 2c - 23:47:30 HV3 BENGT OCH PETRA 00:00:03 | 20 86 32 - 23:47:34 S-6/6 : Spoons I 6 ( 00:10:23 | 20 87 32 - 23:57:57 RV3 FOLK I TRAPPAN 00:00:03 | 20 86 32 - 23:59:36 Reklam block 00:02:50 | 20 83 32 - 00:00:01 LINEUP13 BENGT OCH P 00:00:13 | 20 86 30 - 00:00:14 AABILO6123 00:00:13 | 20 86 30 - 00:00:28 S-4/6 : Allo Allo IV 00:10:28 | 20 87 30 - 00:10:57 RV3 VITA RENA PRICKA 00:00:03 | 20 86 30 - 00:11:00 LOKAL REKLAM 00:01:31 | 20 81 30 - 00:16:37 Reklam block 00:04:25 | 20 83 30 - 00:16:58 HV3 BYGGLOV 2 00:00:03 | 20 86 30 - 00:17:51 Trailer block 00:01:20 | 20 82 30 - 00:18:21 S-4/6 : Allo Allo IV 00:14:14 | 20 87 30 - 00:32:36 AABILO6123 00:00:13 | 20 86 30 - ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | 94 2c 2c - se {ven rundown.tv4.se | 83 73 65 - | 83 20 20 - 23:43 | 83 20 20 - */ @@ -521,9 +520,8 @@ teletext_rundown_copy(tt_private_t *ttp, tt_mag_t *ttm) { /* Sanity check */ if(memcmp((char *)ttm->ttm_page + 2, "Starttid", strlen("Starttid")) || - memcmp((char *)ttm->ttm_page + 11,"Titel", strlen("Titel")) || - memcmp((char *)ttm->ttm_page + 20 * 40 + 9, - "rundown.tv4.se", strlen("rundown.tv4.se"))) + memcmp((char *)ttm->ttm_page + 11, "Titel", strlen("Titel")) || + memcmp((char *)ttm->ttm_page + 21 * 40, "TV4", strlen("TV4"))) return; memcpy(ttp->ttp_rundown, ttm->ttm_page, 23 * 40); @@ -536,15 +534,20 @@ teletext_rundown_scan(service_t *t, tt_private_t *ttp) { int i; uint8_t *l; - time_t now = t->s_tt_clock, start, stop, last = 0; + time_t now = t->s_tt_clock, start, stop; th_commercial_advice_t ca; if(ttp->ttp_rundown_valid == 0) return; + if(t->s_svcname && + strcmp("TV4", t->s_svcname) && + strcmp("TV4 HD", t->s_svcname)) + return; + for(i = 0; i < 23; i++) { l = ttp->ttp_rundown + 40 * i; - if((l[1] & 0xf0) != 0x80 || !is_tt_clock(l + 32) || !is_tt_clock(l + 2)) + if((l[1] & 0xf0) != 0x00 || !is_tt_clock(l + 32) || !is_tt_clock(l + 2)) continue; if(!memcmp(l + 11, "Nyhetspuff", strlen("Nyhetspuff"))) @@ -557,8 +560,5 @@ teletext_rundown_scan(service_t *t, tt_private_t *ttp) if(start <= now && stop > now) t->s_tt_commercial_advice = ca; - - if(start > now && ca != t->s_tt_commercial_advice && last == 0) - last = start; } } diff --git a/src/timeshift.c b/src/timeshift.c new file mode 100644 index 00000000..de3766e9 --- /dev/null +++ b/src/timeshift.c @@ -0,0 +1,264 @@ +/** + * TV headend - Timeshift + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvheadend.h" +#include "streaming.h" +#include "timeshift.h" +#include "timeshift/private.h" +#include "config2.h" +#include "settings.h" +#include "atomic.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int timeshift_index = 0; + +uint32_t timeshift_enabled; +int timeshift_ondemand; +char *timeshift_path; +int timeshift_unlimited_period; +uint32_t timeshift_max_period; +int timeshift_unlimited_size; +uint64_t timeshift_max_size; + +/* + * Intialise global file manager + */ +void timeshift_init ( void ) +{ + htsmsg_t *m; + const char *str; + uint32_t u32; + + timeshift_filemgr_init(); + + /* Defaults */ + timeshift_enabled = 0; // Disabled + timeshift_ondemand = 0; // Permanent + timeshift_path = NULL; // setting dir + timeshift_unlimited_period = 0; + timeshift_max_period = 3600; // 1Hr + timeshift_unlimited_size = 0; + timeshift_max_size = 10000 * (size_t)1048576; // 10G + + /* Load settings */ + if ((m = hts_settings_load("timeshift/config"))) { + if (!htsmsg_get_u32(m, "enabled", &u32)) + timeshift_enabled = u32 ? 1 : 0; + if (!htsmsg_get_u32(m, "ondemand", &u32)) + timeshift_ondemand = u32 ? 1 : 0; + if ((str = htsmsg_get_str(m, "path"))) + timeshift_path = strdup(str); + if (!htsmsg_get_u32(m, "unlimited_period", &u32)) + timeshift_unlimited_period = u32 ? 1 : 0; + htsmsg_get_u32(m, "max_period", ×hift_max_period); + if (!htsmsg_get_u32(m, "unlimited_size", &u32)) + timeshift_unlimited_size = u32 ? 1 : 0; + if (!htsmsg_get_u32(m, "max_size", &u32)) + timeshift_max_size = 1048576LL * u32; + htsmsg_destroy(m); + } +} + +/* + * Terminate global file manager + */ +void timeshift_term ( void ) +{ + timeshift_filemgr_term(); +} + +/* + * Save settings + */ +void timeshift_save ( void ) +{ + htsmsg_t *m; + + m = htsmsg_create_map(); + htsmsg_add_u32(m, "enabled", timeshift_enabled); + htsmsg_add_u32(m, "ondemand", timeshift_ondemand); + if (timeshift_path) + htsmsg_add_str(m, "path", timeshift_path); + htsmsg_add_u32(m, "unlimited_period", timeshift_unlimited_period); + htsmsg_add_u32(m, "max_period", timeshift_max_period); + htsmsg_add_u32(m, "unlimited_size", timeshift_unlimited_size); + htsmsg_add_u32(m, "max_size", timeshift_max_size / 1048576); + + hts_settings_save(m, "timeshift/config"); +} + +/* + * Receive data + */ +static void timeshift_input + ( void *opaque, streaming_message_t *sm ) +{ + int exit = 0; + timeshift_t *ts = opaque; + + pthread_mutex_lock(&ts->state_mutex); + + /* Control */ + if (sm->sm_type == SMT_SKIP) { + if (ts->state >= TS_LIVE) + timeshift_write_skip(ts->rd_pipe.wr, sm->sm_data); + } else if (sm->sm_type == SMT_SPEED) { + if (ts->state >= TS_LIVE) + timeshift_write_speed(ts->rd_pipe.wr, sm->sm_code); + } + + else { + + /* Start */ + if (sm->sm_type == SMT_START && ts->state == TS_INIT) { + ts->state = TS_LIVE; + } + + /* Pass-thru */ + if (ts->state <= TS_LIVE) { + if (sm->sm_type == SMT_START) { + if (ts->smt_start) + streaming_start_unref(ts->smt_start); + ts->smt_start = sm->sm_data; + atomic_add(&ts->smt_start->ss_refcount, 1); + } + streaming_target_deliver2(ts->output, streaming_msg_clone(sm)); + } + + /* Check for exit */ + if (sm->sm_type == SMT_EXIT || + (sm->sm_type == SMT_STOP && sm->sm_code == 0)) + exit = 1; + + /* Record (one-off) PTS delta */ + if (sm->sm_type == SMT_PACKET && ts->pts_delta == PTS_UNSET) { + th_pkt_t *pkt = sm->sm_data; + if (pkt->pkt_pts != PTS_UNSET) + ts->pts_delta = getmonoclock() - ts_rescale(pkt->pkt_pts, 1000000); + } + + /* Buffer to disk */ + if ((ts->state > TS_LIVE) || (!ts->ondemand && (ts->state == TS_LIVE))) { + sm->sm_time = getmonoclock(); + streaming_target_deliver2(&ts->wr_queue.sq_st, sm); + } else + streaming_msg_free(sm); + + /* Exit/Stop */ + if (exit) { + timeshift_write_exit(ts->rd_pipe.wr); + ts->state = TS_EXIT; + } + } + + pthread_mutex_unlock(&ts->state_mutex); +} + +/** + * + */ +void +timeshift_destroy(streaming_target_t *pad) +{ + timeshift_t *ts = (timeshift_t*)pad; + streaming_message_t *sm; + + /* Must hold global lock */ + lock_assert(&global_lock); + + /* Ensure the threads exits */ + // Note: this is a workaround for the fact the Q might have been flushed + // in reader thread (VERY unlikely) + pthread_mutex_lock(&ts->state_mutex); + sm = streaming_msg_create(SMT_EXIT); + streaming_target_deliver2(&ts->wr_queue.sq_st, sm); + timeshift_write_exit(ts->rd_pipe.wr); + pthread_mutex_unlock(&ts->state_mutex); + + /* Wait for all threads */ + pthread_join(ts->rd_thread, NULL); + pthread_join(ts->wr_thread, NULL); + + /* Shut stuff down */ + streaming_queue_deinit(&ts->wr_queue); + + close(ts->rd_pipe.rd); + close(ts->rd_pipe.wr); + + /* Flush files */ + timeshift_filemgr_flush(ts, NULL); + + /* Release SMT_START index */ + if (ts->smt_start) + streaming_start_unref(ts->smt_start); + + if (ts->path) + free(ts->path); + free(ts); +} + +/** + * Create timeshift buffer + * + * max_period of buffer in seconds (0 = unlimited) + * max_size of buffer in bytes (0 = unlimited) + */ +streaming_target_t *timeshift_create + (streaming_target_t *out, time_t max_time) +{ + timeshift_t *ts = calloc(1, sizeof(timeshift_t)); + + /* Must hold global lock */ + lock_assert(&global_lock); + + /* Setup structure */ + TAILQ_INIT(&ts->files); + ts->output = out; + ts->path = NULL; + ts->max_time = max_time; + ts->state = TS_INIT; + ts->full = 0; + ts->vididx = -1; + ts->id = timeshift_index; + ts->ondemand = timeshift_ondemand; + ts->pts_delta = PTS_UNSET; + pthread_mutex_init(&ts->rdwr_mutex, NULL); + pthread_mutex_init(&ts->state_mutex, NULL); + + /* Initialise output */ + tvh_pipe(O_NONBLOCK, &ts->rd_pipe); + + /* Initialise input */ + streaming_queue_init(&ts->wr_queue, 0); + streaming_target_init(&ts->input, timeshift_input, ts, 0); + pthread_create(&ts->wr_thread, NULL, timeshift_writer, ts); + pthread_create(&ts->rd_thread, NULL, timeshift_reader, ts); + + /* Update index */ + timeshift_index++; + + return &ts->input; +} diff --git a/src/timeshift.h b/src/timeshift.h new file mode 100644 index 00000000..db173625 --- /dev/null +++ b/src/timeshift.h @@ -0,0 +1,48 @@ +/* + * TV headend - Timeshift + * Copyright (C) 2012 Adam Sutton + * + * 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 . + */ + +#ifndef __TVH_TIMESHIFT_H__ +#define __TVH_TIMESHIFT_H__ + +extern uint32_t timeshift_enabled; +extern int timeshift_ondemand; +extern char *timeshift_path; +extern int timeshift_unlimited_period; +extern uint32_t timeshift_max_period; +extern int timeshift_unlimited_size; +extern uint64_t timeshift_max_size; +extern uint64_t timeshift_total_size; + +typedef struct timeshift_status +{ + int full; + int64_t shift; + int64_t pts_start; + int64_t pts_end; +} timeshift_status_t; + +void timeshift_init ( void ); +void timeshift_term ( void ); +void timeshift_save ( void ); + +streaming_target_t *timeshift_create + (streaming_target_t *out, time_t max_period); + +void timeshift_destroy(streaming_target_t *pad); + +#endif /* __TVH_TIMESHIFT_H__ */ diff --git a/src/timeshift/private.h b/src/timeshift/private.h new file mode 100644 index 00000000..435eb4f8 --- /dev/null +++ b/src/timeshift/private.h @@ -0,0 +1,155 @@ +/* + * TV headend - Timeshift + * Copyright (C) 2012 Adam Sutton + * + * 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 . + */ + +#ifndef __TVH_TIMESHIFT_PRIVATE_H__ +#define __TVH_TIMESHIFT_PRIVATE_H__ + +#define TIMESHIFT_PLAY_BUF 2000000 // us to buffer in TX +#define TIMESHIFT_FILE_PERIOD 60 // number of secs in each buffer file + +/** + * Indexes of import data in the stream + */ +typedef struct timeshift_index_iframe +{ + off_t pos; ///< Position in the file + int64_t time; ///< Packet time + TAILQ_ENTRY(timeshift_index_iframe) link; ///< List entry +} timeshift_index_iframe_t; + +typedef TAILQ_HEAD(timeshift_index_iframe_list,timeshift_index_iframe) timeshift_index_iframe_list_t; + +/** + * Indexes of import data in the stream + */ +typedef struct timeshift_index_data +{ + off_t pos; ///< Position in the file + streaming_message_t *data; ///< Associated data + TAILQ_ENTRY(timeshift_index_data) link; ///< List entry +} timeshift_index_data_t; + +typedef TAILQ_HEAD(timeshift_index_data_list,timeshift_index_data) timeshift_index_data_list_t; + +/** + * Timeshift file + */ +typedef struct timeshift_file +{ + int fd; ///< Write descriptor + char *path; ///< Full path to file + + time_t time; ///< Files coarse timestamp + size_t size; ///< Current file size; + int64_t last; ///< Latest timestamp + + uint8_t bad; ///< File is broken + + int refcount; ///< Reader ref count + + timeshift_index_iframe_list_t iframes; ///< I-frame indexing + timeshift_index_data_list_t sstart; ///< Stream start messages + + TAILQ_ENTRY(timeshift_file) link; ///< List entry +} timeshift_file_t; + +typedef TAILQ_HEAD(timeshift_file_list,timeshift_file) timeshift_file_list_t; + +/** + * + */ +typedef struct timeshift { + // Note: input MUST BE FIRST in struct + streaming_target_t input; ///< Input source + streaming_target_t *output; ///< Output dest + + int id; ///< Reference number + char *path; ///< Directory containing buffer + time_t max_time; ///< Maximum period to shift + int ondemand; ///< Whether this is an on-demand timeshift + int64_t pts_delta; ///< Delta between system clock and PTS + + enum { + TS_INIT, + TS_EXIT, + TS_LIVE, + TS_PAUSE, + TS_PLAY, + } state; ///< Play state + pthread_mutex_t state_mutex; ///< Protect state changes + uint8_t full; ///< Buffer is full + + streaming_start_t *smt_start; ///< Current stream makeup + + streaming_queue_t wr_queue; ///< Writer queue + pthread_t wr_thread; ///< Writer thread + + pthread_t rd_thread; ///< Reader thread + th_pipe_t rd_pipe; ///< Message passing to reader + + pthread_mutex_t rdwr_mutex; ///< Buffer protection + timeshift_file_list_t files; ///< List of files + + int vididx; ///< Index of (current) video stream + +} timeshift_t; + +/* + * Write functions + */ +ssize_t timeshift_write_start ( int fd, int64_t time, streaming_start_t *ss ); +ssize_t timeshift_write_sigstat ( int fd, int64_t time, signal_status_t *ss ); +ssize_t timeshift_write_packet ( int fd, int64_t time, th_pkt_t *pkt ); +ssize_t timeshift_write_mpegts ( int fd, int64_t time, void *data ); +ssize_t timeshift_write_skip ( int fd, streaming_skip_t *skip ); +ssize_t timeshift_write_speed ( int fd, int speed ); +ssize_t timeshift_write_stop ( int fd, int code ); +ssize_t timeshift_write_exit ( int fd ); +ssize_t timeshift_write_eof ( int fd ); + +void timeshift_writer_flush ( timeshift_t *ts ); + +/* + * Threads + */ +void *timeshift_reader ( void *p ); +void *timeshift_writer ( void *p ); + +/* + * File management + */ +void timeshift_filemgr_init ( void ); +void timeshift_filemgr_term ( void ); +int timeshift_filemgr_makedirs ( int ts_index, char *buf, size_t len ); + +timeshift_file_t *timeshift_filemgr_get + ( timeshift_t *ts, int create ); +timeshift_file_t *timeshift_filemgr_oldest + ( timeshift_t *ts ); +timeshift_file_t *timeshift_filemgr_newest + ( timeshift_t *ts ); +timeshift_file_t *timeshift_filemgr_prev + ( timeshift_file_t *ts, int *end, int keep ); +timeshift_file_t *timeshift_filemgr_next + ( timeshift_file_t *ts, int *end, int keep ); +void timeshift_filemgr_remove + ( timeshift_t *ts, timeshift_file_t *tsf, int force ); +void timeshift_filemgr_flush ( timeshift_t *ts, timeshift_file_t *end ); +void timeshift_filemgr_close ( timeshift_file_t *tsf ); + +#endif /* __TVH_TIMESHIFT_PRIVATE_H__ */ diff --git a/src/timeshift/timeshift_filemgr.c b/src/timeshift/timeshift_filemgr.c new file mode 100644 index 00000000..c7debb9b --- /dev/null +++ b/src/timeshift/timeshift_filemgr.c @@ -0,0 +1,371 @@ +/** + * TV headend - Timeshift File Manager + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvheadend.h" +#include "streaming.h" +#include "timeshift.h" +#include "timeshift/private.h" +#include "config2.h" +#include "settings.h" +#include "atomic.h" + +static int timeshift_reaper_run; +static timeshift_file_list_t timeshift_reaper_list; +static pthread_t timeshift_reaper_thread; +static pthread_mutex_t timeshift_reaper_lock; +static pthread_cond_t timeshift_reaper_cond; + +uint64_t timeshift_total_size; + +/* ************************************************************************** + * File reaper thread + * *************************************************************************/ + +static void* timeshift_reaper_callback ( void *p ) +{ + char *dpath; + timeshift_file_t *tsf; + timeshift_index_iframe_t *ti; + timeshift_index_data_t *tid; + streaming_message_t *sm; + pthread_mutex_lock(×hift_reaper_lock); + while (timeshift_reaper_run) { + + /* Get next */ + tsf = TAILQ_FIRST(×hift_reaper_list); + if (!tsf) { + pthread_cond_wait(×hift_reaper_cond, ×hift_reaper_lock); + continue; + } + TAILQ_REMOVE(×hift_reaper_list, tsf, link); + pthread_mutex_unlock(×hift_reaper_lock); + + tvhtrace("timeshift", "remove file %s", tsf->path); + + /* Remove */ + unlink(tsf->path); + dpath = dirname(tsf->path); + if (rmdir(dpath) == -1) + if (errno != ENOTEMPTY) + tvhlog(LOG_ERR, "timeshift", "failed to remove %s [e=%s]", + dpath, strerror(errno)); + + /* Free memory */ + while ((ti = TAILQ_FIRST(&tsf->iframes))) { + TAILQ_REMOVE(&tsf->iframes, ti, link); + free(ti); + } + while ((tid = TAILQ_FIRST(&tsf->sstart))) { + TAILQ_REMOVE(&tsf->sstart, tid, link); + sm = tid->data; + streaming_msg_free(sm); + free(tid); + } + free(tsf->path); + free(tsf); + } + pthread_mutex_unlock(×hift_reaper_lock); + tvhtrace("timeshift", "reaper thread exit"); + return NULL; +} + +static void timeshift_reaper_remove ( timeshift_file_t *tsf ) +{ + tvhtrace("timeshift", "queue file for removal %s", tsf->path); + pthread_mutex_lock(×hift_reaper_lock); + TAILQ_INSERT_TAIL(×hift_reaper_list, tsf, link); + pthread_cond_signal(×hift_reaper_cond); + pthread_mutex_unlock(×hift_reaper_lock); +} + +/* ************************************************************************** + * File Handling + * *************************************************************************/ + +/* + * Get root directory + * + * TODO: should this be fixed on startup? + */ +static void timeshift_filemgr_get_root ( char *buf, size_t len ) +{ + const char *path = timeshift_path; + if (!path || !*path) { + path = hts_settings_get_root(); + snprintf(buf, len, "%s/timeshift/buffer", path); + } else { + snprintf(buf, len, "%s/buffer", path); + } +} + +/* + * Create timeshift directories (for a given instance) + */ +int timeshift_filemgr_makedirs ( int index, char *buf, size_t len ) +{ + timeshift_filemgr_get_root(buf, len); + snprintf(buf+strlen(buf), len-strlen(buf), "/%d", index); + return makedirs(buf, 0700); +} + +/* + * Close file + */ +void timeshift_filemgr_close ( timeshift_file_t *tsf ) +{ + ssize_t r = timeshift_write_eof(tsf->fd); + if (r > 0) + { + tsf->size += r; + atomic_add_u64(×hift_total_size, r); + } + close(tsf->fd); + tsf->fd = -1; +} + +/* + * Remove file + */ +void timeshift_filemgr_remove + ( timeshift_t *ts, timeshift_file_t *tsf, int force ) +{ + if (tsf->fd != -1) + close(tsf->fd); + tvhlog(LOG_DEBUG, "timeshift", "ts %d remove %s", ts->id, tsf->path); + TAILQ_REMOVE(&ts->files, tsf, link); + atomic_add_u64(×hift_total_size, -tsf->size); + timeshift_reaper_remove(tsf); +} + +/* + * Flush all files + */ +void timeshift_filemgr_flush ( timeshift_t *ts, timeshift_file_t *end ) +{ + timeshift_file_t *tsf; + while ((tsf = TAILQ_FIRST(&ts->files))) { + if (tsf == end) break; + timeshift_filemgr_remove(ts, tsf, 1); + } +} + +/* + * Get current / new file + */ +timeshift_file_t *timeshift_filemgr_get ( timeshift_t *ts, int create ) +{ + int fd; + struct timespec tp; + timeshift_file_t *tsf_tl, *tsf_hd, *tsf_tmp; + timeshift_index_data_t *ti; + char path[512]; + time_t time; + + /* Return last file */ + if (!create) + return timeshift_filemgr_newest(ts); + + /* No space */ + if (ts->full) + return NULL; + + /* Store to file */ + clock_gettime(CLOCK_MONOTONIC_COARSE, &tp); + time = tp.tv_sec / TIMESHIFT_FILE_PERIOD; + tsf_tl = TAILQ_LAST(&ts->files, timeshift_file_list); + if (!tsf_tl || tsf_tl->time != time) { + tsf_hd = TAILQ_FIRST(&ts->files); + + /* Close existing */ + if (tsf_tl && tsf_tl->fd != -1) + timeshift_filemgr_close(tsf_tl); + + /* Check period */ + if (ts->max_time && tsf_hd && tsf_tl) { + time_t d = (tsf_tl->time - tsf_hd->time) * TIMESHIFT_FILE_PERIOD; + if (d > (ts->max_time+5)) { + if (!tsf_hd->refcount) { + timeshift_filemgr_remove(ts, tsf_hd, 0); + tsf_hd = NULL; + } else { + tvhlog(LOG_DEBUG, "timeshift", "ts %d buffer full", ts->id); + ts->full = 1; + } + } + } + + /* Check size */ + if (!timeshift_unlimited_size && + atomic_pre_add_u64(×hift_total_size, 0) >= timeshift_max_size) { + + /* Remove the last file (if we can) */ + if (tsf_hd && !tsf_hd->refcount) { + timeshift_filemgr_remove(ts, tsf_hd, 0); + + /* Full */ + } else { + tvhlog(LOG_DEBUG, "timeshift", "ts %d buffer full", ts->id); + ts->full = 1; + } + } + + /* Create new file */ + tsf_tmp = NULL; + if (!ts->full) { + + /* Create directories */ + if (!ts->path) { + if (timeshift_filemgr_makedirs(ts->id, path, sizeof(path))) + return NULL; + ts->path = strdup(path); + } + + /* Create File */ + snprintf(path, sizeof(path), "%s/tvh-%"PRItime_t, ts->path, time); + tvhtrace("timeshift", "ts %d create file %s", ts->id, path); + if ((fd = open(path, O_WRONLY | O_CREAT, 0600)) > 0) { + tsf_tmp = calloc(1, sizeof(timeshift_file_t)); + tsf_tmp->time = time; + tsf_tmp->fd = fd; + tsf_tmp->path = strdup(path); + tsf_tmp->refcount = 0; + tsf_tmp->last = getmonoclock(); + TAILQ_INIT(&tsf_tmp->iframes); + TAILQ_INIT(&tsf_tmp->sstart); + TAILQ_INSERT_TAIL(&ts->files, tsf_tmp, link); + + /* Copy across last start message */ + if (tsf_tl && (ti = TAILQ_LAST(&tsf_tl->sstart, timeshift_index_data_list))) { + tvhtrace("timeshift", "ts %d copy smt_start to new file", + ts->id); + timeshift_index_data_t *ti2 = calloc(1, sizeof(timeshift_index_data_t)); + ti2->data = streaming_msg_clone(ti->data); + TAILQ_INSERT_TAIL(&tsf_tmp->sstart, ti2, link); + } + } + } + tsf_tl = tsf_tmp; + } + + if (tsf_tl) + tsf_tl->refcount++; + return tsf_tl; +} + +timeshift_file_t *timeshift_filemgr_next + ( timeshift_file_t *tsf, int *end, int keep ) +{ + timeshift_file_t *nxt = TAILQ_NEXT(tsf, link); + if (!nxt && end) *end = 1; + if (!nxt && keep) return tsf; + tsf->refcount--; + if (nxt) + nxt->refcount++; + return nxt; +} + +timeshift_file_t *timeshift_filemgr_prev + ( timeshift_file_t *tsf, int *end, int keep ) +{ + timeshift_file_t *nxt = TAILQ_PREV(tsf, timeshift_file_list, link); + if (!nxt && end) *end = 1; + if (!nxt && keep) return tsf; + tsf->refcount--; + if (nxt) + nxt->refcount++; + return nxt; +} + +/* + * Get the oldest file + */ +timeshift_file_t *timeshift_filemgr_oldest ( timeshift_t *ts ) +{ + timeshift_file_t *tsf = TAILQ_FIRST(&ts->files); + if (tsf) + tsf->refcount++; + return tsf; +} + +/* + * Get the newest file + */ +timeshift_file_t *timeshift_filemgr_newest ( timeshift_t *ts ) +{ + timeshift_file_t *tsf = TAILQ_LAST(&ts->files, timeshift_file_list); + if (tsf) + tsf->refcount++; + return tsf; +} + +/* ************************************************************************** + * Setup / Teardown + * *************************************************************************/ + +/* + * Initialise global structures + */ +void timeshift_filemgr_init ( void ) +{ + char path[512]; + + /* Try to remove any rubbish left from last run */ + timeshift_filemgr_get_root(path, sizeof(path)); + rmtree(path); + + /* Size processing */ + timeshift_total_size = 0; + + /* Start the reaper thread */ + timeshift_reaper_run = 1; + pthread_mutex_init(×hift_reaper_lock, NULL); + pthread_cond_init(×hift_reaper_cond, NULL); + TAILQ_INIT(×hift_reaper_list); + pthread_create(×hift_reaper_thread, NULL, + timeshift_reaper_callback, NULL); +} + +/* + * Terminate + */ +void timeshift_filemgr_term ( void ) +{ + char path[512]; + + /* Wait for thread */ + pthread_mutex_lock(×hift_reaper_lock); + timeshift_reaper_run = 0; + pthread_cond_signal(×hift_reaper_cond); + pthread_mutex_unlock(×hift_reaper_lock); + pthread_join(timeshift_reaper_thread, NULL); + + /* Remove the lot */ + timeshift_filemgr_get_root(path, sizeof(path)); + rmtree(path); +} + + diff --git a/src/timeshift/timeshift_reader.c b/src/timeshift/timeshift_reader.c new file mode 100644 index 00000000..f2a3b6bc --- /dev/null +++ b/src/timeshift/timeshift_reader.c @@ -0,0 +1,812 @@ +/** + * TV headend - Timeshift Reader + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvheadend.h" +#include "streaming.h" +#include "timeshift.h" +#include "timeshift/private.h" +#include "atomic.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ************************************************************************** + * File Reading + * *************************************************************************/ + +static ssize_t _read_pktbuf ( int fd, pktbuf_t **pktbuf ) +{ + ssize_t r, cnt = 0; + size_t sz; + + /* Size */ + r = read(fd, &sz, sizeof(sz)); + if (r < 0) return -1; + if (r != sizeof(sz)) return 0; + cnt += r; + + /* Empty */ + if (!sz) { + *pktbuf = NULL; + return cnt; + } + + /* Data */ + *pktbuf = pktbuf_alloc(NULL, sz); + r = read(fd, (*pktbuf)->pb_data, sz); + if (r != sz) { + free((*pktbuf)->pb_data); + free(*pktbuf); + return r < 0 ? -1 : 0; + } + cnt += r; + + return cnt; +} + + +static ssize_t _read_msg ( int fd, streaming_message_t **sm ) +{ + ssize_t r, cnt = 0; + size_t sz; + streaming_message_type_t type; + int64_t time; + void *data; + int code; + + /* Clear */ + *sm = NULL; + + /* Size */ + r = read(fd, &sz, sizeof(sz)); + if (r < 0) return -1; + if (r != sizeof(sz)) return 0; + cnt += r; + + /* EOF */ + if (sz == 0) return cnt; + + /* Type */ + r = read(fd, &type, sizeof(type)); + if (r < 0) return -1; + if (r != sizeof(type)) return 0; + cnt += r; + + /* Time */ + r = read(fd, &time, sizeof(time)); + if (r < 0) return -1; + if (r != sizeof(time)) return 0; + cnt += r; + + /* Adjust size */ + sz -= sizeof(type) + sizeof(time); + cnt += sz; + + /* Standard messages */ + switch (type) { + + /* Unhandled */ + case SMT_START: + case SMT_NOSTART: + case SMT_SERVICE_STATUS: + return -1; + break; + + /* Code */ + case SMT_STOP: + case SMT_EXIT: + case SMT_SPEED: + if (sz != sizeof(code)) return -1; + r = read(fd, &code, sz); + if (r != sz) { + if (r < 0) return -1; + return 0; + } + *sm = streaming_msg_create_code(type, code); + break; + + /* Data */ + case SMT_SKIP: + case SMT_SIGNAL_STATUS: + case SMT_MPEGTS: + case SMT_PACKET: + data = malloc(sz); + r = read(fd, data, sz); + if (r != sz) { + free(data); + if (r < 0) return -1; + return 0; + } + if (type == SMT_PACKET) { + th_pkt_t *pkt = data; + pkt->pkt_payload = pkt->pkt_header = NULL; + pkt->pkt_refcount = 0; + *sm = streaming_msg_create_pkt(pkt); + r = _read_pktbuf(fd, &pkt->pkt_header); + if (r < 0) { + streaming_msg_free(*sm); + return r; + } + cnt += r; + r = _read_pktbuf(fd, &pkt->pkt_payload); + if (r < 0) { + streaming_msg_free(*sm); + return r; + } + cnt += r; + } else { + *sm = streaming_msg_create_data(type, data); + } + (*sm)->sm_time = time; + break; + + default: + return -1; + } + + /* OK */ + return cnt; +} + +/* ************************************************************************** + * Utilities + * *************************************************************************/ + +static streaming_message_t *_timeshift_find_sstart + ( timeshift_file_t *tsf, int64_t time ) +{ + timeshift_index_data_t *ti; + + /* Find the SMT_START message that relates (comes before) the given time */ + ti = TAILQ_LAST(&tsf->sstart, timeshift_index_data_list); + while (ti && ti->data->sm_time > time) + ti = TAILQ_PREV(ti, timeshift_index_data_list, link); + + return ti ? ti->data : NULL; +} + +static timeshift_index_iframe_t *_timeshift_first_frame + ( timeshift_t *ts ) +{ + int end; + timeshift_index_iframe_t *tsi = NULL; + timeshift_file_t *tsf = timeshift_filemgr_oldest(ts); + while (tsf && !tsi) { + if (!(tsi = TAILQ_FIRST(&tsf->iframes))) { + tsf = timeshift_filemgr_next(tsf, &end, 0); + } + } + if (tsf) + tsf->refcount--; + return tsi; +} + +static timeshift_index_iframe_t *_timeshift_last_frame + ( timeshift_t *ts ) +{ + int end; + timeshift_index_iframe_t *tsi = NULL; + timeshift_file_t *tsf = timeshift_filemgr_get(ts, 0); + while (tsf && !tsi) { + if (!(tsi = TAILQ_LAST(&tsf->iframes, timeshift_index_iframe_list))) { + tsf = timeshift_filemgr_prev(tsf, &end, 0); + } + } + if (tsf) + tsf->refcount--; + return tsi; +} + +static int _timeshift_skip + ( timeshift_t *ts, int64_t req_time, int64_t cur_time, + timeshift_file_t *cur_file, timeshift_file_t **new_file, + timeshift_index_iframe_t **iframe ) +{ + timeshift_index_iframe_t *tsi = *iframe; + timeshift_file_t *tsf = cur_file; + int64_t sec = req_time / (1000000 * TIMESHIFT_FILE_PERIOD); + int back = (req_time < cur_time) ? 1 : 0; + int end = 0; + + /* Hold local ref */ + if (cur_file) + cur_file->refcount++; + + /* Coarse search */ + if (!tsi) { + while (tsf && !end) { + if (back) { + if ((tsf->time <= sec) && + (tsi = TAILQ_LAST(&tsf->iframes, timeshift_index_iframe_list))) + break; + tsf = timeshift_filemgr_prev(tsf, &end, 1); + } else { + if ((tsf->time >= sec) && + (tsi = TAILQ_FIRST(&tsf->iframes))) + break; + tsf = timeshift_filemgr_next(tsf, &end, 0); + } + tsi = NULL; + } + } + + /* Fine search */ + if (back) { + while (!end && tsf && tsi && (tsi->time > req_time)) { + tsi = TAILQ_PREV(tsi, timeshift_index_iframe_list, link); + while (!end && tsf && !tsi) { + if ((tsf = timeshift_filemgr_prev(tsf, &end, 1))) + tsi = TAILQ_LAST(&tsf->iframes, timeshift_index_iframe_list); + } + } + } else { + while (!end && tsf && tsi && (tsi->time < req_time)) { + tsi = TAILQ_NEXT(tsi, link); + while (!end && tsf && !tsi) { + if ((tsf = timeshift_filemgr_next(tsf, &end, 0))) + tsi = TAILQ_FIRST(&tsf->iframes); + } + } + } + + /* End */ + if (!tsf || !tsi) + end = 1; + + /* Find start/end of buffer */ + if (end) { + if (back) { + tsf = timeshift_filemgr_oldest(ts); + tsi = NULL; + while (tsf && !tsi) { + if (!(tsi = TAILQ_FIRST(&tsf->iframes))) + tsf = timeshift_filemgr_next(tsf, &end, 0); + } + end = -1; + } else { + tsf = timeshift_filemgr_get(ts, 0); + tsi = NULL; + while (tsf && !tsi) { + if (!(tsi = TAILQ_LAST(&tsf->iframes, timeshift_index_iframe_list))) + tsf = timeshift_filemgr_prev(tsf, &end, 0); + } + end = 1; + } + } + + if (cur_file) + cur_file->refcount--; + + /* Done */ + *new_file = tsf; + *iframe = tsi; + return end; +} + +/* + * Output packet + */ +static int _timeshift_read + ( timeshift_t *ts, timeshift_file_t **cur_file, off_t *cur_off, int *fd, + streaming_message_t **sm, int *wait ) +{ + if (*cur_file) { + + /* Open file */ + if (*fd == -1) { + tvhtrace("timeshift", "ts %d open file %s", + ts->id, (*cur_file)->path); + *fd = open((*cur_file)->path, O_RDONLY); + } + tvhtrace("timeshift", "ts %d seek to %"PRIoff_t, ts->id, *cur_off); + lseek(*fd, *cur_off, SEEK_SET); + + /* Read msg */ + ssize_t r = _read_msg(*fd, sm); + if (r < 0) { + streaming_message_t *e = streaming_msg_create_code(SMT_STOP, SM_CODE_UNDEFINED_ERROR); + streaming_target_deliver2(ts->output, e); + tvhlog(LOG_ERR, "timeshift", "ts %d could not read buffer", ts->id); + return -1; + } + tvhtrace("timeshift", "ts %d read msg %p (%"PRIssize_t")", + ts->id, *sm, r); + + /* Incomplete */ + if (r == 0) { + lseek(*fd, *cur_off, SEEK_SET); + return 0; + } + + /* Update */ + *cur_off += r; + + /* Special case - EOF */ + if (r == sizeof(size_t) || *cur_off > (*cur_file)->size) { + close(*fd); + *fd = -1; + pthread_mutex_lock(&ts->rdwr_mutex); + *cur_file = timeshift_filemgr_next(*cur_file, NULL, 0); + pthread_mutex_unlock(&ts->rdwr_mutex); + *cur_off = 0; // reset + *wait = 0; + + /* Check SMT_START index */ + } else { + streaming_message_t *ssm = _timeshift_find_sstart(*cur_file, (*sm)->sm_time); + if (ssm && ssm->sm_data != ts->smt_start) { + streaming_target_deliver2(ts->output, streaming_msg_clone(ssm)); + if (ts->smt_start) + streaming_start_unref(ts->smt_start); + ts->smt_start = ssm->sm_data; + atomic_add(&ts->smt_start->ss_refcount, 1); + } + } + } + return 0; +} + + +/* + * Flush all data to live + */ +static int _timeshift_flush_to_live + ( timeshift_t *ts, timeshift_file_t **cur_file, off_t *cur_off, int *fd, + streaming_message_t **sm, int *wait ) +{ + time_t pts = 0; + while (*cur_file) { + if (_timeshift_read(ts, cur_file, cur_off, fd, sm, wait) == -1) + return -1; + if (!*sm) break; + if ((*sm)->sm_type == SMT_PACKET) { + pts = ((th_pkt_t*)(*sm)->sm_data)->pkt_pts; + tvhlog(LOG_DEBUG, "timeshift", "ts %d deliver %"PRId64" pts=%"PRItime_t, + ts->id, (*sm)->sm_time, pts); + } + streaming_target_deliver2(ts->output, *sm); + *sm = NULL; + } + return 0; +} + +/* ************************************************************************** + * Thread + * *************************************************************************/ + +/* + * Timeshift thread + */ +void *timeshift_reader ( void *p ) +{ + timeshift_t *ts = p; + int efd, nfds, end, fd = -1, run = 1, wait = -1; + timeshift_file_t *cur_file = NULL; + off_t cur_off = 0; + int cur_speed = 100, keyframe_mode = 0; + int64_t pause_time = 0, play_time = 0, last_time = 0; + int64_t now, deliver, skip_time = 0; + streaming_message_t *sm = NULL, *ctrl = NULL; + timeshift_index_iframe_t *tsi = NULL; + streaming_skip_t *skip = NULL; + time_t last_status = 0; + + /* Poll */ + struct epoll_event ev = { 0 }; + efd = epoll_create(1); + ev.events = EPOLLIN; + ev.data.fd = ts->rd_pipe.rd; + epoll_ctl(efd, EPOLL_CTL_ADD, ev.data.fd, &ev); + + /* Output */ + while (run) { + + // Note: Previously we allowed unlimited wait, but we now must wake periodically + // to output status message + if (wait < 0 || wait > 1000) + wait = 1000; + + /* Wait for data */ + if(wait) + nfds = epoll_wait(efd, &ev, 1, wait); + else + nfds = 0; + wait = -1; + end = 0; + skip = NULL; + now = getmonoclock(); + + /* Control */ + pthread_mutex_lock(&ts->state_mutex); + if (nfds == 1) { + if (_read_msg(ev.data.fd, &ctrl) > 0) { + + /* Exit */ + if (ctrl->sm_type == SMT_EXIT) { + tvhtrace("timeshift", "ts %d read exit request", ts->id); + run = 0; + streaming_msg_free(ctrl); + ctrl = NULL; + + /* Speed */ + } else if (ctrl->sm_type == SMT_SPEED) { + int speed = ctrl->sm_code; + int keyframe; + + /* Bound it */ + if (speed > 3200) speed = 3200; + if (speed < -3200) speed = -3200; + + /* Ignore negative */ + if (ts->ondemand && (speed < 0)) + speed = 0; + + /* Process */ + if (cur_speed != speed) { + + /* Live playback */ + if (ts->state == TS_LIVE) { + + /* Reject */ + if (speed >= 100) { + tvhlog(LOG_DEBUG, "timeshift", "ts %d reject 1x+ in live mode", + ts->id); + speed = 100; + + /* Set position */ + } else { + tvhlog(LOG_DEBUG, "timeshift", "ts %d enter timeshift mode", + ts->id); + timeshift_writer_flush(ts); + pthread_mutex_lock(&ts->rdwr_mutex); + if ((cur_file = timeshift_filemgr_get(ts, 1))) { + cur_off = cur_file->size; + pause_time = cur_file->last; + last_time = pause_time; + } + pthread_mutex_unlock(&ts->rdwr_mutex); + } + + /* Buffer playback */ + } else if (ts->state == TS_PLAY) { + pause_time = last_time; + + /* Paused */ + } else { + } + + /* Check keyframe mode */ + keyframe = (speed < 0) || (speed > 400); + if (keyframe != keyframe_mode) { + tvhlog(LOG_DEBUG, "timeshift", "using keyframe mode? %s", + keyframe ? "yes" : "no"); + keyframe_mode = keyframe; + if (keyframe) { + tsi = NULL; + } + } + + /* Update */ + play_time = getmonoclock(); + cur_speed = speed; + if (speed != 100 || ts->state != TS_LIVE) + ts->state = speed == 0 ? TS_PAUSE : TS_PLAY; + tvhlog(LOG_DEBUG, "timeshift", "ts %d change speed %d", + ts->id, speed); + } + + /* Send on the message */ + ctrl->sm_code = speed; + streaming_target_deliver2(ts->output, ctrl); + ctrl = NULL; + + /* Skip/Seek */ + } else if (ctrl->sm_type == SMT_SKIP) { + skip = ctrl->sm_data; + switch (skip->type) { + case SMT_SKIP_LIVE: + if (ts->state != TS_LIVE) { + + /* Reset */ + if (ts->full) { + pthread_mutex_lock(&ts->rdwr_mutex); + timeshift_filemgr_flush(ts, NULL); + ts->full = 0; + pthread_mutex_unlock(&ts->rdwr_mutex); + } + + /* Release */ + if (sm) + streaming_msg_free(sm); + + /* Find end */ + skip_time = 0x7fffffffffffffffLL; + // TODO: change this sometime! + } + break; + + case SMT_SKIP_ABS_TIME: + if (ts->pts_delta == PTS_UNSET) { + tvhlog(LOG_ERR, "timeshift", "ts %d abs skip not possible no PTS delta", ts->id); + skip = NULL; + break; + } + /* -fallthrough */ + case SMT_SKIP_REL_TIME: + + /* Convert */ + skip_time = ts_rescale(skip->time, 1000000); + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip %"PRId64" requested %"PRId64, ts->id, skip_time, skip->time); + + /* Live playback (stage1) */ + if (ts->state == TS_LIVE) { + pthread_mutex_lock(&ts->rdwr_mutex); + if ((cur_file = timeshift_filemgr_get(ts, !ts->ondemand))) { + cur_off = cur_file->size; + last_time = cur_file->last; + } else { + tvhlog(LOG_ERR, "timeshift", "ts %d failed to get current file", ts->id); + skip = NULL; + } + pthread_mutex_unlock(&ts->rdwr_mutex); + } + + /* May have failed */ + if (skip) { + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip last_time %"PRId64" pts_delta %"PRId64, + ts->id, last_time, ts->pts_delta); + skip_time += (skip->type == SMT_SKIP_ABS_TIME) ? ts->pts_delta : last_time; + + /* Live (stage2) */ + if (ts->state == TS_LIVE) { + if (skip_time >= now) { + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip ignored, already live", ts->id); + skip = NULL; + } else { + ts->state = TS_PLAY; + } + } + } + + /* OK */ + if (skip) { + + /* Adjust time */ + play_time = now; + pause_time = skip_time; + tsi = NULL; + + /* Clear existing packet */ + if (sm) + streaming_msg_free(sm); + sm = NULL; + } + break; + default: + tvhlog(LOG_ERR, "timeshift", "ts %d invalid/unsupported skip type: %d", ts->id, skip->type); + skip = NULL; + break; + } + + /* Error */ + if (!skip) { + ((streaming_skip_t*)ctrl->sm_data)->type = SMT_SKIP_ERROR; + streaming_target_deliver2(ts->output, ctrl); + ctrl = NULL; + } + + /* Ignore */ + } else { + streaming_msg_free(ctrl); + ctrl = NULL; + } + } + } + + /* Status message */ + if (now >= (last_status + 1000000)) { + streaming_message_t *tsm; + timeshift_status_t *status; + timeshift_index_iframe_t *fst, *lst; + status = calloc(1, sizeof(timeshift_status_t)); + fst = _timeshift_first_frame(ts); + lst = _timeshift_last_frame(ts); + status->full = ts->full; + status->shift = ts->state <= TS_LIVE ? 0 : ts_rescale_i(now - last_time, 1000000); + if (lst && fst && lst != fst && ts->pts_delta != PTS_UNSET) { + status->pts_start = ts_rescale_i(fst->time - ts->pts_delta, 1000000); + status->pts_end = ts_rescale_i(lst->time - ts->pts_delta, 1000000); + } else { + status->pts_start = PTS_UNSET; + status->pts_end = PTS_UNSET; + } + tsm = streaming_msg_create_data(SMT_TIMESHIFT_STATUS, status); + streaming_target_deliver2(ts->output, tsm); + last_status = now; + } + + /* Done */ + if (!run || !cur_file || ((ts->state != TS_PLAY && !skip))) { + pthread_mutex_unlock(&ts->state_mutex); + continue; + } + + /* Calculate delivery time */ + deliver = (now - play_time) + TIMESHIFT_PLAY_BUF; + deliver = (deliver * cur_speed) / 100; + deliver = (deliver + pause_time); + + /* Determine next packet */ + if (!sm) { + + /* Rewind or Fast forward (i-frame only) */ + if (skip || keyframe_mode) { + timeshift_file_t *tsf = NULL; + int64_t req_time; + + /* Time */ + if (!skip) + req_time = last_time + ((cur_speed < 0) ? -1 : 1); + else + req_time = skip_time; + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip to %"PRId64" from %"PRId64, ts->id, req_time, last_time); + + /* Find */ + pthread_mutex_lock(&ts->rdwr_mutex); + end = _timeshift_skip(ts, req_time, last_time, + cur_file, &tsf, &tsi); + pthread_mutex_unlock(&ts->rdwr_mutex); + if (tsi) + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip found pkt @ %"PRId64, ts->id, tsi->time); + + /* File changed (close) */ + if ((tsf != cur_file) && (fd != -1)) { + close(fd); + fd = -1; + } + + /* Position */ + if (cur_file) + cur_file->refcount--; + cur_file = tsf; + if (tsi) + cur_off = tsi->pos; + else + cur_off = 0; + } + + /* Find packet */ + if (_timeshift_read(ts, &cur_file, &cur_off, &fd, &sm, &wait) == -1) { + pthread_mutex_unlock(&ts->state_mutex); + break; + } + } + + /* Send skip response */ + if (skip) { + if (sm && sm->sm_type == SMT_PACKET) { + th_pkt_t *pkt = sm->sm_data; + skip->time = pkt->pkt_pts; + skip->type = SMT_SKIP_ABS_TIME; + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip to %"PRId64" ok", ts->id, skip->time); + } else { + /* Report error */ + skip->type = SMT_SKIP_ERROR; + skip = NULL; + tvhlog(LOG_DEBUG, "timeshift", "ts %d skip failed", ts->id); + } + streaming_target_deliver2(ts->output, ctrl); + ctrl = NULL; + } + + /* Deliver */ + if (sm && (skip || + (((cur_speed < 0) && (sm->sm_time >= deliver)) || + ((cur_speed > 0) && (sm->sm_time <= deliver))))) { + +#if ENABLE_TRACE + if (skip) + { + time_t pts = 0; + int64_t delta = now - sm->sm_time; + if (sm->sm_type == SMT_PACKET) + pts = ((th_pkt_t*)sm->sm_data)->pkt_pts; + tvhtrace("timeshift", "ts %d deliver %"PRId64" pts=%"PRItime_t " shift=%"PRIu64, + ts->id, sm->sm_time, pts, delta); + } +#endif + streaming_target_deliver2(ts->output, sm); + last_time = sm->sm_time; + sm = NULL; + wait = 0; + } else if (sm) { + if (cur_speed > 0) + wait = (sm->sm_time - deliver) / 1000; + else + wait = (deliver - sm->sm_time) / 1000; + if (wait == 0) wait = 1; + tvhtrace("timeshift", "ts %d wait %d", + ts->id, wait); + } + + /* Terminate */ + if (!cur_file || end != 0) { + if (!end) + end = (cur_speed > 0) ? 1 : -1; + + /* Back to live (unless buffer is full) */ + if (end == 1 && !ts->full) { + tvhlog(LOG_DEBUG, "timeshift", "ts %d eob revert to live mode", ts->id); + ts->state = TS_LIVE; + cur_speed = 100; + ctrl = streaming_msg_create_code(SMT_SPEED, cur_speed); + streaming_target_deliver2(ts->output, ctrl); + + /* Flush timeshift buffer to live */ + if (_timeshift_flush_to_live(ts, &cur_file, &cur_off, &fd, &sm, &wait) == -1) + break; + + /* Close file (if open) */ + if (fd != -1) { + close(fd); + fd = -1; + } + + /* Flush ALL files */ + if (ts->ondemand) + timeshift_filemgr_flush(ts, NULL); + + /* Pause */ + } else { + if (cur_speed <= 0) { + cur_speed = 0; + ts->state = TS_PAUSE; + } else { + ts->state = TS_PLAY; + play_time = now; + } + tvhlog(LOG_DEBUG, "timeshift", "ts %d sob speed %d", ts->id, cur_speed); + pause_time = last_time; + ctrl = streaming_msg_create_code(SMT_SPEED, cur_speed); + streaming_target_deliver2(ts->output, ctrl); + } + ctrl = NULL; + + /* Flush unwanted */ + } else if (ts->ondemand && cur_file) { + pthread_mutex_lock(&ts->rdwr_mutex); + timeshift_filemgr_flush(ts, cur_file); + pthread_mutex_unlock(&ts->rdwr_mutex); + } + + pthread_mutex_unlock(&ts->state_mutex); + } + + /* Cleanup */ + if (fd != -1) close(fd); + if (sm) streaming_msg_free(sm); + if (ctrl) streaming_msg_free(ctrl); + tvhtrace("timeshift", "ts %d exit reader thread", ts->id); + + return NULL; +} diff --git a/src/timeshift/timeshift_writer.c b/src/timeshift/timeshift_writer.c new file mode 100644 index 00000000..0e2f50ae --- /dev/null +++ b/src/timeshift/timeshift_writer.c @@ -0,0 +1,331 @@ +/** + * TV headend - Timeshift Write Handler + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvheadend.h" +#include "streaming.h" +#include "timeshift.h" +#include "timeshift/private.h" +#include "atomic.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ************************************************************************** + * File Writing + * *************************************************************************/ + +/* + * Write data (retry on EAGAIN) + */ +static ssize_t _write + ( int fd, const void *buf, size_t count ) +{ + ssize_t r; + size_t n = 0; + while ( n < count ) { + r = write(fd, buf+n, count-n); + if (r == -1) { + if (errno == EAGAIN) + continue; + else + return -1; + } + n += r; + } + return count == n ? n : -1; +} + +/* + * Write message + */ +static ssize_t _write_msg + ( int fd, streaming_message_type_t type, int64_t time, + const void *buf, size_t len ) +{ + size_t len2 = len + sizeof(type) + sizeof(time); + ssize_t err, ret; + ret = err = _write(fd, &len2, sizeof(len2)); + if (err < 0) return err; + err = _write(fd, &type, sizeof(type)); + if (err < 0) return err; + ret += err; + err = _write(fd, &time, sizeof(time)); + if (err < 0) return err; + ret += err; + if (len) { + err = _write(fd, buf, len); + if (err < 0) return err; + ret += err; + } + return ret; +} + +/* + * Write packet buffer + */ +static int _write_pktbuf ( int fd, pktbuf_t *pktbuf ) +{ + ssize_t ret, err; + if (pktbuf) { + ret = err = _write(fd, &pktbuf->pb_size, sizeof(pktbuf->pb_size)); + if (err < 0) return err; + err = _write(fd, pktbuf->pb_data, pktbuf->pb_size); + if (err < 0) return err; + ret += err; + } else { + size_t sz = 0; + ret = _write(fd, &sz, sizeof(sz)); + } + return ret; +} + +/* + * Write signal status + */ +ssize_t timeshift_write_sigstat + ( int fd, int64_t time, signal_status_t *sigstat ) +{ + return _write_msg(fd, SMT_SIGNAL_STATUS, time, sigstat, + sizeof(signal_status_t)); +} + +/* + * Write packet + */ +ssize_t timeshift_write_packet ( int fd, int64_t time, th_pkt_t *pkt ) +{ + ssize_t ret = 0, err; + ret = err = _write_msg(fd, SMT_PACKET, time, pkt, sizeof(th_pkt_t)); + if (err <= 0) return err; + err = _write_pktbuf(fd, pkt->pkt_header); + if (err <= 0) return err; + ret += err; + err = _write_pktbuf(fd, pkt->pkt_payload); + if (err <= 0) return err; + ret += err; + return ret; +} + +/* + * Write MPEGTS data + */ +ssize_t timeshift_write_mpegts ( int fd, int64_t time, void *data ) +{ + return _write_msg(fd, SMT_MPEGTS, time, data, 188); +} + +/* + * Write skip message + */ +ssize_t timeshift_write_skip ( int fd, streaming_skip_t *skip ) +{ + return _write_msg(fd, SMT_SKIP, 0, skip, sizeof(streaming_skip_t)); +} + +/* + * Write speed message + */ +ssize_t timeshift_write_speed ( int fd, int speed ) +{ + return _write_msg(fd, SMT_SPEED, 0, &speed, sizeof(speed)); +} + +/* + * Stop + */ +ssize_t timeshift_write_stop ( int fd, int code ) +{ + return _write_msg(fd, SMT_STOP, 0, &code, sizeof(code)); +} + +/* + * Exit + */ +ssize_t timeshift_write_exit ( int fd ) +{ + int code = 0; + return _write_msg(fd, SMT_EXIT, 0, &code, sizeof(code)); +} + +/* + * Write end of file (special internal message) + */ +ssize_t timeshift_write_eof ( int fd ) +{ + size_t sz = 0; + return _write(fd, &sz, sizeof(sz)); +} + +/* ************************************************************************** + * Thread + * *************************************************************************/ + +static inline ssize_t _process_msg0 + ( timeshift_t *ts, timeshift_file_t *tsf, streaming_message_t **smp ) +{ + int i; + ssize_t err; + streaming_start_t *ss; + streaming_message_t *sm = *smp; + if (sm->sm_type == SMT_START) { + err = 0; + timeshift_index_data_t *ti = calloc(1, sizeof(timeshift_index_data_t)); + ti->pos = tsf->size; + ti->data = sm; + *smp = NULL; + TAILQ_INSERT_TAIL(&tsf->sstart, ti, link); + + /* Update video index */ + ss = sm->sm_data; + for (i = 0; i < ss->ss_num_components; i++) + if (SCT_ISVIDEO(ss->ss_components[i].ssc_type)) + ts->vididx = ss->ss_components[i].ssc_index; + } else if (sm->sm_type == SMT_SIGNAL_STATUS) + err = timeshift_write_sigstat(tsf->fd, sm->sm_time, sm->sm_data); + else if (sm->sm_type == SMT_PACKET) { + err = timeshift_write_packet(tsf->fd, sm->sm_time, sm->sm_data); + if (err > 0) { + th_pkt_t *pkt = sm->sm_data; + + /* Index video iframes */ + if (pkt->pkt_componentindex == ts->vididx && + pkt->pkt_frametype == PKT_I_FRAME) { + timeshift_index_iframe_t *ti = calloc(1, sizeof(timeshift_index_iframe_t)); + ti->pos = tsf->size; + ti->time = sm->sm_time; + TAILQ_INSERT_TAIL(&tsf->iframes, ti, link); + } + } + } else if (sm->sm_type == SMT_MPEGTS) + err = timeshift_write_mpegts(tsf->fd, sm->sm_time, sm->sm_data); + else + err = 0; + + /* OK */ + if (err > 0) { + tsf->last = sm->sm_time; + tsf->size += err; + atomic_add_u64(×hift_total_size, err); + } + return err; +} + +static void _process_msg + ( timeshift_t *ts, streaming_message_t *sm, int *run ) +{ + int err; + timeshift_file_t *tsf; + + /* Process */ + switch (sm->sm_type) { + + /* Terminate */ + case SMT_EXIT: + if (run) *run = 0; + break; + case SMT_STOP: + if (sm->sm_code == 0 && run) + *run = 0; + break; + + /* Timeshifting */ + case SMT_SKIP: + case SMT_SPEED: + break; + + /* Status */ + case SMT_NOSTART: + case SMT_SERVICE_STATUS: + case SMT_TIMESHIFT_STATUS: + break; + + /* Store */ + case SMT_SIGNAL_STATUS: + case SMT_START: + case SMT_MPEGTS: + case SMT_PACKET: + pthread_mutex_lock(&ts->rdwr_mutex); + if ((tsf = timeshift_filemgr_get(ts, 1)) && (tsf->fd != -1)) { + if ((err = _process_msg0(ts, tsf, &sm)) < 0) { + timeshift_filemgr_close(tsf); + tsf->bad = 1; + ts->full = 1; ///< Stop any more writing + } + tsf->refcount--; + } + pthread_mutex_unlock(&ts->rdwr_mutex); + break; + } + + /* Next */ + if (sm) + streaming_msg_free(sm); +} + +void *timeshift_writer ( void *aux ) +{ + int run = 1; + timeshift_t *ts = aux; + streaming_queue_t *sq = &ts->wr_queue; + streaming_message_t *sm; + + pthread_mutex_lock(&sq->sq_mutex); + + while (run) { + + /* Get message */ + sm = TAILQ_FIRST(&sq->sq_queue); + if (sm == NULL) { + pthread_cond_wait(&sq->sq_cond, &sq->sq_mutex); + continue; + } + TAILQ_REMOVE(&sq->sq_queue, sm, sm_link); + pthread_mutex_unlock(&sq->sq_mutex); + + _process_msg(ts, sm, &run); + + pthread_mutex_lock(&sq->sq_mutex); + } + + pthread_mutex_unlock(&sq->sq_mutex); + return NULL; +} + +/* ************************************************************************** + * Utilities + * *************************************************************************/ + +void timeshift_writer_flush ( timeshift_t *ts ) + +{ + streaming_message_t *sm; + streaming_queue_t *sq = &ts->wr_queue; + + pthread_mutex_lock(&sq->sq_mutex); + while ((sm = TAILQ_FIRST(&sq->sq_queue))) { + TAILQ_REMOVE(&sq->sq_queue, sm, sm_link); + _process_msg(ts, sm, NULL); + } + pthread_mutex_unlock(&sq->sq_mutex); +} + diff --git a/src/trap.c b/src/trap.c index bd761186..b0fe543b 100644 --- a/src/trap.c +++ b/src/trap.c @@ -67,6 +67,7 @@ sappend(char *buf, size_t l, const char *fmt, ...) /** * */ +#if ENABLE_EXECINFO static int add2lineresolve(const char *binary, void *addr, char *buf0, size_t buflen) { @@ -126,6 +127,7 @@ add2lineresolve(const char *binary, void *addr, char *buf0, size_t buflen) close(fd[0]); return 0; } +#endif /* ENABLE_EXECINFO */ @@ -133,8 +135,8 @@ static void traphandler(int sig, siginfo_t *si, void *UC) { ucontext_t *uc = UC; - char buf[200]; #if ENABLE_EXECINFO + char buf[200]; static void *frames[MAXFRAMES]; int nframes = backtrace(frames, MAXFRAMES); Dl_info dli; @@ -243,9 +245,9 @@ trap_init(const char *ver) char *m = malloc(st.st_size); if(m != NULL) { if(read(fd, m, st.st_size) == st.st_size) { - SHA_Init(&binsum); - SHA_Update(&binsum, (void *)m, st.st_size); - SHA_Final(digest, &binsum); + SHA1_Init(&binsum); + SHA1_Update(&binsum, (void *)m, st.st_size); + SHA1_Final(digest, &binsum); } free(m); } diff --git a/src/tsdemux.c b/src/tsdemux.c index 796cc087..69b92374 100644 --- a/src/tsdemux.c +++ b/src/tsdemux.c @@ -103,7 +103,6 @@ ts_recv_packet0(service_t *t, elementary_stream_t *st, const uint8_t *tsb) switch(st->es_type) { case SCT_CA: - case SCT_PAT: case SCT_PMT: if(st->es_section == NULL) st->es_section = calloc(1, sizeof(struct psi_section)); @@ -224,7 +223,7 @@ ts_recv_packet1(service_t *t, const uint8_t *tsb, int64_t *pcrp) if((tsb[3] & 0xc0) || (t->s_scrambled_seen && st->es_type != SCT_CA && - st->es_type != SCT_PAT && st->es_type != SCT_PMT)) { + st->es_type != SCT_PMT)) { /** * Lock for descrambling, but only if packet was not in error @@ -248,7 +247,7 @@ ts_recv_packet1(service_t *t, const uint8_t *tsb, int64_t *pcrp) m++; } - if(!error) { + if(!error && t->s_scrambled != 0) { if(n == 0) { service_set_streaming_status_flags(t, TSS_NO_DESCRAMBLER); } else if(m == n) { diff --git a/src/tsmux.c b/src/tsmux.c deleted file mode 100644 index 6dd5c3ca..00000000 --- a/src/tsmux.c +++ /dev/null @@ -1,1403 +0,0 @@ -/* - * tvheadend, MPEG Transport stream muxer - * Copyright (C) 2008 - 2009 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 "tvhead.h" -#include "streaming.h" -#include "tsmux.h" - - -#define PID_PMT 1000 -#define PID_ES_BASE 2000 - -static const AVRational mpeg_tc = {1, 90000}; -static const AVRational mpeg_tc_27M = {1, 27000000}; - -LIST_HEAD(tsmuxer_es_list, tsmuxer_es); -TAILQ_HEAD(tsmuxer_pkt_queue, tsmuxer_pkt); - -/** - * - */ -typedef struct tsmuxer_pkt { - TAILQ_ENTRY(tsmuxer_pkt) tsp_link; - int64_t tsp_deadline; - int64_t tsp_dts; - int64_t tsp_pcr; - int tsp_contentsize; - - uint8_t tsp_payload[188]; - -} tsmuxer_pkt_t; - -/** - * - */ -typedef struct tsmuxer_fifo { - struct tsmuxer_pkt_queue tsf_queue; - int tsf_contentsize; - int tsf_len; - -} tsmuxer_fifo_t; - - - -/** - * TS Elementary stream - */ -typedef struct tsmuxer_es { - LIST_ENTRY(tsmuxer_es) te_link; - - int te_type; - - int te_input_index; - - char te_lang[4]; - - int te_pid; - int te_cc; - - int te_startcode; // Startcode to use for PES packetization - - struct streaming_message_queue te_smq; - int te_lookahead_depth; // bytes in te_smq - int te_lookahead_packets; // packets in te_smq - - int64_t te_mux_offset; - - int te_vbv_delay; - - tsmuxer_fifo_t te_delivery_fifo; - -} tsmuxer_es_t; - - - - -/** - * TS Multiplexer - */ -typedef struct tsmuxer { - - int tsm_run; - - struct tsmuxer_es_list tsm_eslist; - - int64_t tsm_pcr_start; - int64_t tsm_pcr_ref; - int64_t tsm_pcr_last; - - tsmuxer_es_t *tsm_pcr_stream; - - streaming_queue_t tsm_input; - - streaming_target_t *tsm_output; - - pthread_t tsm_thread; - -} tsmuxer_t; - - -/** - * - */ -static void -tmf_enq(tsmuxer_fifo_t *tsf, tsmuxer_pkt_t *tsp) -{ - /* record real content size */ - tsf->tsf_contentsize += tsp->tsp_contentsize; - - /* Enqueue packet */ - TAILQ_INSERT_TAIL(&tsf->tsf_queue, tsp, tsp_link); - tsf->tsf_len++; -} - -#if 0 -/** - * - */ -static void -tsf_remove(tsmuxer_fifo_t *tsf, tsmuxer_pkt_t *tsp) -{ - tsf->tsf_contentsize -= tsp->tsp_contentsize; - TAILQ_REMOVE(&tsf->tsf_queue, tsp, tsp_link); - tsf->tsf_len--; -} - -/** - * - */ -static tsmuxer_pkt_t * -tsf_deq(tsmuxer_fifo_t *tsf) -{ - tsmuxer_pkt_t *tm; - - tm = TAILQ_FIRST(&tsf->tsf_queue); - if(tm != NULL) - tsf_remove(tsf, tm); - return tm; -} -#endif - - -/** - * - */ -static void -tsf_init(tsmuxer_fifo_t *tsf) -{ - TAILQ_INIT(&tsf->tsf_queue); -} - - - -#if 0 -/** - * Check if we need to start delivery timer for the given stream - * - * Also, if it is the PCR stream and we're not yet runnig, figure out - * PCR and start generating packets - */ -static void -ts_check_deliver(tsmuxer_t *tsm, tsmuxer_es_t *te) -{ - int64_t now; - tsmuxer_fifo_t *tsf = &te->te_delivery_fifo; - int64_t next; - - if(dtimer_isarmed(&tms->tms_mux_timer)) - return; /* timer already running, we're fine */ - - assert(tms->tms_delivery_fifo.tmf_len != 0); - - now = getclock_hires(); - - if(ts->ts_pcr_ref == AV_NOPTS_VALUE) { - - if(ts->ts_pcr_start == AV_NOPTS_VALUE) - return; /* dont know anything yet */ - - ts->ts_pcr_ref = now - ts->ts_pcr_start + t->s_pcr_drift; - } - - f = TAILQ_FIRST(&tmf->tmf_queue); /* next packet we are going to send */ - next = f->tm_deadline + ts->ts_pcr_ref - t->s_pcr_drift; - - if(next < now + 100) - next = now + 100; - - dtimer_arm_hires(&tms->tms_mux_timer, ts_deliver, tms, next - 500); -} -#endif - - - -/** - * Generate TS packet, see comments inline - */ -static void -lookahead_dequeue(tsmuxer_t *tsm, tsmuxer_es_t *te) -{ - streaming_message_t *sm; - th_pkt_t *pkt; - tsmuxer_pkt_t *tsp; - uint8_t *tsb; - uint16_t u16; - int frrem, pad, tsrem, len, off, cc, hlen, flags; - int64_t t, tdur, toff, tlen, dts, pcr, basedelivery; - - - tlen = 0; - TAILQ_FOREACH(sm, &te->te_smq, sm_link) { - pkt = sm->sm_data; - tlen += pkt->pkt_payloadlen; - - if(pkt->pkt_pts != pkt->pkt_dts) { - tlen += 19; /* pes header with DTS and PTS */ - } else { - tlen += 14; /* pes header with PTS only */ - } - } - - if(tlen == 0) - return; - - sm = TAILQ_FIRST(&te->te_smq); - pkt = sm->sm_data; - toff = 0; - - /* XXX: assumes duration is linear, but it's probably ok */ - tdur = pkt->pkt_duration * te->te_lookahead_packets; - - if(te->te_mux_offset == AV_NOPTS_VALUE) { - if(te->te_vbv_delay == -1) - te->te_mux_offset = tdur / 2 - pkt->pkt_duration; - else - te->te_mux_offset = te->te_vbv_delay; - } - - if(tsm->tsm_pcr_start == AV_NOPTS_VALUE && tsm->tsm_pcr_stream == te) - tsm->tsm_pcr_start = pkt->pkt_dts - te->te_mux_offset; - - basedelivery = pkt->pkt_dts - te->te_mux_offset; - - while((sm = TAILQ_FIRST(&te->te_smq)) != NULL) { - - off = 0; - pkt = sm->sm_data; - - if(pkt->pkt_dts == pkt->pkt_pts) { - hlen = 8; - flags = 0x80; - } else { - hlen = 13; - flags = 0xc0; - } - - while(off < pkt->pkt_payloadlen) { - - tsp = malloc(sizeof(tsmuxer_pkt_t)); - tsp->tsp_deadline = basedelivery + tdur * toff / tlen; - - dts = (int64_t)pkt->pkt_duration * - (int64_t)off / (int64_t)pkt->pkt_payloadlen; - dts += pkt->pkt_dts; - - tsp->tsp_dts = dts; - tsb = tsp->tsp_payload; - - /* TS marker */ - *tsb++ = 0x47; - - /* Write PID and optionally payload unit start indicator */ - *tsb++ = te->te_pid >> 8 | (off ? 0 : 0x40); - *tsb++ = te->te_pid; - - cc = te->te_cc & 0xf; - te->te_cc++; - - /* Remaing bytes after 4 bytes of TS header */ - tsrem = 184; - - if(off == 0) { - /* When writing the packet header, shave of a bit of available - payload size */ - tsrem -= hlen + 6; - } - - /* Remaining length of frame */ - frrem = pkt->pkt_payloadlen - off; - - /* Compute amout of padding needed */ - pad = tsrem - frrem; - - pcr = tsp->tsp_deadline; - tsp->tsp_pcr = AV_NOPTS_VALUE; - - if(tsm->tsm_pcr_stream == te && tsm->tsm_pcr_last + 20000 < pcr) { - - tsp->tsp_pcr = pcr; - /* Insert PCR */ - - tlen += 8; /* compensate total length */ - tsrem -= 8; - pad -= 8; - if(pad < 0) - pad = 0; - - *tsb++ = 0x30 | cc; - *tsb++ = 7 + pad; - *tsb++ = 0x10; /* PCR flag */ - - t = av_rescale_q(pcr, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = t >> 25; - *tsb++ = t >> 17; - *tsb++ = t >> 9; - *tsb++ = t >> 1; - *tsb++ = (t & 1) << 7; - *tsb++ = 0; - - memset(tsb, 0xff, pad); - tsb += pad; - tsrem -= pad; - - tsm->tsm_pcr_last = pcr + 20000; - - } else if(pad > 0) { - /* Must pad TS packet */ - - *tsb++ = 0x30 | cc; - tsrem -= pad; - *tsb++ = --pad; - - memset(tsb, 0x00, pad); - tsb += pad; - } else { - *tsb++ = 0x10 | cc; - } - - - if(off == 0) { - /* Insert PES header */ - - /* Write startcode */ - *tsb++ = 0; - *tsb++ = 0; - *tsb++ = te->te_startcode >> 8; - *tsb++ = te->te_startcode; - - /* Write total frame length (without accounting for startcode and - length field itself */ - len = pkt->pkt_payloadlen + hlen; - - if(te->te_type == SCT_MPEG2VIDEO) { - /* It's okay to write len as 0 in transport streams, - but only for video frames, and i dont expect any of the - audio frames to exceed 64k - */ - len = 0; - } - - *tsb++ = len >> 8; - *tsb++ = len; - - *tsb++ = 0x80; /* MPEG2 */ - *tsb++ = flags; - *tsb++ = hlen - 3; /* length of rest of header (pts & dts) */ - - /* Write PTS */ - - if(flags == 0xc0) { - t = av_rescale_q(pkt->pkt_pts, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = (((t >> 30) & 7) << 1) | 1; - u16 = (((t >> 15) & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - u16 = ((t & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - } - - /* Write DTS */ - - t = av_rescale_q(pkt->pkt_dts, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = (((t >> 30) & 7) << 1) | 1; - u16 = (((t >> 15) & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - u16 = ((t & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - } - - memcpy(tsb, pkt->pkt_payload + off, tsrem); - - tsp->tsp_contentsize = tsrem; - - tmf_enq(&te->te_delivery_fifo, tsp); - - toff += tsrem; - off += tsrem; - - } - - te->te_lookahead_depth -= pkt->pkt_payloadlen; - te->te_lookahead_packets--; - pkt_ref_dec(pkt); - TAILQ_REMOVE(&te->te_smq, sm, sm_link); - - free(sm); - } - // ts_check_deliver(ts, tms); -} - - - -/** - * Packet input. - * - * We get the entire streaming message cause we might need to hold and - * enqueue the packet. So we can just reuse that allocation from now - * on - */ -static void -tsm_packet_input(tsmuxer_t *tsm, streaming_message_t *sm) -{ - tsmuxer_es_t *te; - th_pkt_t *pkt = sm->sm_data; - - LIST_FOREACH(te, &tsm->tsm_eslist, te_link) - if(te->te_input_index == pkt->pkt_componentindex) - break; - - if(te == NULL) { - // Stream not in use - streaming_msg_free(sm); - return; - } - - lookahead_dequeue(tsm, te); - - te->te_lookahead_depth += pkt->pkt_payloadlen; - te->te_lookahead_packets++; - - TAILQ_INSERT_TAIL(&te->te_smq, sm, sm_link); -} - - - -/** - * - */ -static void -te_destroy(tsmuxer_es_t *te) -{ - streaming_queue_clear(&te->te_smq); - LIST_REMOVE(te, te_link); - free(te); -} - - -/** - * - */ -static void -tsm_start(tsmuxer_t *tsm, const streaming_start_t *ss) -{ - int i, sc; - - tsmuxer_es_t *te; - - tsm->tsm_pcr_start = AV_NOPTS_VALUE; - tsm->tsm_pcr_ref = AV_NOPTS_VALUE; - tsm->tsm_pcr_last = INT64_MIN; - - - for(i = 0; i < ss->ss_num_components; i++) { - const streaming_start_component_t *ssc = &ss->ss_components[i]; - int dopcr = 0; - - switch(ssc->ssc_type) { - - case SCT_MPEG2VIDEO: - sc = 0x1e0; - dopcr = 1; - break; - - case SCT_MPEG2AUDIO: - sc = 0x1c0; - break; - - case SCT_AC3: - sc = 0x1bd; - break; - - case SCT_H264: - sc = 0x1e0; - dopcr = 1; - break; - default: - continue; - } - - te = calloc(1, sizeof(tsmuxer_es_t)); - tsf_init(&te->te_delivery_fifo); - memcpy(te->te_lang, ssc->ssc_lang, 4); - te->te_input_index = ssc->ssc_index; - te->te_type = ssc->ssc_type; - te->te_pid = PID_ES_BASE + i; - te->te_startcode = sc; - te->te_mux_offset = AV_NOPTS_VALUE; - - TAILQ_INIT(&te->te_smq); - - LIST_INSERT_HEAD(&tsm->tsm_eslist, te, te_link); - } -} - - -/** - * - */ -static void -tsm_stop(tsmuxer_t *tsm) -{ - tsmuxer_es_t *te; - - while((te = LIST_FIRST(&tsm->tsm_eslist)) != NULL) - te_destroy(te); -} - -/** - * - */ -static void -tsm_handle_input(tsmuxer_t *tsm, streaming_message_t *sm) -{ - switch(sm->sm_type) { - case SMT_PACKET: - tsm_packet_input(tsm, sm); - sm = NULL; - break; - - case SMT_START: - assert(LIST_FIRST(&tsm->tsm_eslist) == NULL); - tsm_start(tsm, sm->sm_data); - break; - - case SMT_STOP: - tsm_stop(tsm); - break; - - case SMT_TRANSPORT_STATUS: - // htsp_subscription_transport_status(hs, sm->sm_code); - break; - - case SMT_NOSOURCE: - // htsp_subscription_status(hs, "No available sources"); - break; - - case SMT_EXIT: - tsm->tsm_run = 0; - break; - } - - if(sm != NULL) - streaming_msg_free(sm); -} - - -/** - * - */ -static void * -tsm_thread(void *aux) -{ - tsmuxer_t *tsm = aux; - streaming_queue_t *sq = &tsm->tsm_input; - streaming_message_t *sm; - int64_t now, next = 0; - - tsm->tsm_run = 1; - - while(tsm->tsm_run) { - - pthread_mutex_lock(&sq->sq_mutex); - - sm = TAILQ_FIRST(&sq->sq_queue); - if(sm == NULL) { - - if(next == 0) { - pthread_cond_wait(&sq->sq_cond, &sq->sq_mutex); - } else { - struct timespec ts; - - ts.tv_sec = next / 1000000; - ts.tv_nsec = (next % 1000000) * 1000; - pthread_cond_timedwait(&sq->sq_cond, &sq->sq_mutex, &ts); - } - } - - sm = TAILQ_FIRST(&sq->sq_queue); - - if(sm != NULL) - TAILQ_REMOVE(&sq->sq_queue, sm, sm_link); - - pthread_mutex_unlock(&sq->sq_mutex); - - now = getmonoclock(); - - if(sm != NULL) - tsm_handle_input(tsm, sm); - - } - return NULL; -} - - -/** - * - */ -void -tsm_init(void) -{ - tsmuxer_t *tsm = calloc(1, sizeof(tsmuxer_t)); - - streaming_queue_init(&tsm->tsm_input); - - pthread_create(&tsm->tsm_thread, NULL, tsm_thread, tsm); -} - - - - - - -#if 0 - -#define _GNU_SOURCE -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "tvhead.h" -#include "dispatch.h" -#include "transports.h" -#include "subscriptions.h" -#include "psi.h" -#include "buffer.h" -#include "mux.h" -#include "tsmux.h" - -static void lookahead_dequeue(ts_muxer_t *ts, th_muxstream_t *tms); - - -/** - * Send current packet - */ -static void -ts_muxer_send_packet(ts_muxer_t *ts) -{ - int i; - int64_t t, tlow, pcr; - uint8_t *d; - service_t *tr; - - if(ts->ts_block == 0) - return; - - d = ts->ts_packet; - - /* Update PCR */ - - if(ts->ts_pcr_ref != AV_NOPTS_VALUE) { - for(i = 0; i < ts->ts_block; i++) { - d = ts->ts_packet + i * 188; - if((d[3] & 0xf0) == 0x30 && d[4] >= 7 && d[5] & 0x10) { - tr = ts->ts_muxer->tm_subscription->ths_transport; - - pcr = getclock_hires() - ts->ts_pcr_ref - tr->s_pcr_drift; - t = av_rescale_q(pcr, AV_TIME_BASE_Q, mpeg_tc_27M); - tlow = t % 300LL; - t = t / 300LL; - - d[6] = t >> 25; - d[7] = t >> 17; - d[8] = t >> 9; - d[9] = t >> 1; - d[10] = ((t & 1) << 7) | ((tlow >> 8) & 1); - d[11] = tlow; - } - } - } - ts->ts_output(ts->ts_output_opaque, ts->ts_subscription, ts->ts_packet, - ts->ts_block, 0); - ts->ts_block = 0; -} - -/** - * Push a MPEG TS packet to output - */ -static void -ts_muxer_add_packet(ts_muxer_t *ts, void *data, uint16_t pid) -{ - uint8_t *tsb; - - tsb = ts->ts_packet + ts->ts_block * 188; - ts->ts_block++; - - memcpy(tsb, data, 188); - - tsb[2] = pid; - tsb[1] = (tsb[1] & 0xf0) | (pid >> 8); - - if(ts->ts_block == ts->ts_blocks_per_packet) - ts_muxer_send_packet(ts); -} - -/** - * Raw TS input - */ -static void -ts_muxer_raw_input(struct th_subscription *s, void *data, int len, - elementary_stream_t *st, void *opaque) -{ - th_muxer_t *tm = s->ths_muxer; - ts_muxer_t *ts = opaque; - th_muxstream_t *tms; - - LIST_FOREACH(tms, &tm->tm_streams, tms_muxer_link0) - if(tms->tms_stream == st) - break; - - if(tms == NULL || tms->tms_index == 0) - return; /* Unknown / non-mapped stream */ - ts_muxer_add_packet(ts, data, tms->tms_index); -} - - - -/** - * Function for encapsulating a short table into a transport stream packet - */ -static void -ts_muxer_build_table(ts_muxer_t *ts, void *table, int tlen, int cc, int pid) -{ - int pad; - uint8_t tsb0[188], *tsb; - tsb = tsb0; - - pad = 184 - (tlen + 1); - - *tsb++ = 0x47; - *tsb++ = 0x40; - *tsb++ = 0; - *tsb++ = (cc & 0xf) | 0x30; - *tsb++ = --pad; - memset(tsb, 0, pad); - tsb += pad; - - *tsb++ = 0; /* Pointer field for tables */ - memcpy(tsb, table, tlen); - ts_muxer_add_packet(ts, tsb0, pid); -} - - - -/** - * - */ -static void -ts_muxer_generate_tables(void *aux, int64_t now) -{ - ts_muxer_t *ts = aux; - th_muxer_t *tm = ts->ts_muxer; - service_t *t; - th_muxstream_t *tms; - uint8_t table[180]; - int l, pcrpid; - - /* rearm timer */ - dtimer_arm_hires(&ts->ts_patpmt_timer, ts_muxer_generate_tables, - ts, now + 100000); - - l = psi_build_pat(NULL, table, sizeof(table), PID_PMT); - ts_muxer_build_table(ts, table, l, ts->ts_pat_cc, 0); - ts->ts_pat_cc++; - - switch(ts->ts_source) { - case TS_SRC_MUX: - pcrpid = ts->ts_pcr_stream->tms_index; - break; - - case TS_SRC_RAW_TS: - t = tm->tm_subscription->ths_transport; - - LIST_FOREACH(tms, &tm->tm_streams, tms_muxer_link0) - if(tms->tms_stream->st_pid == t->s_pcr_pid) - break; - - pcrpid = tms ? tms->tms_index : 0x1fff; - break; - default: - pcrpid = 0x1fff; - break; - } - - l = psi_build_pmt(tm, table, sizeof(table), pcrpid); - ts_muxer_build_table(ts, table, l, ts->ts_pmt_cc, PID_PMT); - ts->ts_pmt_cc++; - - ts_muxer_send_packet(ts); -} - - - - -/** - * - */ -static void -tmf_enq(th_muxfifo_t *tmf, th_muxpkt_t *tm) -{ - /* record real content size */ - tmf->tmf_contentsize += tm->tm_contentsize; - - /* Enqueue packet */ - TAILQ_INSERT_TAIL(&tmf->tmf_queue, tm, tm_link); - tmf->tmf_len++; -} - -/** - * - */ -static void -tmf_remove(th_muxfifo_t *tmf, th_muxpkt_t *tm) -{ - tmf->tmf_contentsize -= tm->tm_contentsize; - TAILQ_REMOVE(&tmf->tmf_queue, tm, tm_link); - tmf->tmf_len--; -} - - -/** - * - */ -static th_muxpkt_t * -tmf_deq(th_muxfifo_t *tmf) -{ - th_muxpkt_t *tm; - - tm = TAILQ_FIRST(&tmf->tmf_queue); - if(tm != NULL) - tmf_remove(tmf, tm); - return tm; -} - - - - -/** - * - */ -static void -tmf_init(th_muxfifo_t *tmf) -{ - TAILQ_INIT(&tmf->tmf_queue); -} - - -/** - * - */ -static void -ts_deliver(void *opaque, int64_t now) -{ - th_muxstream_t *tms = opaque; - th_muxer_t *tm = tms->tms_muxer; - service_t *t = tm->tm_subscription->ths_transport; - ts_muxer_t *ts = tm->tm_opaque; - th_muxpkt_t *f; - th_muxfifo_t *tmf = &tms->tms_delivery_fifo; - int64_t pcr = now - ts->ts_pcr_ref - t->s_pcr_drift; - int64_t dl, next, delta; - - f = tmf_deq(tmf); - dl = f->tm_deadline; - - delta = pcr - dl; - - ts_muxer_add_packet(ts, f->tm_pkt, tms->tms_index); - free(f); - - f = TAILQ_FIRST(&tmf->tmf_queue); /* next packet we are going to send */ - if(f == NULL) { - lookahead_dequeue(ts, tms); - f = TAILQ_FIRST(&tmf->tmf_queue); - if(f == NULL) - return; - } - - next = f->tm_deadline + ts->ts_pcr_ref - t->s_pcr_drift; - if(next < now + 100) - next = now + 100; - - dtimer_arm_hires(&tms->tms_mux_timer, ts_deliver, tms, next - 500); -} - - - -/** - * Check if we need to start delivery timer for the given stream - * - * Also, if it is the PCR stream and we're not yet runnig, figure out - * PCR and start generating packets - */ -static void -ts_check_deliver(ts_muxer_t *ts, th_muxstream_t *tms) -{ - int64_t now; - th_muxpkt_t *f; - th_muxfifo_t *tmf = &tms->tms_delivery_fifo; - service_t *t = ts->ts_muxer->tm_subscription->ths_transport; - int64_t next; - - if(dtimer_isarmed(&tms->tms_mux_timer)) - return; /* timer already running, we're fine */ - - assert(tms->tms_delivery_fifo.tmf_len != 0); - - now = getclock_hires(); - - if(ts->ts_pcr_ref == AV_NOPTS_VALUE) { - - if(ts->ts_pcr_start == AV_NOPTS_VALUE) - return; /* dont know anything yet */ - - ts->ts_pcr_ref = now - ts->ts_pcr_start + t->s_pcr_drift; - } - - f = TAILQ_FIRST(&tmf->tmf_queue); /* next packet we are going to send */ - next = f->tm_deadline + ts->ts_pcr_ref - t->s_pcr_drift; - - if(next < now + 100) - next = now + 100; - - dtimer_arm_hires(&tms->tms_mux_timer, ts_deliver, tms, next - 500); -} - - - - -/** - * Generate TS packet, see comments inline - * - * TODO: Dont insert both DTS and PTS if they're equal - */ - -static void -lookahead_dequeue(ts_muxer_t *ts, th_muxstream_t *tms) -{ - // elementary_stream_t *st = tms->tms_stream; - th_pkt_t *pkt; - th_muxpkt_t *tm; - th_refpkt_t *o; - uint8_t *tsb; - uint16_t u16; - int frrem, pad, tsrem, len, off, cc, hlen, flags; - int64_t t, tdur, toff, tlen, dts, pcr, basedelivery; - - tlen = 0; - TAILQ_FOREACH(o, &tms->tms_lookahead, trp_link) { - pkt = o->trp_pkt; - tlen += pkt->pkt_payloadlen; - - if(pkt->pkt_pts != pkt->pkt_dts) { - tlen += 19; /* pes header with DTS and PTS */ - } else { - tlen += 14; /* pes header with PTS only */ - } - } - - if(tlen == 0) - return; - - o = TAILQ_FIRST(&tms->tms_lookahead); - pkt = o->trp_pkt; - toff = 0; - - /* XXX: assumes duration is linear, but it's probably ok */ - tdur = pkt->pkt_duration * tms->tms_lookahead_packets; - - if(tms->tms_mux_offset == AV_NOPTS_VALUE) { - if(tms->tms_stream->st_vbv_delay == -1) - tms->tms_mux_offset = tdur / 2 - pkt->pkt_duration; - else - tms->tms_mux_offset = tms->tms_stream->st_vbv_delay; - } - - if(ts->ts_pcr_start == AV_NOPTS_VALUE && ts->ts_pcr_stream == tms) - ts->ts_pcr_start = pkt->pkt_dts - tms->tms_mux_offset; - - basedelivery = pkt->pkt_dts - tms->tms_mux_offset; - - while((o = TAILQ_FIRST(&tms->tms_lookahead)) != NULL) { - - off = 0; - pkt = o->trp_pkt; - - pkt_load(pkt); - - if(pkt->pkt_dts == pkt->pkt_pts) { - hlen = 8; - flags = 0x80; - } else { - hlen = 13; - flags = 0xc0; - } - - while(off < pkt->pkt_payloadlen) { - - tm = malloc(sizeof(th_muxpkt_t) + 188); - tm->tm_deadline = basedelivery + tdur * toff / tlen; - - dts = (int64_t)pkt->pkt_duration * - (int64_t)off / (int64_t)pkt->pkt_payloadlen; - dts += pkt->pkt_dts; - - tm->tm_dts = dts; - tsb = tm->tm_pkt; - - if(ts->ts_flags & TS_HTSCLIENT) { - /* Temporary hack */ - *tsb++ = tms->tms_stream->st_type; - } else { - /* TS marker */ - *tsb++ = 0x47; - } - - - /* Write PID and optionally payload unit start indicator */ - *tsb++ = tms->tms_index >> 8 | (off ? 0 : 0x40); - *tsb++ = tms->tms_index; - - cc = tms->tms_cc & 0xf; - tms->tms_cc++; - - /* Remaing bytes after 4 bytes of TS header */ - tsrem = 184; - - if(off == 0) { - /* When writing the packet header, shave of a bit of available - payload size */ - tsrem -= hlen + 6; - } - - /* Remaining length of frame */ - frrem = pkt->pkt_payloadlen - off; - - /* Compute amout of padding needed */ - pad = tsrem - frrem; - - pcr = tm->tm_deadline; - tm->tm_pcr = AV_NOPTS_VALUE; - - if(ts->ts_pcr_stream == tms && ts->ts_pcr_last + 20000 < pcr) { - - tm->tm_pcr = pcr; - /* Insert PCR */ - - tlen += 8; /* compensate total length */ - tsrem -= 8; - pad -= 8; - if(pad < 0) - pad = 0; - - *tsb++ = 0x30 | cc; - *tsb++ = 7 + pad; - *tsb++ = 0x10; /* PCR flag */ - - t = av_rescale_q(pcr, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = t >> 25; - *tsb++ = t >> 17; - *tsb++ = t >> 9; - *tsb++ = t >> 1; - *tsb++ = (t & 1) << 7; - *tsb++ = 0; - - memset(tsb, 0xff, pad); - tsb += pad; - tsrem -= pad; - - ts->ts_pcr_last = pcr + 20000; - - } else if(pad > 0) { - /* Must pad TS packet */ - - *tsb++ = 0x30 | cc; - tsrem -= pad; - *tsb++ = --pad; - - memset(tsb, 0x00, pad); - tsb += pad; - } else { - *tsb++ = 0x10 | cc; - } - - - if(off == 0) { - /* Insert PES header */ - - /* Write startcode */ - *tsb++ = 0; - *tsb++ = 0; - *tsb++ = tms->tms_sc >> 8; - *tsb++ = tms->tms_sc; - - /* Write total frame length (without accounting for startcode and - length field itself */ - len = pkt_len(pkt) + hlen; - - if(tms->tms_stream->st_type == HTSTV_MPEG2VIDEO) { - /* It's okay to write len as 0 in transport streams, - but only for video frames, and i dont expect any of the - audio frames to exceed 64k - */ - len = 0; - } - - *tsb++ = len >> 8; - *tsb++ = len; - - *tsb++ = 0x80; /* MPEG2 */ - *tsb++ = flags; - *tsb++ = hlen - 3; /* length of rest of header (pts & dts) */ - - /* Write PTS */ - - if(flags == 0xc0) { - t = av_rescale_q(pkt->pkt_pts, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = (((t >> 30) & 7) << 1) | 1; - u16 = (((t >> 15) & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - u16 = ((t & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - } - - /* Write DTS */ - - t = av_rescale_q(pkt->pkt_dts, AV_TIME_BASE_Q, mpeg_tc); - *tsb++ = (((t >> 30) & 7) << 1) | 1; - u16 = (((t >> 15) & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - u16 = ((t & 0x7fff) << 1) | 1; - *tsb++ = u16 >> 8; - *tsb++ = u16; - } - - memcpy(tsb, pkt->pkt_payload + off, tsrem); - - if(tms->tms_corruption_interval != 0) { - /* Only corruption payload, never the header */ - if(tms->tms_corruption_last + tms->tms_corruption_interval < pcr && - off != 0 && pkt->pkt_frametype == PKT_I_FRAME) { - tms->tms_corruption_counter = 50; - tms->tms_corruption_last = pcr; - } - - if(tms->tms_corruption_counter) { - tms->tms_cc++; - tms->tms_corruption_counter--; - } - } - - tm->tm_contentsize = tsrem; - - tmf_enq(&tms->tms_delivery_fifo, tm); - - toff += tsrem; - off += tsrem; - - } - - tms->tms_lookahead_depth -= pkt->pkt_payloadlen; - tms->tms_lookahead_packets--; - pkt_deref(pkt); - TAILQ_REMOVE(&tms->tms_lookahead, o, trp_link); - - free(o); - } - ts_check_deliver(ts, tms); -} - - - -/** - * - */ -static void -ts_mux_packet_input(void *opaque, th_muxstream_t *tms, th_pkt_t *pkt) -{ - ts_muxer_t *ts = opaque; - elementary_stream_t *st = tms->tms_stream; - th_refpkt_t *trp; - - if(tms->tms_index == 0) - return; - - if(st->st_vbv_delay == -1) { - if(tms->tms_lookahead_depth + pkt->pkt_payloadlen > st->st_vbv_size) - lookahead_dequeue(ts, tms); - } else { - lookahead_dequeue(ts, tms); - } - - trp = malloc(sizeof(th_refpkt_t)); - trp->trp_pkt = pkt_ref(pkt); - tms->tms_lookahead_depth += pkt->pkt_payloadlen; - tms->tms_lookahead_packets++; - TAILQ_INSERT_TAIL(&tms->tms_lookahead, trp, trp_link); - -} - -/** - * - */ -ts_muxer_t * -ts_muxer_init(th_subscription_t *s, ts_mux_output_t *output, - void *opaque, int flags, int corruption) -{ - ts_muxer_t *ts = calloc(1, sizeof(ts_muxer_t)); - int dopcr; - int pididx = PID_ES_BASE; - th_muxstream_t *tms; - th_muxer_t *tm; - elementary_stream_t *st; - - - ts->ts_output = output; - ts->ts_output_opaque = opaque; - ts->ts_flags = flags; - - ts->ts_subscription = s; - tm = ts->ts_muxer = muxer_init(s, ts_mux_packet_input, ts); - - ts->ts_blocks_per_packet = 7; - ts->ts_packet = malloc(188 * ts->ts_blocks_per_packet); - - ts->ts_pcr_start = AV_NOPTS_VALUE; - ts->ts_pcr_ref = AV_NOPTS_VALUE; - ts->ts_pcr_last = INT64_MIN; - - - /* Do TS MUX specific init per stream */ - - LIST_FOREACH(tms, &tm->tm_streams, tms_muxer_link0) { - st = tms->tms_stream; - - dopcr = 0; - switch(st->st_type) { - case HTSTV_MPEG2VIDEO: - tms->tms_corruption_interval = (int64_t)corruption * 1000000LL; - tms->tms_sc = 0x1e0; - dopcr = 1; - break; - case HTSTV_MPEG2AUDIO: - tms->tms_sc = 0x1c0; - st->st_vbv_delay = 45000; - break; - case HTSTV_AC3: - tms->tms_sc = 0x1bd; - st->st_vbv_delay = 50000; - break; - case HTSTV_H264: - tms->tms_sc = 0x1e0; - dopcr = 1; - break; - default: - continue; - } - - tms->tms_mux_offset = AV_NOPTS_VALUE; - tmf_init(&tms->tms_delivery_fifo); - TAILQ_INIT(&tms->tms_lookahead); - - if(dopcr && ts->ts_pcr_stream == NULL) - ts->ts_pcr_stream = tms; - - tms->tms_index = pididx++; - } - return ts; -} - - -/** - * - */ -void -ts_muxer_deinit(ts_muxer_t *ts, th_subscription_t *s) -{ - th_muxstream_t *tms; - th_muxer_t *tm = s->ths_muxer; - th_muxpkt_t *f; - th_refpkt_t *o; - - dtimer_disarm(&ts->ts_patpmt_timer); - - LIST_FOREACH(tms, &tm->tm_streams, tms_muxer_link0) { - dtimer_disarm(&tms->tms_mux_timer); - - /* Free mux packets */ - while((f = tmf_deq(&tms->tms_delivery_fifo)) != NULL) - free(f); - - /* Unreference lookahead queue */ - while((o = TAILQ_FIRST(&tms->tms_lookahead)) != NULL) { - pkt_deref(o->trp_pkt); - TAILQ_REMOVE(&tms->tms_lookahead, o, trp_link); - free(o); - } - } - - free(ts->ts_packet); - muxer_deinit(tm, s); - free(ts); -} - - -/** - * - */ -void -ts_muxer_play(ts_muxer_t *ts, int64_t toffset) -{ - th_subscription_t *s = ts->ts_muxer->tm_subscription; - - if(!(ts->ts_flags & TS_SEEK) && - s->ths_transport->s_source_type == S_MPEG_TS) { - /* We dont need to seek and source is MPEG TS, we can use a - shortcut to avoid remuxing stream */ - - ts->ts_source = TS_SRC_RAW_TS; - } else { - ts->ts_source = TS_SRC_MUX; - } - - /* Start PAT / PMT generator */ - ts_muxer_generate_tables(ts, getclock_hires()); - - switch(ts->ts_source) { - case TS_SRC_MUX: - muxer_play(ts->ts_muxer, toffset); - break; - case TS_SRC_RAW_TS: - s->ths_raw_input = ts_muxer_raw_input; - s->ths_opaque = ts; - break; - } -} - - -/** - * - */ -void -ts_muxer_pause(ts_muxer_t *ts) -{ - dtimer_disarm(&ts->ts_patpmt_timer); - muxer_pause(ts->ts_muxer); -} -#endif diff --git a/src/tsmux.h b/src/tsmux.h deleted file mode 100644 index a041c923..00000000 --- a/src/tsmux.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * tvheadend, MPEG transport stream muxer - * Copyright (C) 2008 - 2009 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 . - */ - -#ifndef TSMUX_H -#define TSMUX_H - -void tsm_init(void); - - -#if 0 - -typedef void (ts_mux_output_t)(void *opaque, th_subscription_t *s, - uint8_t *pkt, int npackets, int64_t pcr_ref); - -typedef struct ts_muxer { - th_subscription_t *ts_subscription; - int ts_flags; -#define TS_SEEK 0x1 -#define TS_HTSCLIENT 0x2 - - enum { - TS_SRC_MUX, - TS_SRC_RAW_TS, - - } ts_source; - - th_muxer_t *ts_muxer; - ts_mux_output_t *ts_output; - void *ts_output_opaque; - - int ts_pat_cc; - int ts_pmt_cc; - dtimer_t ts_patpmt_timer; - - uint8_t *ts_packet; - int ts_block; - int ts_blocks_per_packet; - - th_muxstream_t *ts_pcr_stream; - - int64_t ts_pcr_start; - int64_t ts_pcr_ref; /* System clock when PCR was/is/will be 0 */ - int64_t ts_pcr_last; -} ts_muxer_t; - - - - -ts_muxer_t *ts_muxer_init(th_subscription_t *s, ts_mux_output_t *output, - void *opaque, int flags, int corruption); - -void ts_muxer_deinit(ts_muxer_t *ts, th_subscription_t *s); - -void ts_muxer_play(ts_muxer_t *ts, int64_t toffset); - -void ts_muxer_pause(ts_muxer_t *ts); -#endif - - -#endif /* TSMUX_H */ diff --git a/src/tvheadend.h b/src/tvheadend.h index f7bb0f7e..4bc951dc 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -31,20 +31,44 @@ #include "queue.h" #include "avg.h" #include "hts_strtab.h" +#include "htsmsg.h" +#include "tvhlog.h" #include "redblack.h" -extern const char *tvheadend_version; -extern char *tvheadend_cwd; +typedef struct { + const char *name; + const uint32_t *enabled; +} tvh_caps_t; +extern const char *tvheadend_version; +extern const char *tvheadend_cwd; +extern const char *tvheadend_webroot; +extern const tvh_caps_t tvheadend_capabilities[]; + +static inline htsmsg_t *tvheadend_capabilities_list(int check) +{ + int i = 0; + htsmsg_t *r = htsmsg_create_list(); + while (tvheadend_capabilities[i].name) { + if (!check || + !tvheadend_capabilities[i].enabled || + *tvheadend_capabilities[i].enabled) + htsmsg_add_str(r, NULL, tvheadend_capabilities[i].name); + i++; + } + return r; +} #define PTS_UNSET INT64_C(0x8000000000000000) extern pthread_mutex_t global_lock; extern pthread_mutex_t ffmpeg_lock; extern pthread_mutex_t fork_lock; +extern pthread_mutex_t atomic_lock; -extern int webui_port; -extern int htsp_port; +extern int tvheadend_webui_port; +extern int tvheadend_webui_debug; +extern int tvheadend_htsp_port; typedef struct source_info { char *si_device; @@ -53,6 +77,7 @@ typedef struct source_info { char *si_mux; char *si_provider; char *si_service; + int si_type; } source_info_t; static inline void @@ -89,15 +114,21 @@ typedef struct gtimer { LIST_ENTRY(gtimer) gti_link; gti_callback_t *gti_callback; void *gti_opaque; - time_t gti_expire; + struct timespec gti_expire; } gtimer_t; void gtimer_arm(gtimer_t *gti, gti_callback_t *callback, void *opaque, int delta); +void gtimer_arm_ms(gtimer_t *gti, gti_callback_t *callback, void *opaque, + long delta_ms); + void gtimer_arm_abs(gtimer_t *gti, gti_callback_t *callback, void *opaque, time_t when); +void gtimer_arm_abs2(gtimer_t *gti, gti_callback_t *callback, void *opaque, + struct timespec *when); + void gtimer_disarm(gtimer_t *gti); @@ -153,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, @@ -161,7 +193,6 @@ typedef enum { SCT_TELETEXT, SCT_DVBSUB, SCT_CA, - SCT_PAT, SCT_PMT, SCT_AAC, SCT_MPEGTS, @@ -187,6 +218,26 @@ typedef struct signal_status { int unc; /* uncorrected blocks */ } signal_status_t; +/** + * Streaming skip + */ +typedef struct streaming_skip +{ + enum { + SMT_SKIP_ERROR, + SMT_SKIP_REL_TIME, + SMT_SKIP_ABS_TIME, + SMT_SKIP_REL_SIZE, + SMT_SKIP_ABS_SIZE, + SMT_SKIP_LIVE + } type; + union { + off_t size; + int64_t time; + }; +} streaming_skip_t; + + /** * A streaming pad generates data. * It has one or more streaming targets attached to it. @@ -211,6 +262,7 @@ TAILQ_HEAD(streaming_message_queue, streaming_message); * Streaming messages types */ typedef enum { + /** * Packet with data. * @@ -268,6 +320,22 @@ typedef enum { * Internal message to exit receiver */ SMT_EXIT, + + /** + * Set stream speed + */ + SMT_SPEED, + + /** + * Skip the stream + */ + SMT_SKIP, + + /** + * Timeshift status + */ + SMT_TIMESHIFT_STATUS, + } streaming_message_type_t; #define SMT_TO_MASK(x) (1 << ((unsigned int)x)) @@ -303,6 +371,9 @@ typedef enum { typedef struct streaming_message { TAILQ_ENTRY(streaming_message) sm_link; streaming_message_type_t sm_type; +#if ENABLE_TIMESHIFT + int64_t sm_time; +#endif union { void *sm_data; int sm_code; @@ -332,9 +403,10 @@ typedef struct streaming_queue { streaming_target_t sq_st; - pthread_mutex_t sq_mutex; /* Protects sp_queue */ - pthread_cond_t sq_cond; /* Condvar for signalling new - packets */ + pthread_mutex_t sq_mutex; /* Protects sp_queue */ + pthread_cond_t sq_cond; /* Condvar for signalling new packets */ + + size_t sq_maxsize; /* Max queue size (bytes) */ struct streaming_message_queue sq_queue; @@ -352,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) @@ -365,33 +437,11 @@ static inline unsigned int tvh_strhash(const char *s, unsigned int mod) #define MIN(a,b) ((a) < (b) ? (a) : (b)) #define MAX(a,b) ((a) > (b) ? (a) : (b)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) void tvh_str_set(char **strp, const char *src); int tvh_str_update(char **strp, const char *src); -void tvhlog(int severity, const char *subsys, const char *fmt, ...) - __attribute__((format(printf,3,4))); - -void tvhlog_spawn(int severity, const char *subsys, const char *fmt, ...) - __attribute__((format(printf,3,4))); - -#define LOG_EMERG 0 /* system is unusable */ -#define LOG_ALERT 1 /* action must be taken immediately */ -#define LOG_CRIT 2 /* critical conditions */ -#define LOG_ERR 3 /* error conditions */ -#define LOG_WARNING 4 /* warning conditions */ -#define LOG_NOTICE 5 /* normal but significant condition */ -#define LOG_INFO 6 /* informational */ -#define LOG_DEBUG 7 /* debug-level messages */ - -extern int log_debug; - -#define DEBUGLOG(subsys, fmt...) do { \ - if(log_debug) \ - tvhlog(LOG_DEBUG, subsys, fmt); \ -} while(0) - - #ifndef CLOCK_MONOTONIC_COARSE #define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC #endif @@ -430,10 +480,28 @@ extern void scopedunlock(pthread_mutex_t **mtxp); #define tvh_strlcatf(buf, size, fmt...) \ snprintf((buf) + strlen(buf), (size) - strlen(buf), fmt) +static inline const char *tvh_strbegins(const char *s1, const char *s2) +{ + while(*s2) + if(*s1++ != *s2++) + return NULL; + return s1; +} + +typedef struct th_pipe +{ + int rd; + int wr; +} th_pipe_t; + int tvh_open(const char *pathname, int flags, mode_t mode); int tvh_socket(int domain, int type, int protocol); +int tvh_pipe(int flags, th_pipe_t *pipe); + +int tvh_write(int fd, const void *buf, size_t len); + void hexdump(const char *pfx, const uint8_t *data, int len); uint32_t tvh_crc32(uint8_t *data, size_t datalen, uint32_t crc); @@ -448,6 +516,11 @@ static inline int64_t ts_rescale(int64_t ts, int tb) return (ts * tb ) / 90000LL; } +static inline int64_t ts_rescale_i(int64_t ts, int tb) +{ + return (ts * 90000LL) / tb; +} + void sbuf_init(sbuf_t *sb); void sbuf_free(sbuf_t *sb); @@ -470,11 +543,29 @@ void sbuf_put_byte(sbuf_t *sb, uint8_t u8); char *md5sum ( const char *str ); +int makedirs ( const char *path, int mode ); + +int rmtree ( const char *path ); + +char *regexp_escape ( const char *str ); + /* printing */ -#if __SIZEOF_LONG__ == 8 - #define PRItime_t PRId64 +# if __WORDSIZE == 64 +#define PRIsword_t PRId64 +#define PRIuword_t PRIu64 #else - #define PRItime_t "l" PRId32 +#define PRIsword_t PRId32 +#define PRIuword_t PRIu32 +#endif +#define PRIslongword_t "ld" +#define PRIulongword_t "lu" +#define PRIsize_t PRIuword_t +#define PRIssize_t PRIsword_t +#define PRItime_t PRIslongword_t +#if _FILE_OFFSET_BITS == 64 +#define PRIoff_t PRId64 +#else +#define PRIoff_t PRIslongword_t #endif #endif /* TV_HEAD_H */ diff --git a/src/tvhlog.c b/src/tvhlog.c new file mode 100644 index 00000000..f55288ec --- /dev/null +++ b/src/tvhlog.c @@ -0,0 +1,272 @@ +/* + * Tvheadend - logging + * Copyright (C) 2012 Adam Sutton + * + * 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 "tvhlog.h" +#include +#include +#include +#include +#include + +#include "webui/webui.h" + +int tvhlog_level; +int tvhlog_options; +char *tvhlog_path; +htsmsg_t *tvhlog_subsys; +pthread_mutex_t tvhlog_mutex; + +static const char *logtxtmeta[9][2] = { + {"EMERGENCY", "\033[31m"}, + {"ALERT", "\033[31m"}, + {"CRITICAL", "\033[31m"}, + {"ERROR", "\033[31m"}, + {"WARNING", "\033[33m"}, + {"NOTICE", "\033[36m"}, + {"INFO", "\033[32m"}, + {"DEBUG", "\033[32m"}, + {"TRACE", "\033[32m"}, +}; + +/* Initialise */ +void +tvhlog_init ( int level, int options, const char *path ) +{ + tvhlog_level = level; + tvhlog_options = options; + tvhlog_path = path ? strdup(path) : NULL; + tvhlog_subsys = NULL; + openlog("tvheadend", LOG_PID, LOG_DAEMON); + pthread_mutex_init(&tvhlog_mutex, NULL); +} + +/* Get subsys */ +void tvhlog_get_subsys ( char *subsys, size_t len ) +{ + size_t c = 0; + int first = 1; + htsmsg_field_t *f; + *subsys = '\0'; + if (tvhlog_subsys) { + HTSMSG_FOREACH(f, tvhlog_subsys) { + if (f->hmf_type != HMF_S64) continue; + c += snprintf(subsys+c, len-c, "%c%s%s", + f->hmf_s64 ? '+' : '-', + f->hmf_name, + first ? "" : ","); + first = 0; + } + } +} + +/* Set subsys */ +void tvhlog_set_subsys ( const char *subsys ) +{ + uint32_t a; + char *t, *r = NULL, *s; + + if (tvhlog_subsys) + htsmsg_destroy(tvhlog_subsys); + tvhlog_subsys = NULL; + + if (!subsys) + return; + + s = strdup(subsys); + t = strtok_r(s, ",", &r); + while ( t ) { + subsys = NULL; + a = 1; + if (!*t) goto next; + if (t[0] == '+' || t[0] == '-') { + a = t[0] == '+'; + t++; + } + if (!strcmp(t, "all")) { + if (tvhlog_subsys) + htsmsg_destroy(tvhlog_subsys); + tvhlog_subsys = NULL; + } + if (!tvhlog_subsys) + tvhlog_subsys = htsmsg_create_map(); + htsmsg_set_u32(tvhlog_subsys, t, a); +next: + t = strtok_r(NULL, ",", &r); + } + free(s); +} + +/* Log */ +void tvhlogv ( const char *file, int line, + int notify, int severity, + const char *subsys, const char *fmt, va_list *args ) +{ + struct timeval now; + struct tm tm; + char t[128], buf[2048], buf2[2048]; + size_t l; + int s; + int options; + char *path = NULL; + int skip = 0; + + /* Map down */ + if (severity > LOG_DEBUG) + s = LOG_DEBUG; + else + s = severity; + + /* Check debug enabled (and cache config) */ + pthread_mutex_lock(&tvhlog_mutex); + if (severity >= LOG_DEBUG) { + if (!tvhlog_subsys) + skip = 1; + else if (severity > tvhlog_level) + skip = 1; + else { + uint32_t a = htsmsg_get_u32_or_default(tvhlog_subsys, "all", 0); + if (!htsmsg_get_u32_or_default(tvhlog_subsys, subsys, a)) + skip = 1; + } + } + if (!skip) { + if (tvhlog_path) + path = strdup(tvhlog_path); + options = tvhlog_options; + } + pthread_mutex_unlock(&tvhlog_mutex); + if (skip) + return; + + /* Get time */ + gettimeofday(&now, NULL); + localtime_r(&now.tv_sec, &tm); + l = strftime(t, sizeof(t), "%b %d %H:%M:%S", &tm); + if (options & TVHLOG_OPT_MILLIS) { + int ms = now.tv_usec / 1000; + snprintf(t+l, sizeof(t)-l, ".%03d", ms); + } + + /* Basic message */ + l = snprintf(buf, sizeof(buf), "%s: ", subsys); + if (options & TVHLOG_OPT_FILELINE && severity >= LOG_DEBUG) + l += snprintf(buf + l, sizeof(buf) - l, "(%s:%d) ", file, line); + if (args) + l += vsnprintf(buf + l, sizeof(buf) - l, fmt, *args); + else + l += snprintf(buf + l, sizeof(buf) - l, "%s", fmt); + + /* Syslog */ + if (options & TVHLOG_OPT_SYSLOG) { + if (options & TVHLOG_OPT_DBG_SYSLOG || severity < LOG_DEBUG) { + syslog(s, "%s", buf); + } + } + + /* Comet (debug must still be enabled??) */ + if(notify && severity < LOG_TRACE) { + htsmsg_t *m = htsmsg_create_map(); + snprintf(buf2, sizeof(buf2), "%s %s", t, buf); + htsmsg_add_str(m, "notificationClass", "logmessage"); + htsmsg_add_str(m, "logtxt", buf2); + comet_mailbox_add_message(m, severity >= LOG_DEBUG); + htsmsg_destroy(m); + } + + /* Console */ + if (options & TVHLOG_OPT_STDERR) { + if (options & TVHLOG_OPT_DBG_STDERR || severity < LOG_DEBUG) { + const char *leveltxt = logtxtmeta[severity][0]; + const char *sgr = logtxtmeta[severity][1]; + const char *sgroff; + + if (options & TVHLOG_OPT_DECORATE) + sgroff = "\033[0m"; + else { + sgr = ""; + sgroff = ""; + } + fprintf(stderr, "%s%s [%7s] %s%s\n", sgr, t, leveltxt, buf, sgroff); + } + } + + /* File */ + if (path) { + if (options & TVHLOG_OPT_DBG_FILE || severity < LOG_DEBUG) { + const char *leveltxt = logtxtmeta[severity][0]; + FILE *fp = fopen(path, "a"); + if (fp) { + fprintf(fp, "%s [%7s]:%s\n", t, leveltxt, buf); + fclose(fp); + } + } + free(path); + } +} + +/* + * Map args + */ +void _tvhlog ( const char *file, int line, + int notify, int severity, + const char *subsys, const char *fmt, ... ) +{ + va_list args; + va_start(args, fmt); + tvhlogv(file, line, notify, severity, subsys, fmt, &args); + va_end(args); +} + +/* + * Log a hexdump + */ +#define HEXDUMP_WIDTH 16 +void +_tvhlog_hexdump(const char *file, int line, + int notify, int severity, + const char *subsys, + const uint8_t *data, ssize_t len ) +{ + int i, c; + char str[1024]; + + while (len > 0) { + c = 0; + for (i = 0; i < HEXDUMP_WIDTH; i++) { + if (i >= len) + c += snprintf(str+c, sizeof(str)-c, " "); + else + c += snprintf(str+c, sizeof(str)-c, "%02X ", data[i]); + } + for (i = 0; i < HEXDUMP_WIDTH; i++) { + if (i < len) { + if (data[i] < ' ' || data[i] > '~') + str[c] = '.'; + else + str[c] = data[i]; + } else + str[c] = ' '; + c++; + } + str[c] = '\0'; + tvhlogv(file, line, notify, severity, subsys, str, NULL); + len -= HEXDUMP_WIDTH; + data += HEXDUMP_WIDTH; + } +} + diff --git a/src/tvhlog.h b/src/tvhlog.h new file mode 100644 index 00000000..cc4b3011 --- /dev/null +++ b/src/tvhlog.h @@ -0,0 +1,82 @@ +/* + * Tvheadend - logging + * Copyright (C) 2012 Adam Sutton + * + * 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 . + */ +#ifndef __TVH_LOGGING_H__ +#define __TVH_LOGGING_H__ + +#include +#include +#include + +#include "htsmsg.h" + +/* Config */ +extern int tvhlog_level; +extern htsmsg_t *tvhlog_subsys; +extern char *tvhlog_path; +extern int tvhlog_options; +extern pthread_mutex_t tvhlog_mutex; + +/* Initialise */ +void tvhlog_init ( int level, int options, const char *path ); +void tvhlog_set_subsys ( const char *subsys ); +void tvhlog_get_subsys ( char *subsys, size_t len ); +void tvhlogv ( const char *file, int line, + int notify, int severity, + const char *subsys, const char *fmt, va_list *args ); +void _tvhlog ( const char *file, int line, + int notify, int severity, + const char *subsys, const char *fmt, ... ) + __attribute__((format(printf,6,7))); +void _tvhlog_hexdump ( const char *file, int line, + int notify, int severity, + const char *subsys, + const uint8_t *data, ssize_t len ); + + +/* Options */ +#define TVHLOG_OPT_DBG_SYSLOG 0x0001 +#define TVHLOG_OPT_DBG_STDERR 0x0002 +#define TVHLOG_OPT_DBG_FILE 0x0004 +#define TVHLOG_OPT_SYSLOG 0x0010 +#define TVHLOG_OPT_STDERR 0x0020 +#define TVHLOG_OPT_MILLIS 0x0100 +#define TVHLOG_OPT_DECORATE 0x0200 +#define TVHLOG_OPT_FILELINE 0x0400 +#define TVHLOG_OPT_ALL 0xFFFF + +/* Levels */ +#ifndef LOG_TRACE +#define LOG_TRACE (LOG_DEBUG+1) +#endif + +/* Macros */ +#define tvhlog(severity, subsys, fmt, ...)\ + _tvhlog(__FILE__, __LINE__, 1, severity, subsys, fmt, ##__VA_ARGS__) +#define tvhlog_spawn(severity, subsys, fmt, ...)\ + _tvhlog(__FILE__, __LINE__, 0, severity, subsys, fmt, ##__VA_ARGS__) +#if ENABLE_TRACE +#define tvhtrace(subsys, fmt, ...)\ + _tvhlog(__FILE__, __LINE__, 0, LOG_TRACE, subsys, fmt, ##__VA_ARGS__) +#define tvhlog_hexdump(subsys, data, len)\ + _tvhlog_hexdump(__FILE__, __LINE__, 0, LOG_TRACE, subsys, (uint8_t*)data, len) +#else +#define tvhtrace(...) (void)0 +#define tvhlog_hexdump(...) (void)0 +#endif + +#endif /* __TVH_LOGGING_H__ */ diff --git a/src/tvhtime.c b/src/tvhtime.c new file mode 100644 index 00000000..e19d6c8d --- /dev/null +++ b/src/tvhtime.c @@ -0,0 +1,166 @@ +#define _ISOC9X_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tvhtime.h" +#include "tvheadend.h" +#include "settings.h" + +uint32_t tvhtime_update_enabled; +uint32_t tvhtime_ntp_enabled; +uint32_t tvhtime_tolerance; + +/* + * NTP processing + */ +#define NTPD_BASE 0x4e545030 /* "NTP0" */ +#define NTPD_UNIT 2 + +typedef struct +{ + int mode; /* 0 - if valid set + * use values, + * clear valid + * 1 - if valid set + * if count before and after read of values is equal, + * use values + * clear valid + */ + int count; + time_t clockTimeStampSec; + int clockTimeStampUSec; + time_t receiveTimeStampSec; + int receiveTimeStampUSec; + int leap; + int precision; + int nsamples; + int valid; + int pad[10]; +} ntp_shm_t; + +static ntp_shm_t * +ntp_shm_init ( void ) +{ + int shmid, unit, mode; + static ntp_shm_t *shmptr = NULL; + + if (shmptr != NULL) + return shmptr; + + unit = getuid() ? 2 : 0; + mode = getuid() ? 0666 : 0600; + + shmid = shmget((key_t)NTPD_BASE + unit, sizeof(ntp_shm_t), IPC_CREAT | mode); + if (shmid == -1) + return NULL; + + shmptr = shmat(shmid, 0, 0); + memset(shmptr, 0, sizeof(ntp_shm_t)); + if (shmptr) { + shmptr->mode = 1; + shmptr->precision = -1; + shmptr->nsamples = 1; + } + + return shmptr; +} + +/* + * Update time + */ +void +tvhtime_update ( struct tm *tm ) +{ + time_t now; + struct timeval tv; + ntp_shm_t *ntp_shm; + int64_t t1, t2; + + tvhtrace("time", "current time is %04d/%02d/%02d %02d:%02d:%02d", + tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + + /* Current and reported time */ + now = mktime(tm); + gettimeofday(&tv, NULL); + + /* Delta */ + t1 = now * 1000000; + t2 = tv.tv_sec * 1000000 + tv.tv_usec; + + /* Update local clock */ + if (tvhtime_update_enabled) { + if (llabs(t2 - t1) > tvhtime_tolerance) { + tvhlog(LOG_DEBUG, "time", "updated system clock"); + stime(&now); + } + } + + /* NTP */ + if (tvhtime_ntp_enabled) { + if (!(ntp_shm = ntp_shm_init())) + return; + + tvhtrace("time", "ntp delta = %"PRId64" us\n", t2 - t1); + ntp_shm->valid = 0; + ntp_shm->count++; + ntp_shm->clockTimeStampSec = now; + ntp_shm->clockTimeStampUSec = 0; + ntp_shm->receiveTimeStampSec = tv.tv_sec; + ntp_shm->receiveTimeStampUSec = (int)tv.tv_usec; + ntp_shm->count++; + ntp_shm->valid = 1; + } +} + +/* Initialise */ +void tvhtime_init ( void ) +{ + htsmsg_t *m = hts_settings_load("tvhtime/config"); + if (htsmsg_get_u32(m, "update_enabled", &tvhtime_update_enabled)) + tvhtime_update_enabled = 0; + if (htsmsg_get_u32(m, "ntp_enabled", &tvhtime_ntp_enabled)) + tvhtime_ntp_enabled = 0; + if (htsmsg_get_u32(m, "tolerance", &tvhtime_tolerance)) + tvhtime_tolerance = 5000; +} + +static void tvhtime_save ( void ) +{ + htsmsg_t *m = htsmsg_create_map(); + htsmsg_add_u32(m, "update_enabled", tvhtime_update_enabled); + htsmsg_add_u32(m, "ntp_enabled", tvhtime_ntp_enabled); + htsmsg_add_u32(m, "tolerance", tvhtime_tolerance); + hts_settings_save(m, "tvhtime/config"); +} + +void tvhtime_set_update_enabled ( uint32_t on ) +{ + if (tvhtime_update_enabled == on) + return; + tvhtime_update_enabled = on; + tvhtime_save(); +} + +void tvhtime_set_ntp_enabled ( uint32_t on ) +{ + if (tvhtime_ntp_enabled == on) + return; + tvhtime_ntp_enabled = on; + tvhtime_save(); +} + +void tvhtime_set_tolerance ( uint32_t v ) +{ + if (tvhtime_tolerance == v) + return; + tvhtime_tolerance = v; + tvhtime_save(); +} diff --git a/src/tvhtime.h b/src/tvhtime.h new file mode 100644 index 00000000..3829ce52 --- /dev/null +++ b/src/tvhtime.h @@ -0,0 +1,34 @@ +/* + * TVheadend - time processing + * + * Copyright (C) 2013 Adam Sutton + * + * 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 . + */ + +#ifndef __TVH_TIME_H__ +#define __TVH_TIME_H_ + +extern uint32_t tvhtime_update_enabled; +extern uint32_t tvhtime_ntp_enabled; +extern uint32_t tvhtime_tolerance; + +void tvhtime_init ( void ); +void tvhtime_update ( struct tm *now ); + +void tvhtime_set_update_enabled ( uint32_t on ); +void tvhtime_set_ntp_enabled ( uint32_t on ); +void tvhtime_set_tolerance ( uint32_t v ); + +#endif /* __TVH_TIME_H__ */ diff --git a/src/utils.c b/src/utils.c index 89ae56d4..a30ea823 100644 --- a/src/utils.c +++ b/src/utils.c @@ -21,6 +21,10 @@ #include #include #include +#include +#include +#include +#include #include "tvheadend.h" /** @@ -337,3 +341,98 @@ md5sum ( const char *str ) ret[MD5_DIGEST_LENGTH*2] = '\0'; return ret; } + +int +makedirs ( const char *inpath, int mode ) +{ + int err, ok; + size_t x; + struct stat st; + char path[512]; + + if (!inpath || !*inpath) return -1; + + x = 1; + ok = 1; + strcpy(path, inpath); + while(ok) { + ok = path[x]; + if (path[x] == '/' || !path[x]) { + path[x] = 0; + if (stat(path, &st)) { + err = mkdir(path, mode); + } else { + err = S_ISDIR(st.st_mode) ? 0 : 1; + errno = ENOTDIR; + } + if (err) { + tvhlog(LOG_ALERT, "settings", "Unable to create dir \"%s\": %s", + path, strerror(errno)); + return -1; + } + path[x] = '/'; + } + x++; + } + return 0; +} + +int +rmtree ( const char *path ) +{ + int err = 0; + struct dirent de, *der; + struct stat st; + char buf[512]; + DIR *dir = opendir(path); + if (!dir) return -1; + while (!readdir_r(dir, &de, &der) && der) { + if (!strcmp("..", de.d_name) || !strcmp(".", de.d_name)) + continue; + snprintf(buf, sizeof(buf), "%s/%s", path, de.d_name); + err = stat(buf, &st); + if (err) break; + if (S_ISDIR(st.st_mode)) + err = rmtree(buf); + else + err = unlink(buf); + if (err) break; + } + closedir(dir); + if (!err) + err = rmdir(path); + return err; +} + +char * +regexp_escape(const char* str) +{ + const char *a; + char *tmp, *b; + if (!str) + return NULL; + a = str; + b = tmp = malloc(strlen(str) * 2); + while (*a) { + switch (*a) { + case '?': + case '+': + case '.': + case '(': + case ')': + case '[': + case ']': + case '*': + *b = '\\'; + b++; + /* -fallthrough */ + default: + break; + } + *b = *a; + b++; + a++; + } + *b = 0; + return tmp; +} diff --git a/src/v4l.c b/src/v4l.c index 032ce060..1a48fed8 100644 --- a/src/v4l.c +++ b/src/v4l.c @@ -253,7 +253,7 @@ v4l_service_stop(service_t *t) assert(va->va_current_service != NULL); - if(write(va->va_pipe[1], &c, 1) != 1) + if(tvh_write(va->va_pipe[1], &c, 1)) tvhlog(LOG_ERR, "v4l", "Unable to close video thread -- %s", strerror(errno)); @@ -304,6 +304,15 @@ v4l_service_quality(service_t *t) return 100; } +/** + * + */ +static int +v4l_service_is_enabled(service_t *t) +{ + return t->s_enabled; +} + /** * @@ -324,6 +333,7 @@ v4l_service_setsourceinfo(service_t *t, struct source_info *si) char buf[64]; memset(si, 0, sizeof(struct source_info)); + si->si_type = S_MPEG_PS; si->si_adapter = strdup(t->s_v4l_adapter->va_displayname); snprintf(buf, sizeof(buf), "%d Hz", t->s_v4l_frequency); @@ -371,6 +381,7 @@ v4l_service_find(v4l_adapter_t *va, const char *id, int create) t->s_config_save = v4l_service_save; t->s_setsourceinfo = v4l_service_setsourceinfo; t->s_quality_index = v4l_service_quality; + t->s_is_enabled = v4l_service_is_enabled; t->s_grace_period = v4l_grace_period; t->s_iptv_fd = -1; t->s_v4l_adapter = va; diff --git a/src/webui/comet.c b/src/webui/comet.c index a540dbf6..aaf42ae0 100644 --- a/src/webui/comet.c +++ b/src/webui/comet.c @@ -32,6 +32,7 @@ #include "http.h" #include "webui/webui.h" #include "access.h" +#include "tcp.h" static pthread_mutex_t comet_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t comet_cond = PTHREAD_COND_INITIALIZER; @@ -153,16 +154,24 @@ comet_access_update(http_connection_t *hc, comet_mailbox_t *cmb) static void comet_serverIpPort(http_connection_t *hc, comet_mailbox_t *cmb) { - char buf[INET_ADDRSTRLEN + 1]; + char buf[50]; + uint32_t port; - inet_ntop(AF_INET, &hc->hc_self->sin_addr, buf, sizeof(buf)); + tcp_get_ip_str((struct sockaddr*)hc->hc_self, buf, 50); + + if(hc->hc_self->ss_family == AF_INET) + port = ((struct sockaddr_in*)hc->hc_self)->sin_port; + else if(hc->hc_self->ss_family == AF_INET6) + port = ((struct sockaddr_in6*)hc->hc_self)->sin6_port; + else + port = 0; htsmsg_t *m = htsmsg_create_map(); htsmsg_add_str(m, "notificationClass", "setServerIpPort"); htsmsg_add_str(m, "ip", buf); - htsmsg_add_u32(m, "port", ntohs(hc->hc_self->sin_port)); + htsmsg_add_u32(m, "port", ntohs(port)); if(cmb->cmb_messages == NULL) cmb->cmb_messages = htsmsg_create_list(); diff --git a/src/webui/extjs.c b/src/webui/extjs.c index cf318fc5..dd2b07d1 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -46,6 +46,10 @@ #include "epggrab/private.h" #include "config2.h" #include "lang_codes.h" +#include "subscriptions.h" +#include "imagecache.h" +#include "timeshift.h" +#include "tvhtime.h" /** * @@ -84,23 +88,24 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) htsbuf_queue_t *hq = &hc->hc_reply; #define EXTJSPATH "static/extjs" + htsbuf_qprintf(hq, "\n"); + htsbuf_qprintf(hq, "\n"); - htsbuf_qprintf(hq, "\n" - "\n" - "\n" - "\n" - "\n" - "\n"); - - extjs_exec(hq, "Ext.BLANK_IMAGE_URL = " - "'"EXTJSPATH"/resources/images/default/s.gif';"); - -#if 0 - htsbuf_qprintf(hq, - ""); -#endif + // Issue #1504 - IE9 temporary fix + htsbuf_qprintf(hq, "\n"); + + htsbuf_qprintf(hq, "\n" + "\n" + "\n" + "\n" + "\n" + "\n", + tvheadend_webui_debug ? "-debug" : "", + tvheadend_webui_debug ? "-debug" : "", + tvheadend_webui_debug ? "" : "-min"); + + extjs_exec(hq, "Ext.BLANK_IMAGE_URL = " "'"EXTJSPATH"/resources/images/default/s.gif';"); /** * Load extjs extensions @@ -132,12 +137,17 @@ extjs_root(http_connection_t *hc, const char *remain, void *opaque) extjs_load(hq, "static/app/iptv.js"); #if ENABLE_V4L extjs_load(hq, "static/app/v4l.js"); +#endif +#if ENABLE_TIMESHIFT + extjs_load(hq, "static/app/timeshift.js"); #endif extjs_load(hq, "static/app/chconf.js"); extjs_load(hq, "static/app/epg.js"); extjs_load(hq, "static/app/dvr.js"); extjs_load(hq, "static/app/epggrab.js"); extjs_load(hq, "static/app/config.js"); + extjs_load(hq, "static/app/tvhlog.js"); + extjs_load(hq, "static/app/status.js"); /** * Finally, the app itself @@ -188,10 +198,10 @@ page_about(http_connection_t *hc, const char *remain, void *opaque) "
" "HTS Tvheadend %s" "

" - "© 2006 - 2012 Andreas \303\226man, et al.

" + "© 2006 - 2013 Andreas \303\226man, et al.

" "
" - "" - "http://www.lonelycoder.com/tvheadend

" + "" + "https://tvheadend.org

" "Based on software from " "ExtJS. " "Icons from " @@ -205,7 +215,7 @@ page_about(http_connection_t *hc, const char *remain, void *opaque) "All proceeds are used to support server infrastructure and buy test " "equipment." "
" - "" + "" "", tvheadend_version, tvheadend_version); @@ -286,10 +296,10 @@ extjs_tablemgr(http_connection_t *hc, const char *remain, void *opaque) if(in != NULL) htsmsg_destroy(in); - if(out != NULL) { - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - } + if(out == NULL) + out = htsmsg_create_map(); + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); http_output_content(hc, "text/x-json; charset=UTF-8"); return 0; } @@ -350,7 +360,7 @@ extjs_channels_update(htsmsg_t *in) if((s = htsmsg_get_str(c, "epggrabsrc")) != NULL) { char *tmp = strdup(s); - char *sptr = NULL; + char *sptr = NULL, *sptr2 = NULL; char *modecid = strtok_r(tmp, ",", &sptr); char *modid, *ecid; epggrab_module_t *mod; @@ -375,8 +385,8 @@ extjs_channels_update(htsmsg_t *in) /* Add new */ while (modecid) { - modid = strtok(modecid, "|"); - ecid = strtok(NULL, "|"); + modid = strtok_r(modecid, "|", &sptr2); + ecid = strtok_r(NULL, "|", &sptr2); modecid = strtok_r(NULL, ",", &sptr); if (!(mod = epggrab_module_find_by_id(modid))) @@ -413,8 +423,10 @@ build_record_channel ( channel_t *ch ) htsmsg_add_str(c, "name", ch->ch_name); htsmsg_add_u32(c, "chid", ch->ch_id); - if(ch->ch_icon != NULL) + if(ch->ch_icon != NULL) { + htsmsg_add_imageurl(c, "chicon", "imagecache/%d", ch->ch_icon); htsmsg_add_str(c, "ch_icon", ch->ch_icon); + } buf[0] = 0; LIST_FOREACH(ctm, &ch->ch_ctms, ctm_channel_link) { @@ -468,10 +480,10 @@ extjs_channels(http_connection_t *hc, const char *remain, void *opaque) if(op == NULL) return 400; - htsmsg_autodtor(in) = + htsmsg_t *in = entries != NULL ? htsmsg_json_deserialize(entries) : NULL; - htsmsg_autodtor(out) = htsmsg_create_map(); + htsmsg_t *out = htsmsg_create_map(); scopedgloballock(); @@ -485,6 +497,7 @@ extjs_channels(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_msg(out, "entries", array); } else if(!strcmp(op, "create")) { + htsmsg_destroy(out); out = build_record_channel(channel_create()); } else if(!strcmp(op, "delete") && in != NULL) { @@ -494,11 +507,15 @@ extjs_channels(http_connection_t *hc, const char *remain, void *opaque) extjs_channels_update(in); } else { + htsmsg_destroy(in); + htsmsg_destroy(out); return 400; } htsmsg_json_serialize(out, hq, 0); http_output_content(hc, "text/x-json; charset=UTF-8"); + htsmsg_destroy(in); + htsmsg_destroy(out); return 0; } @@ -573,6 +590,7 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_u32(r, "channel_rename", epggrab_channel_rename); htsmsg_add_u32(r, "channel_renumber", epggrab_channel_renumber); htsmsg_add_u32(r, "channel_reicon", epggrab_channel_reicon); + htsmsg_add_u32(r, "epgdb_periodicsave", epggrab_epgdb_periodicsave / 3600); pthread_mutex_unlock(&epggrab_mutex); out = json_single_record(r, "epggrabSettings"); @@ -603,6 +621,8 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque) save |= epggrab_set_channel_renumber(str ? 1 : 0); str = http_arg_get(&hc->hc_req_args, "channel_reicon"); save |= epggrab_set_channel_reicon(str ? 1 : 0); + if ( (str = http_arg_get(&hc->hc_req_args, "epgdb_periodicsave")) ) + save |= epggrab_set_periodicsave(atoi(str) * 3600); if ( (str = http_arg_get(&hc->hc_req_args, "interval")) ) save |= epggrab_set_interval(atoi(str)); if ( (str = http_arg_get(&hc->hc_req_args, "module")) ) @@ -617,6 +637,7 @@ extjs_epggrab(http_connection_t *hc, const char *remain, void *opaque) if ( str ) save |= epggrab_enable_module_by_id(str, u32); } } + htsmsg_destroy(array); } } if (save) epggrab_save(); @@ -727,6 +748,47 @@ skip: } + +/** + * + */ +static int +extjs_dvr_containers(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + const char *op = http_arg_get(&hc->hc_req_args, "op"); + htsmsg_t *out, *array; + + pthread_mutex_lock(&global_lock); + + if(op != NULL && !strcmp(op, "list")) { + + out = htsmsg_create_map(); + array = htsmsg_create_list(); + + if (http_access_verify(hc, ACCESS_RECORDER_ALL)) + goto skip; + + muxer_container_list(array); + +skip: + htsmsg_add_msg(out, "entries", array); + + } else { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_BAD_REQUEST; + } + + pthread_mutex_unlock(&global_lock); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; + +} + + /** * */ @@ -850,7 +912,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_str(m, "channel", ch->ch_name); htsmsg_add_u32(m, "channelid", ch->ch_id); if(ch->ch_icon != NULL) - htsmsg_add_str(m, "chicon", ch->ch_icon); + htsmsg_add_imageurl(m, "chicon", "imagecache/%d", ch->ch_icon); if((s = epg_episode_get_title(ee, lang))) htsmsg_add_str(m, "title", s); @@ -932,7 +994,8 @@ extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque) m = htsmsg_create_map(); htsmsg_add_u32(m, "id", ebc->id); if ( ch->ch_name ) htsmsg_add_str(m, "channel", ch->ch_name); - if ( ch->ch_icon ) htsmsg_add_str(m, "chicon", ch->ch_icon); + if (ch->ch_icon) + htsmsg_add_imageurl(m, "chicon", "imagecache/%d", ch->ch_icon); htsmsg_add_u32(m, "start", ebc->start); htsmsg_add_msg(array, NULL, m); } @@ -1201,6 +1264,7 @@ extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_u32(r, "episodeInTitle", !!(cfg->dvr_flags & DVR_EPISODE_IN_TITLE)); htsmsg_add_u32(r, "cleanTitle", !!(cfg->dvr_flags & DVR_CLEAN_TITLE)); htsmsg_add_u32(r, "tagFiles", !!(cfg->dvr_flags & DVR_TAG_FILES)); + htsmsg_add_u32(r, "commSkip", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS)); out = json_single_record(r, "dvrSettings"); @@ -1251,6 +1315,9 @@ extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) flags |= DVR_EPISODE_IN_TITLE; if(http_arg_get(&hc->hc_req_args, "tagFiles") != NULL) flags |= DVR_TAG_FILES; + if(http_arg_get(&hc->hc_req_args, "commSkip") != NULL) + flags |= DVR_SKIP_COMMERCIALS; + dvr_flags_set(cfg,flags); @@ -1284,7 +1351,8 @@ extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) * */ static int -extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) +extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque, + dvr_entry_filter filter, dvr_entry_comparator cmp) { htsbuf_queue_t *hq = &hc->hc_reply; htsmsg_t *out, *array, *m; @@ -1292,7 +1360,7 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) dvr_entry_t *de; int start = 0, end, limit, i; const char *s; - off_t fsize; + int64_t fsize = 0; char buf[100]; if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL) @@ -1314,9 +1382,9 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) array = htsmsg_create_list(); - dvr_query(&dqr); + dvr_query_filter(&dqr, filter); - dvr_query_sort(&dqr); + dvr_query_sort_cmp(&dqr, cmp); htsmsg_add_u32(out, "totalCount", dqr.dqr_entries); @@ -1328,10 +1396,11 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) m = htsmsg_create_map(); + htsmsg_add_str(m, "channel", DVR_CH_NAME(de)); if(de->de_channel != NULL) { - htsmsg_add_str(m, "channel", de->de_channel->ch_name); - if(de->de_channel->ch_icon != NULL) - htsmsg_add_str(m, "chicon", de->de_channel->ch_icon); + if (de->de_channel->ch_icon) + htsmsg_add_imageurl(m, "chicon", "imagecache/%d", + de->de_channel->ch_icon); } htsmsg_add_str(m, "config_name", de->de_config_name); @@ -1361,16 +1430,14 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) if(de->de_sched_state == DVR_COMPLETED) { fsize = dvr_get_filesize(de); - if(fsize > 0) { - char url[100]; - htsmsg_add_s64(m, "filesize", fsize); - - snprintf(url, sizeof(url), "dvrfile/%d", de->de_id); - htsmsg_add_str(m, "url", url); + if (fsize > 0) { + char url[100]; + htsmsg_add_s64(m, "filesize", fsize); + snprintf(url, sizeof(url), "dvrfile/%d", de->de_id); + htsmsg_add_str(m, "url", url); } } - htsmsg_add_msg(array, NULL, m); } @@ -1386,6 +1453,80 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) return 0; } +static int is_dvr_entry_finished(dvr_entry_t *entry) +{ + dvr_entry_sched_state_t state = entry->de_sched_state; + return state == DVR_COMPLETED && !entry->de_last_error && dvr_get_filesize(entry) != -1; +} + +static int is_dvr_entry_upcoming(dvr_entry_t *entry) +{ + dvr_entry_sched_state_t state = entry->de_sched_state; + return state == DVR_RECORDING || state == DVR_SCHEDULED; +} + + +static int is_dvr_entry_failed(dvr_entry_t *entry) +{ + if (is_dvr_entry_finished(entry)) + return 0; + if (is_dvr_entry_upcoming(entry)) + return 0; + return 1; +} + +static int +extjs_dvrlist_finished(http_connection_t *hc, const char *remain, void *opaque) +{ + return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_finished, dvr_sort_start_descending); +} + +static int +extjs_dvrlist_upcoming(http_connection_t *hc, const char *remain, void *opaque) +{ + return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_upcoming, dvr_sort_start_ascending); +} + +static int +extjs_dvrlist_failed(http_connection_t *hc, const char *remain, void *opaque) +{ + return extjs_dvrlist(hc, remain, opaque, is_dvr_entry_failed, dvr_sort_start_descending); +} + +/** + * + */ +static int +extjs_subscriptions(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *out, *array; + th_subscription_t *s; + + pthread_mutex_lock(&global_lock); + + if(http_access_verify(hc, ACCESS_ADMIN)) { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_UNAUTHORIZED; + } + + out = htsmsg_create_map(); + array = htsmsg_create_list(); + + LIST_FOREACH(s, &subscriptions, ths_global_link) + htsmsg_add_msg(array, NULL, subscription_create_msg(s)); + + pthread_mutex_unlock(&global_lock); + + htsmsg_add_msg(out, "entries", array); + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; +} + + /** * */ @@ -1431,6 +1572,9 @@ service_update(htsmsg_t *in) if((chname = htsmsg_get_str(c, "channelname")) != NULL) service_map_channel(t, channel_find_by_name(chname, 1, 0), 1); + if(!htsmsg_get_u32(c, "prefcapid", &u32)) + service_set_prefcapid(t, u32); + if((dvb_charset = htsmsg_get_str(c, "dvb_charset")) != NULL) service_set_dvb_charset(t, dvb_charset); @@ -1490,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: @@ -1762,6 +1912,9 @@ extjs_service_update(htsmsg_t *in) if(!htsmsg_get_u32(c, "enabled", &u32)) service_set_enable(t, u32); + if(!htsmsg_get_u32(c, "prefcapid", &u32)) + service_set_prefcapid(t, u32); + if((chname = htsmsg_get_str(c, "channelname")) != NULL) service_map_channel(t, channel_find_by_name(chname, 1, 0), 1); @@ -1828,24 +1981,72 @@ extjs_config(http_connection_t *hc, const char *remain, void *opaque) pthread_mutex_unlock(&global_lock); - /* Basic settings (not the advanced schedule) */ + /* Basic settings */ if(!strcmp(op, "loadSettings")) { + + /* Misc */ pthread_mutex_lock(&global_lock); m = config_get_all(); + + /* Time */ + htsmsg_add_u32(m, "tvhtime_update_enabled", tvhtime_update_enabled); + htsmsg_add_u32(m, "tvhtime_ntp_enabled", tvhtime_ntp_enabled); + htsmsg_add_u32(m, "tvhtime_tolerance", tvhtime_tolerance); + pthread_mutex_unlock(&global_lock); + + /* Image cache */ +#if ENABLE_IMAGECACHE + pthread_mutex_lock(&imagecache_mutex); + htsmsg_add_u32(m, "imagecache_enabled", imagecache_enabled); + htsmsg_add_u32(m, "imagecache_ok_period", imagecache_ok_period); + htsmsg_add_u32(m, "imagecache_fail_period", imagecache_fail_period); + htsmsg_add_u32(m, "imagecache_ignore_sslcert", imagecache_ignore_sslcert); + pthread_mutex_unlock(&imagecache_mutex); +#endif + if (!m) return HTTP_STATUS_BAD_REQUEST; out = json_single_record(m, "config"); /* Save settings */ } else if (!strcmp(op, "saveSettings") ) { int save = 0; + + /* Misc settings */ pthread_mutex_lock(&global_lock); if ((str = http_arg_get(&hc->hc_req_args, "muxconfpath"))) save |= config_set_muxconfpath(str); if ((str = http_arg_get(&hc->hc_req_args, "language"))) save |= config_set_language(str); - if (save) config_save(); + if (save) + config_save(); + + /* Time */ + if ((str = http_arg_get(&hc->hc_req_args, "tvhtime_update_enabled"))) + tvhtime_set_update_enabled(!!str); + if ((str = http_arg_get(&hc->hc_req_args, "tvhtime_ntp_enabled"))) + tvhtime_set_ntp_enabled(!!str); + if ((str = http_arg_get(&hc->hc_req_args, "tvhtime_tolerance"))) + tvhtime_set_tolerance(atoi(str)); + pthread_mutex_unlock(&global_lock); + + /* Image Cache */ +#if ENABLE_IMAGECACHE + pthread_mutex_lock(&imagecache_mutex); + str = http_arg_get(&hc->hc_req_args, "imagecache_enabled"); + save = imagecache_set_enabled(!!str); + if ((str = http_arg_get(&hc->hc_req_args, "imagecache_ok_period"))) + save |= imagecache_set_ok_period(atoi(str)); + if ((str = http_arg_get(&hc->hc_req_args, "imagecache_fail_period"))) + save |= imagecache_set_fail_period(atoi(str)); + str = http_arg_get(&hc->hc_req_args, "imagecache_ignore_sslcert"); + save |= imagecache_set_ignore_sslcert(!!str); + if (save) + imagecache_save(); + pthread_mutex_unlock(&imagecache_mutex); +#endif + out = htsmsg_create_map(); htsmsg_add_u32(out, "success", 1); @@ -1860,31 +2061,210 @@ extjs_config(http_connection_t *hc, const char *remain, void *opaque) return 0; } +/** + * + */ +static int +extjs_tvhlog(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + const char *op = http_arg_get(&hc->hc_req_args, "op"); + htsmsg_t *out, *m; + + if(op == NULL) + return 400; + + pthread_mutex_lock(&global_lock); + + if(http_access_verify(hc, ACCESS_ADMIN)) { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_UNAUTHORIZED; + } + + pthread_mutex_unlock(&global_lock); + + /* Basic settings */ + if(!strcmp(op, "loadSettings")) { + char str[2048]; + + /* Get config */ + pthread_mutex_lock(&tvhlog_mutex); + m = htsmsg_create_map(); + htsmsg_add_u32(m, "tvhlog_level", tvhlog_level); + htsmsg_add_u32(m, "tvhlog_trace", tvhlog_level > LOG_DEBUG); + tvhlog_get_subsys(str, sizeof(str)); + htsmsg_add_str(m, "tvhlog_subsys", str); + htsmsg_add_str(m, "tvhlog_path", tvhlog_path ?: ""); + htsmsg_add_u32(m, "tvhlog_options", tvhlog_options); + htsmsg_add_u32(m, "tvhlog_dbg_syslog", + tvhlog_options & TVHLOG_OPT_DBG_SYSLOG); + pthread_mutex_unlock(&tvhlog_mutex); + + if (!m) return HTTP_STATUS_BAD_REQUEST; + out = json_single_record(m, "config"); + + /* Save settings */ + } else if (!strcmp(op, "saveSettings") ) { + const char *str; + + pthread_mutex_lock(&tvhlog_mutex); + if ((str = http_arg_get(&hc->hc_req_args, "tvhlog_level"))) + tvhlog_level = atoi(str); + if ((str = http_arg_get(&hc->hc_req_args, "tvhlog_trace"))) + tvhlog_level = LOG_TRACE; + else + tvhlog_level = LOG_DEBUG; + if ((str = http_arg_get(&hc->hc_req_args, "tvhlog_path"))) { + free(tvhlog_path); + if (*str) + tvhlog_path = strdup(str); + else + tvhlog_path = NULL; + } + if ((str = http_arg_get(&hc->hc_req_args, "tvhlog_options"))) + tvhlog_options = atoi(str); + if ((str = http_arg_get(&hc->hc_req_args, "tvhlog_dbg_syslog"))) + tvhlog_options |= TVHLOG_OPT_DBG_SYSLOG; + else + tvhlog_options &= ~TVHLOG_OPT_DBG_SYSLOG; + tvhlog_set_subsys(http_arg_get(&hc->hc_req_args, "tvhlog_subsys")); + pthread_mutex_unlock(&tvhlog_mutex); + + out = htsmsg_create_map(); + htsmsg_add_u32(out, "success", 1); + + } else { + return HTTP_STATUS_BAD_REQUEST; + } + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + + return 0; +} + +/** + * Capability check + */ +static int +extjs_capabilities(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *l = tvheadend_capabilities_list(0); + htsmsg_json_serialize(l, hq, 0); + htsmsg_destroy(l); + http_output_content(hc, "text/x-json; charset=UTF-8"); + return 0; +} + +/** + * + */ +#if ENABLE_TIMESHIFT +static int +extjs_timeshift(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + const char *op = http_arg_get(&hc->hc_req_args, "op"); + htsmsg_t *out, *m; + const char *str; + + if(op == NULL) + return 400; + + pthread_mutex_lock(&global_lock); + + if(http_access_verify(hc, ACCESS_ADMIN)) { + pthread_mutex_unlock(&global_lock); + return HTTP_STATUS_UNAUTHORIZED; + } + + pthread_mutex_unlock(&global_lock); + + /* Basic settings (not the advanced schedule) */ + if(!strcmp(op, "loadSettings")) { + pthread_mutex_lock(&global_lock); + m = htsmsg_create_map(); + htsmsg_add_u32(m, "timeshift_enabled", timeshift_enabled); + htsmsg_add_u32(m, "timeshift_ondemand", timeshift_ondemand); + if (timeshift_path) + htsmsg_add_str(m, "timeshift_path", timeshift_path); + htsmsg_add_u32(m, "timeshift_unlimited_period", timeshift_unlimited_period); + htsmsg_add_u32(m, "timeshift_max_period", timeshift_max_period / 60); + htsmsg_add_u32(m, "timeshift_unlimited_size", timeshift_unlimited_size); + htsmsg_add_u32(m, "timeshift_max_size", timeshift_max_size / 1048576); + pthread_mutex_unlock(&global_lock); + out = json_single_record(m, "config"); + + /* Save settings */ + } else if (!strcmp(op, "saveSettings") ) { + pthread_mutex_lock(&global_lock); + timeshift_enabled = http_arg_get(&hc->hc_req_args, "timeshift_enabled") ? 1 : 0; + timeshift_ondemand = http_arg_get(&hc->hc_req_args, "timeshift_ondemand") ? 1 : 0; + if ((str = http_arg_get(&hc->hc_req_args, "timeshift_path"))) { + if (timeshift_path) + free(timeshift_path); + timeshift_path = strdup(str); + } + timeshift_unlimited_period = http_arg_get(&hc->hc_req_args, "timeshift_unlimited_period") ? 1 : 0; + if ((str = http_arg_get(&hc->hc_req_args, "timeshift_max_period"))) + timeshift_max_period = (uint32_t)atol(str) * 60; + timeshift_unlimited_size = http_arg_get(&hc->hc_req_args, "timeshift_unlimited_size") ? 1 : 0; + if ((str = http_arg_get(&hc->hc_req_args, "timeshift_max_size"))) + timeshift_max_size = atol(str) * 1048576LL; + timeshift_save(); + pthread_mutex_unlock(&global_lock); + + out = htsmsg_create_map(); + htsmsg_add_u32(out, "success", 1); + + } else { + return HTTP_STATUS_BAD_REQUEST; + } + + htsmsg_json_serialize(out, hq, 0); + htsmsg_destroy(out); + http_output_content(hc, "text/x-json; charset=UTF-8"); + + return 0; +} +#endif + /** * WEB user interface */ void extjs_start(void) { - http_path_add("/about.html", NULL, page_about, ACCESS_WEB_INTERFACE); - http_path_add("/extjs.html", NULL, extjs_root, ACCESS_WEB_INTERFACE); - http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); - http_path_add("/channels", NULL, extjs_channels, ACCESS_WEB_INTERFACE); - http_path_add("/epggrab", NULL, extjs_epggrab, ACCESS_WEB_INTERFACE); - http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); - http_path_add("/confignames", NULL, extjs_confignames, ACCESS_WEB_INTERFACE); - http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); - http_path_add("/epgrelated", NULL, extjs_epgrelated, ACCESS_WEB_INTERFACE); - http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); - http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); - http_path_add("/dvrlist", NULL, extjs_dvrlist, ACCESS_WEB_INTERFACE); - http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); - http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); - http_path_add("/languages", NULL, extjs_languages, ACCESS_WEB_INTERFACE); - http_path_add("/mergechannel", NULL, extjs_mergechannel, ACCESS_ADMIN); - http_path_add("/iptv/services", NULL, extjs_iptvservices, ACCESS_ADMIN); - http_path_add("/servicedetails", NULL, extjs_servicedetails, ACCESS_ADMIN); - http_path_add("/tv/adapter", NULL, extjs_tvadapter, ACCESS_ADMIN); + http_path_add("/about.html", NULL, page_about, ACCESS_WEB_INTERFACE); + http_path_add("/extjs.html", NULL, extjs_root, ACCESS_WEB_INTERFACE); + http_path_add("/capabilities", NULL, extjs_capabilities, ACCESS_WEB_INTERFACE); + http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); + http_path_add("/channels", NULL, extjs_channels, ACCESS_WEB_INTERFACE); + http_path_add("/epggrab", NULL, extjs_epggrab, ACCESS_WEB_INTERFACE); + http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); + http_path_add("/confignames", NULL, extjs_confignames, ACCESS_WEB_INTERFACE); + http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); + http_path_add("/epgrelated", NULL, extjs_epgrelated, ACCESS_WEB_INTERFACE); + http_path_add("/epgobject", NULL, extjs_epgobject, ACCESS_WEB_INTERFACE); + http_path_add("/dvr", NULL, extjs_dvr, ACCESS_WEB_INTERFACE); + http_path_add("/dvrlist_upcoming", NULL, extjs_dvrlist_upcoming, ACCESS_WEB_INTERFACE); + http_path_add("/dvrlist_finished", NULL, extjs_dvrlist_finished, ACCESS_WEB_INTERFACE); + http_path_add("/dvrlist_failed", NULL, extjs_dvrlist_failed, ACCESS_WEB_INTERFACE); + http_path_add("/dvr_containers", NULL, extjs_dvr_containers, ACCESS_WEB_INTERFACE); + http_path_add("/subscriptions", NULL, extjs_subscriptions, ACCESS_WEB_INTERFACE); + http_path_add("/ecglist", NULL, extjs_ecglist, ACCESS_WEB_INTERFACE); + http_path_add("/config", NULL, extjs_config, ACCESS_WEB_INTERFACE); + http_path_add("/languages", NULL, extjs_languages, ACCESS_WEB_INTERFACE); + http_path_add("/mergechannel", NULL, extjs_mergechannel, ACCESS_ADMIN); + http_path_add("/iptv/services", NULL, extjs_iptvservices, ACCESS_ADMIN); + http_path_add("/servicedetails", NULL, extjs_servicedetails, ACCESS_ADMIN); + http_path_add("/tv/adapter", NULL, extjs_tvadapter, ACCESS_ADMIN); +#if ENABLE_TIMESHIFT + http_path_add("/timeshift", NULL, extjs_timeshift, ACCESS_ADMIN); +#endif + http_path_add("/tvhlog", NULL, extjs_tvhlog, ACCESS_ADMIN); #if ENABLE_LINUXDVB extjs_start_dvb(); diff --git a/src/webui/extjs_dvb.c b/src/webui/extjs_dvb.c index 94adc399..4aefa6b7 100644 --- a/src/webui/extjs_dvb.c +++ b/src/webui/extjs_dvb.c @@ -145,6 +145,7 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) if(!strcmp(op, "load")) { r = htsmsg_create_map(); htsmsg_add_str(r, "id", tda->tda_identifier); + htsmsg_add_u32(r, "enabled", tda->tda_enabled); htsmsg_add_str(r, "device", tda->tda_rootpath ?: "No hardware attached"); htsmsg_add_str(r, "name", tda->tda_displayname); htsmsg_add_u32(r, "automux", tda->tda_autodiscovery); @@ -153,15 +154,19 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_u32(r, "idleclose", tda->tda_idleclose); htsmsg_add_u32(r, "skip_checksubscr", tda->tda_skip_checksubscr); htsmsg_add_u32(r, "qmon", tda->tda_qmon); - htsmsg_add_u32(r, "dumpmux", tda->tda_dump_muxes); htsmsg_add_u32(r, "poweroff", tda->tda_poweroff); htsmsg_add_u32(r, "sidtochan", tda->tda_sidtochan); htsmsg_add_u32(r, "nitoid", tda->tda_nitoid); htsmsg_add_u32(r, "disable_pmt_monitor", tda->tda_disable_pmt_monitor); + htsmsg_add_u32(r, "full_mux_rx", tda->tda_full_mux_rx+1); + htsmsg_add_u32(r, "grace_period", tda->tda_grace_period); htsmsg_add_str(r, "diseqcversion", ((const char *[]){"DiSEqC 1.0 / 2.0", "DiSEqC 1.1 / 2.1"}) [tda->tda_diseqc_version % 2]); + htsmsg_add_str(r, "diseqcrepeats", + ((const char *[]){"0", "1", "3"}) + [tda->tda_diseqc_repeats % 3]); htsmsg_add_u32(r, "extrapriority", tda->tda_extrapriority); out = json_single_record(r, "dvbadapters"); @@ -170,6 +175,9 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) if((s = http_arg_get(&hc->hc_req_args, "name")) != NULL) dvb_adapter_set_displayname(tda, s); + s = http_arg_get(&hc->hc_req_args, "enabled"); + dvb_adapter_set_enabled(tda, !!s); + s = http_arg_get(&hc->hc_req_args, "automux"); dvb_adapter_set_auto_discovery(tda, !!s); @@ -194,12 +202,15 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) s = http_arg_get(&hc->hc_req_args, "sidtochan"); dvb_adapter_set_sidtochan(tda, !!s); - s = http_arg_get(&hc->hc_req_args, "dumpmux"); - dvb_adapter_set_dump_muxes(tda, !!s); - s = http_arg_get(&hc->hc_req_args, "disable_pmt_monitor"); dvb_adapter_set_disable_pmt_monitor(tda, !!s); + s = http_arg_get(&hc->hc_req_args, "full_mux_rx"); + dvb_adapter_set_full_mux_rx(tda, atoi(s)-1); + + s = http_arg_get(&hc->hc_req_args, "grace_period"); + dvb_adapter_set_grace_period(tda, atoi(s)); + if((s = http_arg_get(&hc->hc_req_args, "nitoid")) != NULL) dvb_adapter_set_nitoid(tda, atoi(s)); @@ -210,6 +221,14 @@ extjs_dvbadapter(http_connection_t *hc, const char *remain, void *opaque) dvb_adapter_set_diseqc_version(tda, 1); } + if((s = http_arg_get(&hc->hc_req_args, "diseqcrepeats")) != NULL) { + if(!strcmp(s, "0")) + dvb_adapter_set_diseqc_repeats(tda, 0); + else if(!strcmp(s, "1")) + dvb_adapter_set_diseqc_repeats(tda, 1); + else if(!strcmp(s, "2")) + dvb_adapter_set_diseqc_repeats(tda, 2); + } if((s = http_arg_get(&hc->hc_req_args, "extrapriority")) != NULL) dvb_adapter_set_extrapriority(tda, atoi(s)); @@ -425,7 +444,7 @@ extjs_dvbservices(http_connection_t *hc, const char *remain, void *opaque) qsort(tvec, count, sizeof(service_t *), transportcmp); for(i = 0; i < count; i++) - htsmsg_add_msg(array, NULL, dvb_transport_build_msg(tvec[i])); + htsmsg_add_msg(array, NULL, dvb_service_build_msg(tvec[i])); htsmsg_add_msg(out, "entries", array); diff --git a/src/webui/simpleui.c b/src/webui/simpleui.c index 0950fd72..30f87699 100644 --- a/src/webui/simpleui.c +++ b/src/webui/simpleui.c @@ -322,7 +322,7 @@ page_pvrinfo(http_connection_t *hc, const char *remain, void *opaque) a.tm_hour, a.tm_min, b.tm_hour, b.tm_min); htsbuf_qprintf(hq, "
\"%s\": \"%s\"

", - de->de_channel->ch_name, lang_str_get(de->de_title, NULL)); + DVR_CH_NAME(de), lang_str_get(de->de_title, NULL)); if((rstatus = val2str(de->de_sched_state, recstatustxt)) != NULL) htsbuf_qprintf(hq, "Recording status: %s
", rstatus); @@ -471,6 +471,27 @@ page_status(http_connection_t *hc, return 0; } +/** + * flush epgdb to disk on call + */ +static int +page_epgsave(http_connection_t *hc, + const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + + htsbuf_qprintf(hq, "\n" + "1\n"); + + pthread_mutex_lock(&global_lock); + epg_save(NULL); + pthread_mutex_unlock(&global_lock); + + http_output_content(hc, "text/xml"); + + return 0; +} + /** @@ -482,7 +503,6 @@ simpleui_start(void) http_path_add("/simple.html", NULL, page_simple, ACCESS_SIMPLE); http_path_add("/eventinfo", NULL, page_einfo, ACCESS_SIMPLE); http_path_add("/pvrinfo", NULL, page_pvrinfo, ACCESS_SIMPLE); - http_path_add("/status.xml", NULL, page_status, ACCESS_SIMPLE); + http_path_add("/status.xml", NULL, page_status, ACCESS_SIMPLE); + http_path_add("/epgsave", NULL, page_epgsave, ACCESS_SIMPLE); } - - diff --git a/src/webui/statedump.c b/src/webui/statedump.c index a9758e6f..603fd158 100644 --- a/src/webui/statedump.c +++ b/src/webui/statedump.c @@ -29,6 +29,7 @@ #include "access.h" #include "epg.h" #include "psi.h" +#include "channels.h" #if ENABLE_LINUXDVB #include "dvr/dvr.h" #include "dvb/dvb.h" diff --git a/src/webui/static/app/capmteditor.js b/src/webui/static/app/capmteditor.js index 9503c599..6f0c73ea 100644 --- a/src/webui/static/app/capmteditor.js +++ b/src/webui/static/app/capmteditor.js @@ -7,6 +7,12 @@ tvheadend.capmteditor = function() { width : 60 }); + var oscamColumn = new Ext.grid.CheckColumn({ + header : "OSCam mode", + dataIndex : 'oscam', + width : 60 + }); + function setMetaAttr(meta, record) { var enabled = record.get('enabled'); if (!enabled) return; @@ -43,7 +49,7 @@ tvheadend.capmteditor = function() { editor : new fm.TextField({ allowBlank : false }) - }, { + }, oscamColumn, { header : "Comment", dataIndex : 'comment', width : 400, @@ -55,7 +61,7 @@ tvheadend.capmteditor = function() { } ]}); var rec = Ext.data.Record.create([ 'enabled', 'connected', 'camdfilename', - 'port', 'comment' ]); + 'port', 'oscam', 'comment' ]); store = new Ext.data.JsonStore({ root : 'entries', @@ -77,5 +83,5 @@ tvheadend.capmteditor = function() { }); return new tvheadend.tableEditor('Capmt Connections', 'capmt', cm, rec, - [ enabledColumn ], store, 'config_capmt.html', 'key'); + [ enabledColumn, oscamColumn ], store, 'config_capmt.html', 'key'); } diff --git a/src/webui/static/app/config.js b/src/webui/static/app/config.js index 2c9b6a86..a7d84bbb 100644 --- a/src/webui/static/app/config.js +++ b/src/webui/static/app/config.js @@ -37,12 +37,20 @@ tvheadend.miscconf = function() { */ var confreader = new Ext.data.JsonReader({ root : 'config' - }, [ 'muxconfpath', 'language' ]); + }, [ 'muxconfpath', 'language', + 'imagecache_enabled', 'imagecache_ok_period', + 'imagecache_fail_period', 'imagecache_ignore_sslcert', + 'tvhtime_update_enabled', 'tvhtime_ntp_enabled', + 'tvhtime_tolerance']); /* **************************************************************** * Form Fields * ***************************************************************/ + /* + * DVB path + */ + var dvbscanPath = new Ext.form.TextField({ fieldLabel : 'DVB scan files path', name : 'muxconfpath', @@ -50,6 +58,10 @@ tvheadend.miscconf = function() { width: 400 }); + /* + * Language + */ + var language = new Ext.ux.ItemSelector({ name: 'language', fromStore: tvheadend.languages, @@ -65,6 +77,66 @@ tvheadend.miscconf = function() { fromLegend: 'Available' }); + /* + * Time/Date + */ + var tvhtimeUpdateEnabled = new Ext.form.Checkbox({ + name: 'tvhtime_update_enabled', + fieldLabel: 'Update time' + }); + + var tvhtimeNtpEnabled = new Ext.form.Checkbox({ + name: 'tvhtime_ntp_enabled', + fieldLabel: 'Enable NTP driver' + }); + + var tvhtimeTolerance = new Ext.form.NumberField({ + name: 'tvhtime_tolerance', + fieldLabel: 'Update tolerance (ms)' + }); + + var tvhtimePanel = new Ext.form.FieldSet({ + title: 'Time Update', + width: 700, + autoHeight: true, + collapsible: true, + items : [ tvhtimeUpdateEnabled, tvhtimeNtpEnabled, tvhtimeTolerance ] + }); + + /* + * Image cache + */ + var imagecacheEnabled = new Ext.form.Checkbox({ + name: 'imagecache_enabled', + fieldLabel: 'Enabled', + }); + + var imagecacheOkPeriod = new Ext.form.NumberField({ + name: 'imagecache_ok_period', + fieldLabel: 'Re-fetch period (hours)' + }); + + var imagecacheFailPeriod = new Ext.form.NumberField({ + name: 'imagecache_fail_period', + fieldLabel: 'Re-try period (hours)', + }); + + var imagecacheIgnoreSSLCert = new Ext.form.Checkbox({ + name: 'imagecache_ignore_sslcert', + fieldLabel: 'Ignore invalid SSL certificate' + }); + + var imagecachePanel = new Ext.form.FieldSet({ + title: 'Image Caching', + width: 700, + autoHeight: true, + collapsible: true, + items : [ imagecacheEnabled, imagecacheOkPeriod, imagecacheFailPeriod, + imagecacheIgnoreSSLCert ] + }); + if (tvheadend.capabilities.indexOf('imagecache') == -1) + imagecachePanel.hide(); + /* **************************************************************** * Form * ***************************************************************/ @@ -89,13 +161,14 @@ tvheadend.miscconf = function() { border : false, bodyStyle : 'padding:15px', labelAlign : 'left', - labelWidth : 150, + labelWidth : 200, waitMsgTarget : true, reader : confreader, layout : 'form', defaultType : 'textfield', autoHeight : true, - items : [ language, dvbscanPath ], + items : [ language, dvbscanPath, + imagecachePanel, tvhtimePanel ], tbar : [ saveButton, '->', helpButton ] }); diff --git a/src/webui/static/app/dvb.js b/src/webui/static/app/dvb.js index a71e7ebb..4d423d3f 100644 --- a/src/webui/static/app/dvb.js +++ b/src/webui/static/app/dvb.js @@ -24,6 +24,14 @@ tvheadend.dvb_muxes = function(adapterData, satConfStore) { var cmlist = Array(); cmlist.push(enabledColumn, { + header : "Play", + dataIndex : 'id', + width : 50, + renderer : function(value, metadata, record, row, col, store) { + url = 'stream/mux/' + value + return 'Play' + } + }, { header : "Network", dataIndex : 'network', width : 200 @@ -112,6 +120,9 @@ tvheadend.dvb_muxes = function(adapterData, satConfStore) { tvheadend.comet.on('dvbMux', function(m) { + if(m.adapterId !== adapterId) + return; + r = store.getById(m.id) if (typeof r === 'undefined') { store.reload(); @@ -375,8 +386,10 @@ tvheadend.dvb_muxes = function(adapterData, satConfStore) { /** * DVB service grid */ -tvheadend.dvb_services = function(adapterId) { +tvheadend.dvb_services = function(adapterData, satConfStore) { + adapterId = adapterData.identifier; + var fm = Ext.form; var enabledColumn = new Ext.grid.CheckColumn({ @@ -410,10 +423,10 @@ tvheadend.dvb_services = function(adapterId) { } ] }); - var cm = new Ext.grid.ColumnModel({ - defaultSortable: true, - columns: [ - enabledColumn, + + var cmlist = Array(); + + cmlist.push(enabledColumn, { header : "Service name", dataIndex : 'svcname', @@ -497,15 +510,55 @@ tvheadend.dvb_services = function(adapterId) { header : "Network", dataIndex : 'network', width : 100 + }, { + header : "Encryption", + dataIndex : 'encryption', + width : 100 }, { header : "Multiplex", dataIndex : 'mux', width : 100 - }, { + }); + + if (adapterData.satConf) { + // Include DVB-S specific stuff + + satConfStore.on('update', function(s, r, c) { + if (grid.rendered) grid.getView().refresh(); + }); + + satConfStore.on('load', function(s, r, o) { + if (grid.rendered) grid.getView().refresh(); + }); + + tvheadend.comet.on('dvbSatConf', function(m) { + if (m.adapterId == adapterId) satConfStore.reload(); + }); + + cmlist.push( + { + header : "Satellite config", + dataIndex : 'satconf', + width : 100, + renderer : function(value, metadata, record, row, col, store) { + r = satConfStore.getById(value); + return typeof r === 'undefined' ? 'Unset' + : r.data.name; + } + }); + } + + cmlist.push( + { header : "Service ID", dataIndex : 'sid', width : 50, hidden : true + }, { + header: "Preferred CA pid", + dataIndex: 'prefcapid', + width: 50, + editor: new fm.TextField({allowBlank: true}) }, { header : "PMT PID", dataIndex : 'pmt', @@ -516,13 +569,17 @@ tvheadend.dvb_services = function(adapterId) { dataIndex : 'pcr', width : 50, hidden : true - }, actions ]}); + }, actions ); + + var cm = new Ext.grid.ColumnModel({ + columns: cmlist, + defaultSortable: true}); var store = new Ext.data.JsonStore({ root : 'entries', fields : Ext.data.Record.create([ 'id', 'enabled', 'type', 'sid', 'pmt', - 'pcr', 'svcname', 'network', 'provider', 'mux', 'channelname', - 'dvb_charset', 'dvb_eit_enable' ]), + 'pcr', 'svcname', 'network', 'provider', 'encryption', 'mux', 'satconf', + 'channelname', 'prefcapid', 'dvb_charset', 'dvb_eit_enable' ]), url : "dvb/services/" + adapterId, autoLoad : true, id : 'id', @@ -557,7 +614,6 @@ tvheadend.dvb_services = function(adapterId) { 'Please select at least one item to delete'); } } - ; function saveChanges() { var mr = store.getModifiedRecords(); @@ -583,6 +639,14 @@ tvheadend.dvb_services = function(adapterId) { }); } + function mapSelected() { + grid.selModel.each(function(rec) { + if(!rec.get('channelname')) + rec.set('channelname', rec.get('svcname')); + return true; + }); + } + var saveBtn = new Ext.Toolbar.Button({ tooltip : 'Save any changes made (Changed cells have red borders).', iconCls : 'save', @@ -601,10 +665,22 @@ tvheadend.dvb_services = function(adapterId) { disabled : true }); + var mapBtn = new Ext.Toolbar.Button({ + tooltip : 'Map selected services to channels based on their name. Does nothing if selected item is already mapped.', + iconCls : 'clone', + text : "Map selected", + handler : mapSelected, + disabled : true + }); + var selModel = new Ext.grid.RowSelectionModel({ singleSelect : false }); + selModel.on('selectionchange', function(s) { + mapBtn.setDisabled(s.getCount() == 0); + }); + var grid = new Ext.grid.EditorGridPanel({ stripeRows : true, title : 'Services', @@ -616,7 +692,7 @@ tvheadend.dvb_services = function(adapterId) { forceFit : true }, selModel : selModel, - tbar : [ saveBtn, rejectBtn ] + tbar : [ saveBtn, rejectBtn, '-', mapBtn ] }); return grid; } @@ -716,6 +792,36 @@ tvheadend.addMuxManually = function(adapterData, satConfStore) { var items = []; switch (adapterData.deliverySystem) { + case 'ATSC': + items.push(new Ext.form.NumberField({ + fieldLabel : 'Frequency (kHz)', + name : 'frequency', + allowNegative : false, + allowBlank : false, + minValue : adapterData.freqMin, + maxValue : adapterData.freqMax + })); + + items.push(new Ext.form.ComboBox({ + fieldLabel : 'Modulation', + name : 'constellation', + hiddenName : 'constellationID', + editable : false, + allowBlank : false, + displayField : 'title', + valueField : 'id', + mode : 'remote', + triggerAction : 'all', + store : new Ext.data.JsonStore({ + root : 'entries', + fields : [ 'title', 'id' ], + url : 'dvb/feopts/constellations/' + adId + }) + })); + + break; + + case 'DVB-T': items.push(new Ext.form.NumberField({ @@ -1086,9 +1192,10 @@ tvheadend.dvb_adapter_general = function(adapterData, satConfStore) { var confreader = new Ext.data.JsonReader({ root : 'dvbadapters' - }, [ 'name', 'automux', 'skip_initialscan', 'idlescan', 'diseqcversion', - 'qmon', 'skip_checksubscr', 'dumpmux', 'poweroff', 'sidtochan', 'nitoid', - 'extrapriority', 'disable_pmt_monitor', 'idleclose' ]); + }, [ 'name', 'enabled', 'automux', 'skip_initialscan', 'idlescan', 'diseqcversion', + 'diseqcrepeats', 'qmon', 'skip_checksubscr', + 'poweroff', 'sidtochan', 'nitoid', 'extrapriority', + ,'disable_pmt_monitor', 'full_mux_rx', 'idleclose', 'grace_period' ]); function saveConfForm() { confform.getForm().submit({ @@ -1106,6 +1213,10 @@ tvheadend.dvb_adapter_general = function(adapterData, satConfStore) { name : 'name', width : 250 }, + new Ext.form.Checkbox({ + fieldLabel : 'Enabled', + name : 'enabled' + }), new Ext.form.Checkbox({ fieldLabel : 'Autodetect muxes', name : 'automux' @@ -1134,19 +1245,26 @@ tvheadend.dvb_adapter_general = function(adapterData, satConfStore) { fieldLabel : 'Monitor signal quality', name : 'qmon' }), + new Ext.form.ComboBox({ + fieldLabel : 'Full mux reception', + name : 'full_mux_rx', + hiddenName: 'full_mux_rx', + displayField: 'num', + valueField: 'str', + editable : false, + allowBlank : false, + mode : 'remote', + triggerAction : 'all', + fields: [ 'num', 'str' ], + store : [ [0, 'Auto'], [1, 'Off'], [2, 'On'] ] + }), + new Ext.form.NumberField({ + fieldLabel: 'Grace Period', + name: 'grace_period' + }), new Ext.form.Checkbox({ fieldLabel : 'Disable PMT monitoring', name : 'disable_pmt_monitor' - }), - new Ext.form.Checkbox({ - fieldLabel : 'Write full DVB MUX to disk', - name : 'dumpmux', - handler : function(s, v) { - if (v) Ext.MessageBox.alert('DVB Mux dump', - 'Please note that keeping this ' - + 'option enabled can consume a lot ' - + 'of diskspace. You have been warned'); - } }), { fieldLabel : 'Original Network ID', name : 'nitoid', @@ -1169,6 +1287,17 @@ tvheadend.dvb_adapter_general = function(adapterData, satConfStore) { }); items.push(v); + v = new Ext.form.ComboBox({ + name : 'diseqcrepeats', + fieldLabel : 'DiSEqC repeats', + editable : false, + allowBlank : false, + mode : 'remote', + triggerAction : 'all', + store : [ '0', '1', '2' ] + }); + items.push(v); + v = new Ext.form.Checkbox({ fieldLabel : 'Turn off LNB when idle', name : 'poweroff' @@ -1222,7 +1351,11 @@ tvheadend.dvb_adapter_general = function(adapterData, satConfStore) { + '

Status

' + '

Currently tuned to:

{currentMux} ' + '

Services:

{services}' + '

Muxes:

{muxes}' - + '

Muxes awaiting initial scan:

{initialMuxes}'); + + '

Muxes awaiting initial scan:

{initialMuxes}' + + '

Signal Strength:

{signal}%' + + '

Bit Error Rate:

{ber}/s' + + '

Uncorrected Bit Errors:

{uncavg}/s' + ); var infoPanel = new Ext.Panel({ title : 'Information and capabilities', @@ -1293,7 +1426,7 @@ tvheadend.dvb_satconf = function(adapterId, lnbStore) { dataIndex : 'port', editor : new fm.NumberField({ minValue : 0, - maxValue : 15 + maxValue : 63 }) }, { header : "LNB type", @@ -1349,7 +1482,7 @@ tvheadend.dvb_adapter = function(data) { var items = [ new tvheadend.dvb_adapter_general(data, satConfStore), new tvheadend.dvb_muxes(data, satConfStore), - new tvheadend.dvb_services(data.identifier) ]; + new tvheadend.dvb_services(data, satConfStore) ]; if (data.satConf) items.push(new tvheadend.dvb_satconf(data.identifier, lnbStore)); diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 72a5ed9e..02021eb3 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -14,13 +14,20 @@ tvheadend.dvrprio = new Ext.data.SimpleStore({ [ 'unimportant', 'Unimportant' ] ] }); + //For the container configuration -tvheadend.containers = new Ext.data.SimpleStore({ - fields : [ 'identifier', 'name' ], - id : 0, - data : [ [ 'matroska', 'Matroska' ], [ 'pass', 'TS (Pass-through)' ] ] +tvheadend.containers = new Ext.data.JsonStore({ + autoLoad : true, + root : 'entries', + fields : [ 'name', 'description' ], + id : 'name', + url : 'dvr_containers', + baseParams : { + op : 'list' + } }); + /** * Configuration names */ @@ -129,7 +136,6 @@ tvheadend.dvrDetails = function(entry) { success : function(response, options) { win.close(); - v }, failure : function(response, options) { @@ -143,7 +149,7 @@ tvheadend.dvrDetails = function(entry) { /** * */ -tvheadend.dvrschedule = function() { +tvheadend.dvrschedule = function(title, iconCls, dvrStore) { var actions = new Ext.ux.grid.RowActions({ header : '', @@ -176,6 +182,13 @@ tvheadend.dvrschedule = function() { } } + function renderSize(value) + { + if (value == null) + return ''; + return parseInt(value / 1000000) + ' MB'; + } + function renderPri(value) { return tvheadend.dvrprio.getById(value).data.name; } @@ -195,11 +208,12 @@ tvheadend.dvrschedule = function() { id : 'pri', header : "Priority", dataIndex : 'pri', - renderer : renderPri + renderer : renderPri, + hidden : iconCls != 'clock', }, { width : 100, id : 'start', - header : "Start", + header : iconCls == 'clock' ? "Start" : "Date/Time", dataIndex : 'start', renderer : renderDate }, { @@ -215,6 +229,13 @@ tvheadend.dvrschedule = function() { header : "Duration", dataIndex : 'duration', renderer : renderDuration + }, { + width : 100, + id : 'filesize', + header : "Filesize", + dataIndex : 'filesize', + renderer : renderSize, + hidden : iconCls != 'television' }, { width : 250, id : 'channel', @@ -238,12 +259,14 @@ tvheadend.dvrschedule = function() { return value; } }, - dataIndex : 'config_name' + dataIndex : 'config_name', + hidden: iconCls != 'clock' }, { width : 200, id : 'status', header : "Status", - dataIndex : 'status' + dataIndex : 'status', + hidden: iconCls != 'exclamation' } ]); function addEntry() { @@ -358,9 +381,9 @@ tvheadend.dvrschedule = function() { loadMask : true, stripeRows : true, disableSelection : true, - title : 'Recorder schedule', - iconCls : 'clock', - store : tvheadend.dvrStore, + title : title, + iconCls : iconCls, + store : dvrStore, cm : dvrCm, plugins : [ actions ], viewConfig : { @@ -378,7 +401,7 @@ tvheadend.dvrschedule = function() { } } ], bbar : new Ext.PagingToolbar({ - store : tvheadend.dvrStore, + store : dvrStore, pageSize : 20, displayInfo : true, displayMsg : 'Programs {0} - {1} of {2}', @@ -570,7 +593,8 @@ tvheadend.autoreceditor = function() { */ tvheadend.dvr = function() { - tvheadend.dvrStore = new Ext.data.JsonStore({ + function datastoreBuilder(url) { + return new Ext.data.JsonStore({ root : 'entries', totalProperty : 'totalCount', fields : [ { @@ -601,6 +625,8 @@ tvheadend.dvr = function() { name : 'status' }, { name : 'schedstate' + }, { + name : 'error' }, { name : 'creator' }, { @@ -610,29 +636,51 @@ tvheadend.dvr = function() { }, { name : 'url' } ], - url : 'dvrlist', + url : url, autoLoad : true, id : 'id', remoteSort : true - }); + }); + } + tvheadend.dvrStoreUpcoming = datastoreBuilder('dvrlist_upcoming'); + tvheadend.dvrStoreFinished = datastoreBuilder('dvrlist_finished'); + tvheadend.dvrStoreFailed = datastoreBuilder('dvrlist_failed'); + tvheadend.dvrStores = [tvheadend.dvrStoreUpcoming, + tvheadend.dvrStoreFinished, + tvheadend.dvrStoreFailed]; + + + function updateDvrStore(store, r, m) { + r.data.status = m.status; + r.data.schedstate = m.schedstate; + + store.afterEdit(r); + store.fireEvent('updated', store, r, + Ext.data.Record.COMMIT); + } + + function reloadStores() { + for (var i = 0; i < tvheadend.dvrStores.length; i++) { + tvheadend.dvrStores[i].reload(); + } + } tvheadend.comet.on('dvrdb', function(m) { - if (m.reload != null) tvheadend.dvrStore.reload(); + if (m.reload != null) { + reloadStores(); + } if (m.updateEntry != null) { - r = tvheadend.dvrStore.getById(m.id) - if (typeof r === 'undefined') { - tvheadend.dvrStore.reload(); - return; + for (var i = 0; i < tvheadend.dvrStores.length; i++) { + var store = tvheadend.dvrStores[i]; + r = tvheadend.dvrStoreUpcoming.getById(m.id); + if (typeof r !== 'undefined') { + updateDvrStore(store, r, m); + return; + } } - - r.data.status = m.status; - r.data.schedstate = m.schedstate; - - tvheadend.dvrStore.afterEdit(r); - tvheadend.dvrStore.fireEvent('updated', tvheadend.dvrStore, r, - Ext.data.Record.COMMIT); + reloadStores(); } }); @@ -661,7 +709,12 @@ tvheadend.dvr = function() { autoScroll : true, title : 'Digital Video Recorder', iconCls : 'drive', - items : [ new tvheadend.dvrschedule, new tvheadend.autoreceditor ] + items : [ + new tvheadend.dvrschedule('Upcoming recordings', 'clock', tvheadend.dvrStoreUpcoming), + new tvheadend.dvrschedule('Finished recordings', 'television', tvheadend.dvrStoreFinished), + new tvheadend.dvrschedule('Failed recordings', 'exclamation', tvheadend.dvrStoreFailed), + new tvheadend.autoreceditor + ] }); return panel; } @@ -676,7 +729,7 @@ tvheadend.dvrsettings = function() { }, [ 'storage', 'postproc', 'retention', 'dayDirs', 'channelDirs', 'channelInTitle', 'container', 'dateInTitle', 'timeInTitle', 'preExtraTime', 'postExtraTime', 'whitespaceInTitle', 'titleDirs', - 'episodeInTitle', 'cleanTitle', 'tagFiles' ]); + 'episodeInTitle', 'cleanTitle', 'tagFiles', 'commSkip' ]); var confcombo = new Ext.form.ComboBox({ store : tvheadend.configNames, @@ -716,11 +769,11 @@ tvheadend.dvrsettings = function() { }, new Ext.form.ComboBox({ store : tvheadend.containers, fieldLabel : 'Media container', - mode : 'local', triggerAction : 'all', - displayField : 'name', - valueField : 'identifier', + displayField : 'description', + valueField : 'name', editable : false, + width : 200, hiddenName : 'container' }), new Ext.form.NumberField({ allowNegative : false, @@ -766,6 +819,9 @@ tvheadend.dvrsettings = function() { }), new Ext.form.Checkbox({ fieldLabel : 'Tag files with metadata', name : 'tagFiles' + }), new Ext.form.Checkbox({ + fieldLabel : 'Skip commercials', + name : 'commSkip' }), { width : 300, fieldLabel : 'Post-processor command', diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index 1b09184b..9fc74aa8 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -20,7 +20,7 @@ tvheadend.contentGroupLookupName = function(code) { ret = ""; tvheadend.ContentGroupStore.each(function(r) { if (r.data.code == code) ret = r.data.name; - else if (ret == "" && r.data.code == code & 0xF0) ret = r.data.name; + else if (ret == "" && r.data.code == (code & 0xF0)) ret = r.data.name; }); return ret; } diff --git a/src/webui/static/app/epggrab.js b/src/webui/static/app/epggrab.js index 8e1a8f6b..e8fc3738 100644 --- a/src/webui/static/app/epggrab.js +++ b/src/webui/static/app/epggrab.js @@ -82,7 +82,7 @@ tvheadend.epggrab = function() { var confreader = new Ext.data.JsonReader({ root : 'epggrabSettings' }, [ 'module', 'interval', 'channel_rename', 'channel_renumber', - 'channel_reicon' ]); + 'channel_reicon', 'epgdb_periodicsave' ]); /* **************************************************************** * Basic Fields @@ -184,6 +184,17 @@ tvheadend.epggrab = function() { fieldLabel : 'Update channel icon' }); + var epgPeriodicSave = new Ext.form.NumberField({ + width : 30, + allowNegative : false, + allowDecimals : false, + minValue : 0, + maxValue : 24, + value : 0, + fieldLabel : 'Periodic save EPG to disk', + name : 'epgdb_periodicsave', + }); + /* * Simple fields */ @@ -192,7 +203,7 @@ tvheadend.epggrab = function() { width : 700, autoHeight : true, collapsible : true, - items : [ channelRename, channelRenumber, channelReicon ] + items : [ channelRename, channelRenumber, channelReicon, epgPeriodicSave ] }); /* diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index a256b6aa..8296e46c 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -224,6 +224,10 @@ background-image: url(../icons/clock.png) !important; } +.exclamation { + background-image: url(../icons/exclamation.png) !important; +} + .wrench { background-image: url(../icons/wrench.png) !important; } diff --git a/src/webui/static/app/iptv.js b/src/webui/static/app/iptv.js index 7914685f..e1c9a41b 100644 --- a/src/webui/static/app/iptv.js +++ b/src/webui/static/app/iptv.js @@ -1,19 +1,19 @@ -tvheadend.servicetypeStore = new Ext.data.JsonStore({ - root : 'entries', - id : 'val', - url : '/iptv/services', - baseParams : { - op : 'servicetypeList' - }, - fields : [ 'val', 'str' ], - autoLoad : true -}); - /** * IPTV service grid */ tvheadend.iptv = function(adapterId) { + var servicetypeStore = new Ext.data.JsonStore({ + root : 'entries', + id : 'val', + url : '/iptv/services', + baseParams : { + op : 'servicetypeList' + }, + fields : [ 'val', 'str' ], + autoLoad : false + }); + var fm = Ext.form; var enabledColumn = new Ext.grid.CheckColumn({ @@ -113,10 +113,10 @@ tvheadend.iptv = function(adapterId) { editable : false, mode : 'local', triggerAction : 'all', - store : tvheadend.servicetypeStore + store : servicetypeStore }), renderer : function(value, metadata, record, row, col, store) { - var val = value ? tvheadend.servicetypeStore.getById(value) : null; + var val = value ? servicetypeStore.getById(value) : null; return val ? val.get('str') : 'Unset'; } diff --git a/src/webui/static/app/status.js b/src/webui/static/app/status.js new file mode 100644 index 00000000..5aacae41 --- /dev/null +++ b/src/webui/static/app/status.js @@ -0,0 +1,222 @@ +/** + * + */ +tvheadend.status_subs = function() { + + tvheadend.subsStore = new Ext.data.JsonStore({ + root : 'entries', + totalProperty : 'totalCount', + fields : [ { + name : 'id' + }, { + name : 'hostname' + }, { + name : 'username' + }, { + name : 'title' + }, { + name : 'channel' + }, { + name : 'service' + }, { + name : 'state' + }, { + name : 'errors' + }, { + name : 'bw' + }, { + name : 'start', + type : 'date', + dateFormat : 'U' /* unix time */ + } ], + url : 'subscriptions', + autoLoad : true, + id : 'id' + }); + + + + tvheadend.comet.on('subscriptions', function(m) { + + if (m.reload != null) tvheadend.subsStore.reload(); + + if (m.updateEntry != null) { + r = tvheadend.subsStore.getById(m.id) + if (typeof r === 'undefined') { + tvheadend.subsStore.reload(); + return; + } + + r.data.channel = m.channel; + r.data.service = m.service; + r.data.state = m.state; + r.data.errors = m.errors; + r.data.bw = m.bw + + tvheadend.subsStore.afterEdit(r); + tvheadend.subsStore.fireEvent('updated', tvheadend.subsStore, r, + Ext.data.Record.COMMIT); + } + }); + + function renderDate(value) { + var dt = new Date(value); + return dt.format('D j M H:i'); + } + + function renderBw(value) { + return parseInt(value / 125); + } + + var subsCm = new Ext.grid.ColumnModel([{ + width : 50, + id : 'hostname', + header : "Hostname", + dataIndex : 'hostname' + }, { + width : 50, + id : 'username', + header : "Username", + dataIndex : 'username' + }, { + width : 80, + id : 'title', + header : "Title", + dataIndex : 'title' + }, { + width : 50, + id : 'channel', + header : "Channel", + dataIndex : 'channel' + }, { + width : 200, + id : 'service', + header : "Service", + dataIndex : 'service', + }, { + width : 50, + id : 'start', + header : "Start", + dataIndex : 'start', + renderer : renderDate + }, { + width : 50, + id : 'state', + header : "State", + dataIndex : 'state' + }, { + width : 50, + id : 'errors', + header : "Errors", + dataIndex : 'errors' + }, { + width : 50, + id : 'bw', + header : "Bandwidth (kb/s)", + dataIndex : 'bw', + renderer: renderBw + } ]); + + var subs = new Ext.grid.GridPanel({ + border: false, + loadMask : true, + stripeRows : true, + disableSelection : true, + title : 'Active subscriptions', + iconCls : 'eye', + store : tvheadend.subsStore, + cm : subsCm, + flex: 1, + viewConfig : { + forceFit : true + } + }); + return subs; +} + + +/** + * + */ +tvheadend.status_adapters = function() { + + var signal = new Ext.ux.grid.ProgressColumn({ + header : "Signal Strength", + dataIndex : 'signal', + width : 85, + textPst : '%', + colored : true + }); + + function renderBw(value) { + return parseInt(value / 125); + } + + var cm = new Ext.grid.ColumnModel([{ + width : 50, + header : "Name", + dataIndex : 'name' + },{ + width : 50, + header : "Hardware device", + dataIndex : 'path' + },{ + width : 100, + header : "Currently tuned to", + dataIndex : 'currentMux' + },{ + width : 100, + header : "Bandwidth (kb/s)", + dataIndex : 'bw', + renderer: renderBw + },{ + width : 50, + header : "Bit error rate", + dataIndex : 'ber' + },{ + width : 50, + header : "Uncorrected bit error rate", + dataIndex : 'uncavg' + },{ + width : 50, + header : "SNR", + dataIndex : 'snr', + renderer: function(value) { + if(value > 0) { + return value.toFixed(1) + " dB"; + } else { + return 'Unknown'; + } + } + }, signal]); + + var panel = new Ext.grid.GridPanel({ + border: false, + loadMask : true, + stripeRows : true, + disableSelection : true, + title : 'Adapters', + iconCls : 'hardware', + store : tvheadend.tvAdapterStore, + cm : cm, + flex: 1, + viewConfig : { + forceFit : true + } + }); + return panel; +} + +tvheadend.status = function() { + + var panel = new Ext.Panel({ + border: false, + layout : 'vbox', + title : 'Status', + iconCls : 'eye', + items : [ new tvheadend.status_subs, new tvheadend.status_adapters ] + }); + + return panel; +} + diff --git a/src/webui/static/app/tableeditor.js b/src/webui/static/app/tableeditor.js index c3a1972d..c36498d8 100644 --- a/src/webui/static/app/tableeditor.js +++ b/src/webui/static/app/tableeditor.js @@ -15,6 +15,11 @@ tvheadend.tableEditor = function(title, dtable, cm, rec, plugins, store, }); } + tvheadend.comet.on(dtable, function(m){ + if (m.reload) + store.reload(); + }); + function addRecord() { Ext.Ajax.request({ url : "tablemgr", @@ -65,7 +70,6 @@ tvheadend.tableEditor = function(title, dtable, cm, rec, plugins, store, Ext.MessageBox.alert('Server Error', 'Unable to delete'); }, success : function(response, options) { - store.reload(); } }) } @@ -88,6 +92,7 @@ tvheadend.tableEditor = function(title, dtable, cm, rec, plugins, store, entries : Ext.encode(out) }, success : function(response, options) { + // Note: this call is mostly redundant (comet update will pick it up anyway) store.commitChanges(); }, failure : function(response, options) { diff --git a/src/webui/static/app/timeshift.js b/src/webui/static/app/timeshift.js new file mode 100644 index 00000000..02335006 --- /dev/null +++ b/src/webui/static/app/timeshift.js @@ -0,0 +1,152 @@ +tvheadend.timeshift = function() { + + /* **************************************************************** + * Data + * ***************************************************************/ + + var confreader = new Ext.data.JsonReader( + { + root: 'config' + }, + [ + 'timeshift_enabled', 'timeshift_ondemand', + 'timeshift_path', + 'timeshift_unlimited_period', 'timeshift_max_period', + 'timeshift_unlimited_size', 'timeshift_max_size' + ] + ); + + /* **************************************************************** + * Fields + * ***************************************************************/ + + var timeshiftEnabled = new Ext.form.Checkbox({ + fieldLabel: 'Enabled', + name: 'timeshift_enabled', + width: 300 + }); + + var timeshiftOndemand = new Ext.form.Checkbox({ + fieldLabel: 'On-Demand', + name: 'timeshift_ondemand', + width: 300 + }); + + var timeshiftPath = new Ext.form.TextField({ + fieldLabel: 'Storage Path', + name: 'timeshift_path', + allowBlank: true, + width: 300 + }); + + var timeshiftMaxPeriod = new Ext.form.NumberField({ + fieldLabel: 'Max. Period (mins)', + name: 'timeshift_max_period', + allowBlank: false, + width: 300 + }); + + var timeshiftUnlPeriod = new Ext.form.Checkbox({ + fieldLabel: '   (unlimited)', + name: 'timeshift_unlimited_period', + Width: 300 + }); + + var timeshiftMaxSize = new Ext.form.NumberField({ + fieldLabel: 'Max. Size (MB)', + name: 'timeshift_max_size', + allowBlank: false, + width: 300 + }); + + var timeshiftUnlSize = new Ext.form.Checkbox({ + fieldLabel: '   (unlimited)', + name: 'timeshift_unlimited_size', + Width: 300 + }); + + /* **************************************************************** + * Events + * ***************************************************************/ + + timeshiftUnlPeriod.on('check', function(e, c){ + timeshiftMaxPeriod.setDisabled(c); + }); + timeshiftUnlSize.on('check', function(e, c){ + timeshiftMaxSize.setDisabled(c); + }); + + /* **************************************************************** + * Form + * ***************************************************************/ + + var saveButton = new Ext.Button({ + text : "Save configuration", + tooltip : 'Save changes made to configuration below', + iconCls : 'save', + handler : saveChanges + }); + + var helpButton = new Ext.Button({ + text : 'Help', + handler : function() { + new tvheadend.help('Timeshift Configuration', 'config_timeshift.html'); + } + }); + + var confpanel = new Ext.FormPanel({ + title : 'Timeshift', + iconCls : 'clock', + border : false, + bodyStyle : 'padding:15px', + labelAlign : 'left', + labelWidth : 150, + waitMsgTarget : true, + reader : confreader, + layout : 'form', + defaultType : 'textfield', + autoHeight : true, + items : [ + timeshiftEnabled, timeshiftOndemand, + timeshiftPath, + timeshiftMaxPeriod, timeshiftUnlPeriod, + timeshiftMaxSize, timeshiftUnlSize + ], + tbar : [ saveButton, '->', helpButton ] + }); + + /* **************************************************************** + * Load/Save + * ***************************************************************/ + + confpanel.on('render', function() { + confpanel.getForm().load({ + url: 'timeshift', + params: { + 'op': 'loadSettings' + }, + success: function() { + confpanel.enable(); + timeshiftMaxPeriod.setDisabled(timeshiftUnlPeriod.getValue()); + timeshiftMaxSize.setDisabled(timeshiftUnlSize.getValue()); + } + }); + }); + + function saveChanges() { + confpanel.getForm().submit({ + url : 'timeshift', + params : { + op : 'saveSettings', + }, + waitMsg : 'Saving Data...', + success : function(form, action) { + }, + failure : function(form, action) { + Ext.Msg.alert('Save failed', action.result.errormsg); + } + }); + } + + return confpanel; +} diff --git a/src/webui/static/app/tvadapters.js b/src/webui/static/app/tvadapters.js index 0d3fcc56..8da843f1 100644 --- a/src/webui/static/app/tvadapters.js +++ b/src/webui/static/app/tvadapters.js @@ -5,9 +5,9 @@ tvheadend.tvAdapterStore = new Ext.data.JsonStore({ root : 'entries', id : 'identifier', fields : [ 'identifier', 'type', 'name', 'path', 'devicename', - 'hostconnection', 'currentMux', 'services', 'muxes', 'initialMuxes', - 'satConf', 'deliverySystem', 'freqMin', 'freqMax', 'freqStep', - 'symrateMin', 'symrateMax' ], + 'hostconnection', 'currentMux', 'services', 'muxes', 'initialMuxes', + 'satConf', 'deliverySystem', 'freqMin', 'freqMax', 'freqStep', + 'symrateMin', 'symrateMax', 'signal', 'snr', 'ber', 'unc', 'uncavg', 'bw'], url : 'tv/adapter' }); @@ -24,6 +24,8 @@ tvheadend.comet.on('tvAdapter', function(m) { }); tvheadend.tvadapters = function() { + tvheadend.tvAdapterStore.load(); + var adapterSelection = new Ext.form.ComboBox({ loadingText : 'Loading...', width : 300, @@ -97,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', diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index ab942852..761be214 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -1,3 +1,10 @@ +tvheadend.accessupdate = null; +tvheadend.capabilties = null; +tvheadend.conf_chepg = null; +tvheadend.conf_dvbin = null; +tvheadend.conf_tsdvr = null; +tvheadend.conf_csa = null; + /** * Displays a help popup window */ @@ -27,6 +34,21 @@ tvheadend.help = function(title, pagename) { }); } +/* + * General capabilities + */ +Ext.Ajax.request({ + url: 'capabilities', + success: function(d) + { + if (d && d.responseText) + tvheadend.capabilities = Ext.util.JSON.decode(d.responseText); + if (tvheadend.capabilities && tvheadend.accessupdate) + accessUpdate(tvheadend.accessUpdate); + + } +}); + /** * Displays a mediaplayer using VLC plugin */ @@ -223,39 +245,113 @@ tvheadend.VLC = function(url) { * Obviosuly, access is verified in the server too. */ function accessUpdate(o) { + tvheadend.accessUpdate = o; + if (!tvheadend.capabilities) + return; - if (o.dvr == true && tvheadend.dvrpanel == null) { - tvheadend.dvrpanel = new tvheadend.dvr; - tvheadend.rootTabPanel.add(tvheadend.dvrpanel); - } + if (o.dvr == true && tvheadend.dvrpanel == null) { + tvheadend.dvrpanel = new tvheadend.dvr; + tvheadend.rootTabPanel.add(tvheadend.dvrpanel); + } - if (o.admin == true && tvheadend.confpanel == null) { - tvheadend.confpanel = new Ext.TabPanel({ - activeTab : 0, - autoScroll : true, - title : 'Configuration', - iconCls : 'wrench', - items : [ new tvheadend.miscconf, new tvheadend.chconf, - new tvheadend.epggrab, new tvheadend.cteditor, - new tvheadend.dvrsettings, new tvheadend.tvadapters, - new tvheadend.iptv, new tvheadend.acleditor, - new tvheadend.cwceditor, new tvheadend.capmteditor ] - }); - tvheadend.rootTabPanel.add(tvheadend.confpanel); - } + if (o.admin == true && tvheadend.confpanel == null) { + var tabs1 = [ + new tvheadend.miscconf, + new tvheadend.acleditor + ] + var tabs2; - if (tvheadend.aboutPanel == null) { - tvheadend.aboutPanel = new Ext.Panel({ - border : false, - layout : 'fit', - title : 'About', - iconCls : 'info', - autoLoad : 'about.html' - }); - tvheadend.rootTabPanel.add(tvheadend.aboutPanel); - } + /* DVB inputs */ + tabs2 = []; + if (tvheadend.capabilities.indexOf('linuxdvb') != -1 || + tvheadend.capabilities.indexOf('v4l') != -1) { + tabs2.push(new tvheadend.tvadapters); + } + tabs2.push(new tvheadend.iptv); + tvheadend.conf_dvbin = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title: 'DVB Inputs', + iconCls: 'hardware', + items : tabs2 + }); + tabs1.push(tvheadend.conf_dvbin); - tvheadend.rootTabPanel.doLayout(); + /* Channel / EPG */ + tvheadend.conf_chepg = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title : 'Channel / EPG', + iconCls : 'television', + items : [ + new tvheadend.chconf, + new tvheadend.cteditor, + new tvheadend.epggrab + ] + }); + tabs1.push(tvheadend.conf_chepg); + + /* DVR / Timeshift */ + tabs2 = [ new tvheadend.dvrsettings ]; + if (tvheadend.capabilities.indexOf('timeshift') != -1) { + tabs2.push(new tvheadend.timeshift) + } + tvheadend.conf_tsdvr = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title: 'Recording', + iconCls: 'drive', + items : tabs2 + }); + tabs1.push(tvheadend.conf_tsdvr); + + /* CSA */ + if (tvheadend.capabilities.indexOf('cwc') != -1) { + tvheadend.conf_csa = new Ext.TabPanel({ + activeTab: 0, + autoScroll: true, + title: 'CSA', + iconCls: 'key', + items: [ + new tvheadend.cwceditor, + new tvheadend.capmteditor + ] + }); + tabs1.push(tvheadend.conf_csa); + } + + /* Debug */ + tabs1.push(new tvheadend.tvhlog); + + tvheadend.confpanel = new Ext.TabPanel({ + activeTab : 0, + autoScroll : true, + title : 'Configuration', + iconCls : 'wrench', + items : tabs1 + }); + + tvheadend.rootTabPanel.add(tvheadend.confpanel); + tvheadend.confpanel.doLayout(); + } + + if (o.admin == true && tvheadend.statuspanel == null) { + tvheadend.statuspanel = new tvheadend.status; + tvheadend.rootTabPanel.add(tvheadend.statuspanel); + } + + if (tvheadend.aboutPanel == null) { + tvheadend.aboutPanel = new Ext.Panel({ + border : false, + layout : 'fit', + title : 'About', + iconCls : 'info', + autoLoad : 'about.html' + }); + tvheadend.rootTabPanel.add(tvheadend.aboutPanel); + } + + tvheadend.rootTabPanel.doLayout(); } /** diff --git a/src/webui/static/app/tvhlog.js b/src/webui/static/app/tvhlog.js new file mode 100644 index 00000000..5737405f --- /dev/null +++ b/src/webui/static/app/tvhlog.js @@ -0,0 +1,102 @@ +tvheadend.tvhlog = function() { + /* + * Basic Config + */ + var confreader = new Ext.data.JsonReader({ + root : 'config' + }, [ 'tvhlog_path', 'tvhlog_dbg_syslog', 'tvhlog_subsys', 'tvhlog_trace' ]); + + /* **************************************************************** + * Form Fields + * ***************************************************************/ + + var tvhlogLogPath = new Ext.form.TextField({ + fieldLabel : 'Debug Log Path', + name : 'tvhlog_path', + allowBlank : true, + width: 400 + }); + + var tvhlogToSyslog = new Ext.form.Checkbox({ + name: 'tvhlog_dbg_syslog', + fieldLabel: 'Debug to syslog' + }); + + var tvhlogTrace = new Ext.form.Checkbox({ + name: 'tvhlog_trace', + fieldLabel: 'Debug trace (low-level stuff)' + }); + + var tvhlogSubsys = new Ext.form.TextField({ + fieldLabel : 'Debug Subsystems', + name : 'tvhlog_subsys', + allowBlank : true, + width: 400 + }); + + /* **************************************************************** + * Form + * ***************************************************************/ + + var saveButton = new Ext.Button({ + text : "Save configuration", + tooltip : 'Save changes made to configuration below', + iconCls : 'save', + handler : saveChanges + }); + + var helpButton = new Ext.Button({ + text : 'Help', + handler : function() { + new tvheadend.help('Debug Configuration', 'config_tvhlog.html'); + } + }); + + var confpanel = new Ext.form.FormPanel({ + title : 'Debugging', + iconCls : 'wrench', + border : false, + bodyStyle : 'padding:15px', + labelAlign : 'left', + labelWidth : 200, + waitMsgTarget : true, + reader : confreader, + layout : 'form', + defaultType : 'textfield', + autoHeight : true, + items : [ tvhlogLogPath, tvhlogToSyslog, + tvhlogTrace, tvhlogSubsys ], + tbar : [ saveButton, '->', helpButton ] + }); + + /* **************************************************************** + * Load/Save + * ***************************************************************/ + + confpanel.on('render', function() { + confpanel.getForm().load({ + url : 'tvhlog', + params : { + op : 'loadSettings' + }, + success : function(form, action) { + confpanel.enable(); + } + }); + }); + + function saveChanges() { + confpanel.getForm().submit({ + url : 'tvhlog', + params : { + op : 'saveSettings' + }, + waitMsg : 'Saving Data...', + failure : function(form, action) { + Ext.Msg.alert('Save failed', action.result.errormsg); + } + }); + } + + return confpanel; +} diff --git a/src/webui/static/extjs/resources/css/ext-all-notheme-min.css b/src/webui/static/extjs/resources/css/ext-all-notheme-min.css new file mode 100644 index 00000000..517c95a4 --- /dev/null +++ b/src/webui/static/extjs/resources/css/ext-all-notheme-min.css @@ -0,0 +1,8 @@ +/*! + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,p,blockquote,th,td{margin:0;padding:0}img,body,html{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%}q:before,q:after{content:''}.ext-forced-border-box,.ext-forced-border-box *{-moz-box-sizing:border-box;-ms-box-sizing:border-box;-webkit-box-sizing:border-box}.ext-el-mask{z-index:100;position:absolute;top:0;left:0;opacity:.50;filter:alpha(opacity=50);width:100%;height:100%}.ext-el-mask-msg{z-index:20001;position:absolute;top:0;left:0;border:1px solid;background:repeat-x 0 -16px;padding:2px}.ext-el-mask-msg div{padding:5px 10px 5px 10px;border:1px solid;cursor:wait}.ext-shim{position:absolute;visibility:hidden;left:0;top:0;overflow:hidden}.ext-ie .ext-shim{filter:alpha(opacity=0)}.ext-ie6 .ext-shim{margin-left:5px;margin-top:3px}.x-mask-loading div{padding:5px 10px 5px 25px;background:no-repeat 5px 5px;line-height:16px}.x-hidden,.x-hide-offsets{position:absolute!important;left:-10000px;top:-10000px;visibility:hidden}.x-hide-display{display:none!important}.x-hide-nosize,.x-hide-nosize *{height:0!important;width:0!important;visibility:hidden!important;border:none!important}.x-hide-visibility{visibility:hidden!important}.x-masked{overflow:hidden!important}.x-masked-relative{position:relative!important}.x-masked select,.x-masked object,.x-masked embed{visibility:hidden}.x-layer{visibility:hidden}.x-unselectable,.x-unselectable *{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-repaint{background-color:transparent;outline:0}.x-item-disabled{cursor:default;opacity:.6;filter:alpha(opacity=60)}.x-item-disabled *{cursor:default!important}.x-form-radio-group .x-item-disabled{filter:none}.x-splitbar-proxy{position:absolute;visibility:hidden;z-index:20001;line-height:1px;font-size:1px;overflow:hidden}.x-splitbar-h,.x-splitbar-proxy-h{cursor:e-resize;cursor:col-resize}.x-splitbar-v,.x-splitbar-proxy-v{cursor:s-resize;cursor:row-resize}.x-color-palette{width:150px;height:92px;cursor:pointer}.x-color-palette a{border:1px solid;float:left;padding:2px;text-decoration:none;outline:0 none;cursor:pointer}.x-color-palette a:hover,.x-color-palette a.x-color-palette-sel{border:1px solid}.x-color-palette em{display:block;border:1px solid}.x-color-palette em span{cursor:pointer;display:block;height:10px;line-height:10px;width:10px}.x-ie-shadow{display:none;position:absolute;overflow:hidden;left:0;top:0}.x-shadow{display:none;position:absolute;overflow:hidden;left:0;top:0}.x-shadow *{overflow:hidden}.x-shadow *{padding:0;border:0;margin:0;clear:none}.x-shadow .xstc,.x-shadow .xsbc{height:6px;float:left}.x-shadow .xstl,.x-shadow .xstr,.x-shadow .xsbl,.x-shadow .xsbr{width:6px;height:6px;float:left}.x-shadow .xsc{width:100%}.x-shadow .xsml,.x-shadow .xsmr{width:6px;float:left;height:100%}.x-shadow .xsmc{float:left;height:100%;background-color:transparent}.x-shadow .xst,.x-shadow .xsb{height:6px;overflow:hidden;width:100%}.x-shadow .xsml{background:transparent repeat-y 0 0}.x-shadow .xsmr{background:transparent repeat-y -6px 0}.x-shadow .xstl{background:transparent no-repeat 0 0}.x-shadow .xstc{background:transparent repeat-x 0 -30px}.x-shadow .xstr{background:transparent repeat-x 0 -18px}.x-shadow .xsbl{background:transparent no-repeat 0 -12px}.x-shadow .xsbc{background:transparent repeat-x 0 -36px}.x-shadow .xsbr{background:transparent repeat-x 0 -6px}.loading-indicator{background:no-repeat left;padding-left:20px;line-height:16px;margin:3px}.x-text-resize{position:absolute;left:-1000px;top:-1000px;visibility:hidden}.x-drag-overlay{width:100%;height:100%;display:none;position:absolute;left:0;top:0;background-image:url(../images/default/s.gif);z-index:20000}.x-clear{clear:both;height:0;overflow:hidden;line-height:0;font-size:0}.x-spotlight{z-index:8999;position:absolute;top:0;left:0;opacity:.50;filter:alpha(opacity=50);width:0;height:0}#x-history-frame{position:absolute;top:-1px;left:0;width:1px;height:1px;visibility:hidden}#x-history-field{position:absolute;top:0;left:-1px;width:1px;height:1px;visibility:hidden}.x-resizable-handle{position:absolute;z-index:100;font-size:1px;line-height:6px;overflow:hidden;filter:alpha(opacity=0);opacity:0}.x-resizable-handle-east{width:6px;cursor:e-resize;right:0;top:0;height:100%}.ext-ie .x-resizable-handle-east{margin-right:-1px}.x-resizable-handle-south{width:100%;cursor:s-resize;left:0;bottom:0;height:6px}.ext-ie .x-resizable-handle-south{margin-bottom:-1px}.x-resizable-handle-west{width:6px;cursor:w-resize;left:0;top:0;height:100%}.x-resizable-handle-north{width:100%;cursor:n-resize;left:0;top:0;height:6px}.x-resizable-handle-southeast{width:6px;cursor:se-resize;right:0;bottom:0;height:6px;z-index:101}.x-resizable-handle-northwest{width:6px;cursor:nw-resize;left:0;top:0;height:6px;z-index:101}.x-resizable-handle-northeast{width:6px;cursor:ne-resize;right:0;top:0;height:6px;z-index:101}.x-resizable-handle-southwest{width:6px;cursor:sw-resize;left:0;bottom:0;height:6px;z-index:101}.x-resizable-over .x-resizable-handle,.x-resizable-pinned .x-resizable-handle{filter:alpha(opacity=100);opacity:1}.x-resizable-over .x-resizable-handle-east,.x-resizable-pinned .x-resizable-handle-east,.x-resizable-over .x-resizable-handle-west,.x-resizable-pinned .x-resizable-handle-west{background-position:left}.x-resizable-over .x-resizable-handle-south,.x-resizable-pinned .x-resizable-handle-south,.x-resizable-over .x-resizable-handle-north,.x-resizable-pinned .x-resizable-handle-north{background-position:top}.x-resizable-over .x-resizable-handle-southeast,.x-resizable-pinned .x-resizable-handle-southeast{background-position:top left}.x-resizable-over .x-resizable-handle-northwest,.x-resizable-pinned .x-resizable-handle-northwest{background-position:bottom right}.x-resizable-over .x-resizable-handle-northeast,.x-resizable-pinned .x-resizable-handle-northeast{background-position:bottom left}.x-resizable-over .x-resizable-handle-southwest,.x-resizable-pinned .x-resizable-handle-southwest{background-position:top right}.x-resizable-proxy{border:1px dashed;position:absolute;overflow:hidden;display:none;left:0;top:0;z-index:50000}.x-resizable-overlay{width:100%;height:100%;display:none;position:absolute;left:0;top:0;z-index:200000;opacity:0;filter:alpha(opacity=0)}.x-tab-panel{overflow:hidden}.x-tab-panel-header,.x-tab-panel-footer{border:1px solid;overflow:hidden}.x-tab-panel-header{border:1px solid;padding-bottom:2px}.x-tab-panel-footer{border:1px solid;padding-top:2px}.x-tab-strip-wrap{width:100%;overflow:hidden;position:relative}ul.x-tab-strip{display:block;width:5000px}ul.x-tab-strip-top{padding-top:1px;background:repeat-x bottom;border-bottom:1px solid}ul.x-tab-strip-bottom{padding-bottom:1px;background:repeat-x top;border-top:1px solid;border-bottom:0 none}.x-tab-panel-header-plain .x-tab-strip-top{background:transparent!important;padding-top:0!important}.x-tab-panel-header-plain{background:transparent!important;border-width:0!important;padding-bottom:0!important}.x-tab-panel-header-plain .x-tab-strip-spacer,.x-tab-panel-footer-plain .x-tab-strip-spacer{border:1px solid;height:2px;font-size:1px;line-height:1px}.x-tab-panel-header-plain .x-tab-strip-spacer{border-top:0 none}.x-tab-panel-footer-plain .x-tab-strip-spacer{border-bottom:0 none}.x-tab-panel-footer-plain .x-tab-strip-bottom{background:transparent!important;padding-bottom:0!important}.x-tab-panel-footer-plain{background:transparent!important;border-width:0!important;padding-top:0!important}.ext-border-box .x-tab-panel-header-plain .x-tab-strip-spacer,.ext-border-box .x-tab-panel-footer-plain .x-tab-strip-spacer{height:3px}ul.x-tab-strip li{float:left;margin-left:2px}ul.x-tab-strip li.x-tab-edge{float:left;margin:0!important;padding:0!important;border:0 none!important;font-size:1px!important;line-height:1px!important;overflow:hidden;background:transparent!important;width:1px}.x-tab-strip a,.x-tab-strip span,.x-tab-strip em{display:block}.x-tab-strip a{text-decoration:none!important;outline:0;cursor:pointer}.x-tab-strip-inner{overflow:hidden;text-overflow:ellipsis}.x-tab-strip span.x-tab-strip-text{white-space:nowrap;cursor:pointer;padding:4px 0}.x-tab-strip-top .x-tab-with-icon .x-tab-right{padding-left:6px}.x-tab-strip .x-tab-with-icon span.x-tab-strip-text{padding-left:20px;background-position:0 3px;background-repeat:no-repeat}.x-tab-strip-active,.x-tab-strip-active a.x-tab-right{cursor:default}.x-tab-strip-active span.x-tab-strip-text{cursor:default}.x-tab-strip-disabled .x-tabs-text{cursor:default}.x-tab-panel-body{overflow:hidden}.x-tab-panel-bwrap{overflow:hidden}.ext-ie .x-tab-strip .x-tab-right{position:relative}.x-tab-strip-top .x-tab-strip-active .x-tab-right{margin-bottom:-1px}.ext-ie8 .x-tab-strip li{position:relative}.ext-border-box .ext-ie8 .x-tab-strip-top .x-tab-right{top:1px}.ext-ie8 .x-tab-strip-top{padding-top:1}.ext-border-box .ext-ie8 .x-tab-strip-top{padding-top:0}.ext-ie8 .x-tab-strip .x-tab-strip-closable a.x-tab-strip-close{top:3px}.ext-border-box .ext-ie8 .x-tab-strip .x-tab-strip-closable a.x-tab-strip-close{top:4px}.ext-ie8 .x-tab-strip-bottom .x-tab-right{top:0}.x-tab-strip-top .x-tab-strip-active .x-tab-right span.x-tab-strip-text{padding-bottom:5px}.x-tab-strip-bottom .x-tab-strip-active .x-tab-right{margin-top:-1px}.x-tab-strip-bottom .x-tab-strip-active .x-tab-right span.x-tab-strip-text{padding-top:5px}.x-tab-strip-top .x-tab-right{background:transparent no-repeat 0 -51px;padding-left:10px}.x-tab-strip-top .x-tab-left{background:transparent no-repeat right -351px;padding-right:10px}.x-tab-strip-top .x-tab-strip-inner{background:transparent repeat-x 0 -201px}.x-tab-strip-top .x-tab-strip-over .x-tab-right{background-position:0 -101px}.x-tab-strip-top .x-tab-strip-over .x-tab-left{background-position:right -401px}.x-tab-strip-top .x-tab-strip-over .x-tab-strip-inner{background-position:0 -251px}.x-tab-strip-top .x-tab-strip-active .x-tab-right{background-position:0 0}.x-tab-strip-top .x-tab-strip-active .x-tab-left{background-position:right -301px}.x-tab-strip-top .x-tab-strip-active .x-tab-strip-inner{background-position:0 -151px}.x-tab-strip-bottom .x-tab-right{background:no-repeat bottom right}.x-tab-strip-bottom .x-tab-left{background:no-repeat bottom left}.x-tab-strip-bottom .x-tab-strip-active .x-tab-right{background:no-repeat bottom right}.x-tab-strip-bottom .x-tab-strip-active .x-tab-left{background:no-repeat bottom left}.x-tab-strip-bottom .x-tab-left{margin-right:3px;padding:0 10px}.x-tab-strip-bottom .x-tab-right{padding:0}.x-tab-strip .x-tab-strip-close{display:none}.x-tab-strip-closable{position:relative}.x-tab-strip-closable .x-tab-left{padding-right:19px}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close{opacity:.6;background-repeat:no-repeat;display:block;width:11px;height:11px;position:absolute;top:3px;right:3px;cursor:pointer;z-index:2}.x-tab-strip .x-tab-strip-active a.x-tab-strip-close{opacity:.8}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close:hover{opacity:1}.x-tab-panel-body{border:1px solid}.x-tab-panel-body-top{border-top:0 none}.x-tab-panel-body-bottom{border-bottom:0 none}.x-tab-scroller-left{background:transparent no-repeat -18px 0;border-bottom:1px solid;width:18px;position:absolute;left:0;top:0;z-index:10;cursor:pointer}.x-tab-scroller-left-over{background-position:0 0}.x-tab-scroller-left-disabled{background-position:-18px 0;opacity:.5;filter:alpha(opacity=50);cursor:default}.x-tab-scroller-right{background:transparent no-repeat 0 0;border-bottom:1px solid;width:18px;position:absolute;right:0;top:0;z-index:10;cursor:pointer}.x-tab-scroller-right-over{background-position:-18px 0}.x-tab-scroller-right-disabled{background-position:0 0;opacity:.5;filter:alpha(opacity=50);cursor:default}.x-tab-scrolling-bottom .x-tab-scroller-left,.x-tab-scrolling-bottom .x-tab-scroller-right{margin-top:1px}.x-tab-scrolling .x-tab-strip-wrap{margin-left:18px;margin-right:18px}.x-tab-scrolling{position:relative}.x-tab-panel-bbar .x-toolbar{border:1px solid;border-top:0 none;overflow:hidden;padding:2px}.x-tab-panel-tbar .x-toolbar{border:1px solid;border-top:0 none;overflow:hidden;padding:2px}.x-form-field{margin:0}.ext-webkit *:focus{outline:none!important}.x-form-text,textarea.x-form-field{padding:1px 3px;background:repeat-x 0 0;border:1px solid}textarea.x-form-field{padding:2px 3px}.x-form-text,.ext-ie .x-form-file{height:22px;line-height:18px;vertical-align:middle}.ext-ie6 .x-form-text,.ext-ie7 .x-form-text{margin:-1px 0;height:22px;line-height:18px}.x-quirks .ext-ie9 .x-form-text{height:22px;padding-top:3px;padding-bottom:0}.x-quirks .ext-ie9 .x-input-wrapper .x-form-text,.x-quirks .ext-ie9 .x-form-field-trigger-wrap .x-form-text{margin-top:-1px;margin-bottom:-1px}.x-quirks .ext-ie9 .x-input-wrapper .x-form-element{margin-bottom:-1px}.ext-ie6 .x-form-field-wrap .x-form-file-btn,.ext-ie7 .x-form-field-wrap .x-form-file-btn{top:-1px}.ext-ie6 textarea.x-form-field,.ext-ie7 textarea.x-form-field{margin:-1px 0}.ext-strict .x-form-text{height:18px}.ext-safari.ext-mac textarea.x-form-field{margin-bottom:-2px}.ext-gecko .x-form-text,.ext-ie8 .x-form-text{padding-top:2px;padding-bottom:0}.ext-ie6 .x-form-composite .x-form-text.x-box-item,.ext-ie7 .x-form-composite .x-form-text.x-box-item{margin:0!important}textarea{resize:none}.x-form-select-one{height:20px;line-height:18px;vertical-align:middle;border:1px solid}.x-form-check-wrap{line-height:18px;height:auto}.ext-ie .x-form-check-wrap input{width:15px;height:15px}.x-form-check-wrap input{vertical-align:bottom}.x-editor .x-form-check-wrap{padding:3px}.x-editor .x-form-checkbox{height:13px}.x-form-check-group-label{border-bottom:1px solid;margin-bottom:5px;padding-left:3px!important;float:none!important}.x-form-field-wrap .x-form-trigger{width:17px;height:21px;border:0;background:transparent no-repeat 0 0;cursor:pointer;border-bottom:1px solid;position:absolute;top:0}.x-form-field-wrap .x-form-date-trigger,.x-form-field-wrap .x-form-clear-trigger,.x-form-field-wrap .x-form-search-trigger{cursor:pointer}.x-form-field-wrap .x-form-twin-triggers .x-form-trigger{position:static;top:auto;vertical-align:top}.x-form-field-wrap{position:relative;left:0;top:0;text-align:left;white-space:nowrap}.ext-strict .ext-ie8 .x-toolbar-cell .x-form-field-trigger-wrap .x-form-trigger{right:0}.x-form-field-wrap .x-form-trigger-over{background-position:-17px 0}.x-form-field-wrap .x-form-trigger-click{background-position:-34px 0}.x-trigger-wrap-focus .x-form-trigger{background-position:-51px 0}.x-trigger-wrap-focus .x-form-trigger-over{background-position:-68px 0}.x-trigger-wrap-focus .x-form-trigger-click{background-position:-85px 0}.x-trigger-wrap-focus .x-form-trigger{border-bottom:1px solid}.x-item-disabled .x-form-trigger-over{background-position:0 0!important;border-bottom:1px solid}.x-item-disabled .x-form-trigger-click{background-position:0 0!important;border-bottom:1px solid}.x-trigger-noedit{cursor:pointer}.x-form-focus,textarea.x-form-focus{border:1px solid}.x-form-invalid,textarea.x-form-invalid{background:repeat-x bottom;border:1px solid}.x-form-inner-invalid,textarea.x-form-inner-invalid{background:repeat-x bottom}.x-editor{visibility:hidden;padding:0;margin:0}.x-form-grow-sizer{left:-10000px;padding:8px 3px;position:absolute;visibility:hidden;top:-10000px;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.x-form-grow-sizer p{margin:0!important;border:0 none!important;padding:0!important}.x-form-item{display:block;margin-bottom:4px}.x-form-item label.x-form-item-label{display:block;float:left;width:100px;padding:3px;padding-left:0;clear:left;z-index:2;position:relative}.x-form-element{padding-left:105px;position:relative}.x-form-invalid-msg{padding:2px;padding-left:18px;background:transparent no-repeat 0 2px;line-height:16px;width:200px}.x-form-label-left label.x-form-item-label{text-align:left}.x-form-label-right label.x-form-item-label{text-align:right}.x-form-label-top .x-form-item label.x-form-item-label{width:auto;float:none;clear:none;display:inline;margin-bottom:4px;position:static}.x-form-label-top .x-form-element{padding-left:0;padding-top:4px}.x-form-label-top .x-form-item{padding-bottom:4px}.x-small-editor .x-form-text{height:20px;line-height:16px;vertical-align:middle}.ext-ie6 .x-small-editor .x-form-text,.ext-ie7 .x-small-editor .x-form-text{margin-top:-1px!important;margin-bottom:-1px!important;height:20px!important;line-height:16px!important}.ext-strict .x-small-editor .x-form-text{height:16px!important}.ext-ie6 .x-small-editor .x-form-text,.ext-ie7 .x-small-editor .x-form-text{height:20px;line-height:16px}.ext-border-box .x-small-editor .x-form-text{height:20px}.x-small-editor .x-form-select-one{height:20px;line-height:16px;vertical-align:middle}.x-small-editor .x-form-num-field{text-align:right}.x-small-editor .x-form-field-wrap .x-form-trigger{height:19px}.ext-webkit .x-small-editor .x-form-text{padding-top:3px;font-size:100%}.ext-strict .ext-webkit .x-small-editor .x-form-text{height:14px!important}.x-form-clear{clear:both;height:0;overflow:hidden;line-height:0;font-size:0}.x-form-clear-left{clear:left;height:0;overflow:hidden;line-height:0;font-size:0}.ext-ie6 .x-form-check-wrap input,.ext-border-box .x-form-check-wrap input{margin-top:3px}.x-form-cb-label{position:relative;margin-left:4px;top:2px}.ext-ie .x-form-cb-label{top:1px}.ext-ie6 .x-form-cb-label,.ext-border-box .x-form-cb-label{top:3px}.x-form-display-field{padding-top:2px}.ext-gecko .x-form-display-field,.ext-strict .ext-ie7 .x-form-display-field{padding-top:1px}.ext-ie .x-form-display-field{padding-top:3px}.ext-strict .ext-ie8 .x-form-display-field{padding-top:0}.x-form-column{float:left;padding:0;margin:0;width:48%;overflow:hidden}.x-form .x-form-btns-ct .x-btn{float:right;clear:none}.x-form .x-form-btns-ct .x-form-btns td{border:0;padding:0}.x-form .x-form-btns-ct .x-form-btns-right table{float:right;clear:none}.x-form .x-form-btns-ct .x-form-btns-left table{float:left;clear:none}.x-form .x-form-btns-ct .x-form-btns-center{text-align:center}.x-form .x-form-btns-ct .x-form-btns-center table{margin:0 auto}.x-form .x-form-btns-ct table td.x-form-btn-td{padding:3px}.x-form .x-form-btns-ct .x-btn-focus .x-btn-left{background-position:0 -147px}.x-form .x-form-btns-ct .x-btn-focus .x-btn-right{background-position:0 -168px}.x-form .x-form-btns-ct .x-btn-focus .x-btn-center{background-position:0 -189px}.x-form .x-form-btns-ct .x-btn-click .x-btn-center{background-position:0 -126px}.x-form .x-form-btns-ct .x-btn-click .x-btn-right{background-position:0 -84px}.x-form .x-form-btns-ct .x-btn-click .x-btn-left{background-position:0 -63px}.x-form-invalid-icon{width:16px;height:18px;visibility:hidden;position:absolute;left:0;top:0;display:block;background:transparent no-repeat 0 2px}.x-fieldset{border:1px solid;padding:10px;margin-bottom:10px;display:block}.ext-webkit .x-fieldset-header{padding-top:1px}.ext-ie .x-fieldset legend{margin-bottom:10px}.ext-strict .ext-ie9 .x-fieldset legend.x-fieldset-header{padding-top:1px}.ext-ie .x-fieldset{padding-top:0;padding-bottom:10px}.x-fieldset legend .x-tool-toggle{margin-right:3px;margin-left:0;float:left!important}.x-fieldset legend input{margin-right:3px;float:left!important;height:13px;width:13px}fieldset.x-panel-collapsed{padding-bottom:0!important;border-width:1px 1px 0 1px!important;border-left-color:transparent;border-right-color:transparent}.ext-ie6 fieldset.x-panel-collapsed{padding-bottom:0!important;border-width:1px 0 0 0!important;margin-left:1px;margin-right:1px}fieldset.x-panel-collapsed .x-fieldset-bwrap{visibility:hidden;position:absolute;left:-1000px;top:-1000px}.x-fieldset-noborder{border:0 none transparent}.x-fieldset-noborder legend{margin-left:-3px}.ext-ie .x-fieldset-noborder legend{position:relative;margin-bottom:23px}.ext-ie .x-fieldset-noborder legend span{position:absolute;left:16px}.ext-gecko .x-window-body .x-form-item{outline:0;overflow:auto}.ext-mac.ext-gecko .x-window-body .x-form-item{overflow:hidden}.ext-gecko .x-form-item{outline:0}.x-hide-label label.x-form-item-label{display:none}.x-hide-label .x-form-element{padding-left:0!important}.x-form-label-top .x-hide-label label.x-form-item-label{display:none}.x-fieldset{overflow:hidden}.x-fieldset-bwrap{overflow:hidden}.x-fieldset-body{overflow:hidden}.x-btn{cursor:pointer;white-space:nowrap}.x-btn button{border:0 none;background-color:transparent;padding-left:3px;padding-right:3px;cursor:pointer;margin:0;overflow:visible;width:auto;outline:0 none}* html .ext-ie .x-btn button{width:1px}.ext-gecko .x-btn button,.ext-webkit .x-btn button{padding-left:0;padding-right:0}.ext-gecko .x-btn button::-moz-focus-inner{padding:0}.ext-ie .x-btn button{padding-top:2px}.x-btn td{padding:0!important}.x-btn-text{cursor:pointer;white-space:nowrap;padding:0}.x-btn-noicon .x-btn-small .x-btn-text{height:16px}.x-btn-noicon .x-btn-medium .x-btn-text{height:24px}.x-btn-noicon .x-btn-large .x-btn-text{height:32px}.x-btn-icon .x-btn-text{background-position:center;background-repeat:no-repeat}.x-btn-icon .x-btn-small .x-btn-text{height:16px;width:16px}.x-btn-icon .x-btn-medium .x-btn-text{height:24px;width:24px}.x-btn-icon .x-btn-large .x-btn-text{height:32px;width:32px}.x-btn-text-icon .x-btn-icon-small-left .x-btn-text{background-position:0 center;background-repeat:no-repeat;padding-left:18px;height:16px}.x-btn-text-icon .x-btn-icon-medium-left .x-btn-text{background-position:0 center;background-repeat:no-repeat;padding-left:26px;height:24px}.x-btn-text-icon .x-btn-icon-large-left .x-btn-text{background-position:0 center;background-repeat:no-repeat;padding-left:34px;height:32px}.x-btn-text-icon .x-btn-icon-small-top .x-btn-text{background-position:center 0;background-repeat:no-repeat;padding-top:18px}.x-btn-text-icon .x-btn-icon-medium-top .x-btn-text{background-position:center 0;background-repeat:no-repeat;padding-top:26px}.x-btn-text-icon .x-btn-icon-large-top .x-btn-text{background-position:center 0;background-repeat:no-repeat;padding-top:34px}.x-btn-text-icon .x-btn-icon-small-right .x-btn-text{background-position:right center;background-repeat:no-repeat;padding-right:18px;height:16px}.x-btn-text-icon .x-btn-icon-medium-right .x-btn-text{background-position:right center;background-repeat:no-repeat;padding-right:26px;height:24px}.x-btn-text-icon .x-btn-icon-large-right .x-btn-text{background-position:right center;background-repeat:no-repeat;padding-right:34px;height:32px}.x-btn-text-icon .x-btn-icon-small-bottom .x-btn-text{background-position:center bottom;background-repeat:no-repeat;padding-bottom:18px}.x-btn-text-icon .x-btn-icon-medium-bottom .x-btn-text{background-position:center bottom;background-repeat:no-repeat;padding-bottom:26px}.x-btn-text-icon .x-btn-icon-large-bottom .x-btn-text{background-position:center bottom;background-repeat:no-repeat;padding-bottom:34px}.x-btn-tr i,.x-btn-tl i,.x-btn-mr i,.x-btn-ml i,.x-btn-br i,.x-btn-bl i{font-size:1px;line-height:1px;width:3px;display:block;overflow:hidden}.x-btn-tr i,.x-btn-tl i,.x-btn-br i,.x-btn-bl i{height:3px}.x-btn-tl{width:3px;height:3px;background:no-repeat 0 0}.x-btn-tr{width:3px;height:3px;background:no-repeat -3px 0}.x-btn-tc{height:3px;background:repeat-x 0 -6px}.x-btn-ml{width:3px;background:no-repeat 0 -24px}.x-btn-mr{width:3px;background:no-repeat -3px -24px}.x-btn-mc{background:repeat-x 0 -1096px;vertical-align:middle;text-align:center;padding:0 5px;cursor:pointer;white-space:nowrap}.ext-strict .ext-ie6 .x-btn-mc,.ext-strict .ext-ie7 .x-btn-mc{height:100%}.x-btn-bl{width:3px;height:3px;background:no-repeat 0 -3px}.x-btn-br{width:3px;height:3px;background:no-repeat -3px -3px}.x-btn-bc{height:3px;background:repeat-x 0 -15px}.x-btn-over .x-btn-tl{background-position:-6px 0}.x-btn-over .x-btn-tr{background-position:-9px 0}.x-btn-over .x-btn-tc{background-position:0 -9px}.x-btn-over .x-btn-ml{background-position:-6px -24px}.x-btn-over .x-btn-mr{background-position:-9px -24px}.x-btn-over .x-btn-mc{background-position:0 -2168px}.x-btn-over .x-btn-bl{background-position:-6px -3px}.x-btn-over .x-btn-br{background-position:-9px -3px}.x-btn-over .x-btn-bc{background-position:0 -18px}.x-btn-click .x-btn-tl,.x-btn-menu-active .x-btn-tl,.x-btn-pressed .x-btn-tl{background-position:-12px 0}.x-btn-click .x-btn-tr,.x-btn-menu-active .x-btn-tr,.x-btn-pressed .x-btn-tr{background-position:-15px 0}.x-btn-click .x-btn-tc,.x-btn-menu-active .x-btn-tc,.x-btn-pressed .x-btn-tc{background-position:0 -12px}.x-btn-click .x-btn-ml,.x-btn-menu-active .x-btn-ml,.x-btn-pressed .x-btn-ml{background-position:-12px -24px}.x-btn-click .x-btn-mr,.x-btn-menu-active .x-btn-mr,.x-btn-pressed .x-btn-mr{background-position:-15px -24px}.x-btn-click .x-btn-mc,.x-btn-menu-active .x-btn-mc,.x-btn-pressed .x-btn-mc{background-position:0 -3240px}.x-btn-click .x-btn-bl,.x-btn-menu-active .x-btn-bl,.x-btn-pressed .x-btn-bl{background-position:-12px -3px}.x-btn-click .x-btn-br,.x-btn-menu-active .x-btn-br,.x-btn-pressed .x-btn-br{background-position:-15px -3px}.x-btn-click .x-btn-bc,.x-btn-menu-active .x-btn-bc,.x-btn-pressed .x-btn-bc{background-position:0 -21px}.x-btn-disabled *{cursor:default!important}.x-btn-mc em.x-btn-arrow{display:block;background:transparent no-repeat right center;padding-right:10px}.x-btn-mc em.x-btn-split{display:block;background:transparent no-repeat right center;padding-right:14px}.x-btn-mc em.x-btn-arrow-bottom{display:block;background:transparent no-repeat center bottom;padding-bottom:14px}.x-btn-mc em.x-btn-split-bottom{display:block;background:transparent no-repeat center bottom;padding-bottom:14px}.x-btn-as-arrow .x-btn-mc em{display:block;background-color:transparent;padding-bottom:14px}.x-btn-group{padding:1px}.x-btn-group-header{padding:2px;text-align:center}.x-btn-group-tc{background:transparent repeat-x 0 0;overflow:hidden}.x-btn-group-tl{background:transparent no-repeat 0 0;padding-left:3px}.x-btn-group-tr{background:transparent no-repeat right 0;padding-right:3px}.x-btn-group-bc{background:transparent repeat-x 0 bottom}.x-btn-group-bl{background:transparent no-repeat 0 bottom;padding-left:3px}.x-btn-group-br{background:transparent no-repeat right bottom;padding-right:3px}.x-btn-group-mc{border:0 none;padding:1px 0 0 0;margin:0}.x-btn-group-mc .x-btn-group-body{background-color:transparent;border:0 none}.x-btn-group-ml{background:transparent repeat-y 0 0;padding-left:3px}.x-btn-group-mr{background:transparent repeat-y right 0;padding-right:3px}.x-btn-group-bc .x-btn-group-footer{padding-bottom:6px}.x-panel-nofooter .x-btn-group-bc{height:3px;font-size:0;line-height:0}.x-btn-group-bwrap{overflow:hidden}.x-btn-group-body{overflow:hidden}.x-btn-group-notitle .x-btn-group-tc{background:transparent repeat-x 0 0;overflow:hidden;height:2px}.x-toolbar{border-style:solid;border-width:0 0 1px 0;display:block;padding:2px;background:repeat-x top left;position:relative;left:0;top:0;overflow:hidden}.x-toolbar-left{width:100%}.x-toolbar .x-item-disabled .x-btn-icon{opacity:.35;filter:alpha(opacity=35)}.x-toolbar td{vertical-align:middle}.x-toolbar td,.x-toolbar span,.x-toolbar input,.x-toolbar div,.x-toolbar select,.x-toolbar label{white-space:nowrap}.x-toolbar .x-item-disabled{cursor:default;opacity:.6;filter:alpha(opacity=60)}.x-toolbar .x-item-disabled *{cursor:default}.x-toolbar .x-toolbar-cell{vertical-align:middle}.x-toolbar .x-btn-tl,.x-toolbar .x-btn-tr,.x-toolbar .x-btn-tc,.x-toolbar .x-btn-ml,.x-toolbar .x-btn-mr,.x-toolbar .x-btn-mc,.x-toolbar .x-btn-bl,.x-toolbar .x-btn-br,.x-toolbar .x-btn-bc{background-position:500px 500px}.x-toolbar .x-btn-over .x-btn-tl{background-position:-6px 0}.x-toolbar .x-btn-over .x-btn-tr{background-position:-9px 0}.x-toolbar .x-btn-over .x-btn-tc{background-position:0 -9px}.x-toolbar .x-btn-over .x-btn-ml{background-position:-6px -24px}.x-toolbar .x-btn-over .x-btn-mr{background-position:-9px -24px}.x-toolbar .x-btn-over .x-btn-mc{background-position:0 -2168px}.x-toolbar .x-btn-over .x-btn-bl{background-position:-6px -3px}.x-toolbar .x-btn-over .x-btn-br{background-position:-9px -3px}.x-toolbar .x-btn-over .x-btn-bc{background-position:0 -18px}.x-toolbar .x-btn-click .x-btn-tl,.x-toolbar .x-btn-menu-active .x-btn-tl,.x-toolbar .x-btn-pressed .x-btn-tl{background-position:-12px 0}.x-toolbar .x-btn-click .x-btn-tr,.x-toolbar .x-btn-menu-active .x-btn-tr,.x-toolbar .x-btn-pressed .x-btn-tr{background-position:-15px 0}.x-toolbar .x-btn-click .x-btn-tc,.x-toolbar .x-btn-menu-active .x-btn-tc,.x-toolbar .x-btn-pressed .x-btn-tc{background-position:0 -12px}.x-toolbar .x-btn-click .x-btn-ml,.x-toolbar .x-btn-menu-active .x-btn-ml,.x-toolbar .x-btn-pressed .x-btn-ml{background-position:-12px -24px}.x-toolbar .x-btn-click .x-btn-mr,.x-toolbar .x-btn-menu-active .x-btn-mr,.x-toolbar .x-btn-pressed .x-btn-mr{background-position:-15px -24px}.x-toolbar .x-btn-click .x-btn-mc,.x-toolbar .x-btn-menu-active .x-btn-mc,.x-toolbar .x-btn-pressed .x-btn-mc{background-position:0 -3240px}.x-toolbar .x-btn-click .x-btn-bl,.x-toolbar .x-btn-menu-active .x-btn-bl,.x-toolbar .x-btn-pressed .x-btn-bl{background-position:-12px -3px}.x-toolbar .x-btn-click .x-btn-br,.x-toolbar .x-btn-menu-active .x-btn-br,.x-toolbar .x-btn-pressed .x-btn-br{background-position:-15px -3px}.x-toolbar .x-btn-click .x-btn-bc,.x-toolbar .x-btn-menu-active .x-btn-bc,.x-toolbar .x-btn-pressed .x-btn-bc{background-position:0 -21px}.x-toolbar div.xtb-text{padding:2px 2px 0;line-height:16px;display:block}.x-toolbar .xtb-sep{background-position:center;background-repeat:no-repeat;display:block;font-size:1px;height:16px;width:4px;overflow:hidden;cursor:default;margin:0 2px 0;border:0}.x-toolbar .xtb-spacer{width:2px}.x-tbar-page-number{width:30px;height:14px}.ext-ie .x-tbar-page-number{margin-top:2px}.x-paging-info{position:absolute;top:5px;right:8px}.x-toolbar-ct{width:100%}.x-toolbar-right td{text-align:center}.x-panel-tbar,.x-panel-bbar,.x-window-tbar,.x-window-bbar,.x-tab-panel-tbar,.x-tab-panel-bbar,.x-plain-tbar,.x-plain-bbar{overflow:hidden}.x-toolbar-more .x-btn-small .x-btn-text{height:16px;width:12px}.x-toolbar-more em.x-btn-arrow{display:inline;background-color:transparent;padding-right:0}.x-toolbar-more .x-btn-mc em.x-btn-arrow{background-image:none}div.x-toolbar-no-items{color:gray!important;padding:5px 10px!important}.ext-border-box .x-toolbar-cell .x-form-text{margin-bottom:-1px!important}.ext-border-box .x-toolbar-cell .x-form-field-wrap .x-form-text{margin:0!important}.ext-ie .x-toolbar-cell .x-form-field-wrap{height:21px}.ext-ie .x-toolbar-cell .x-form-text{position:relative;top:-1px}.ext-strict .ext-ie8 .x-toolbar-cell .x-form-field-trigger-wrap .x-form-text,.ext-strict .ext-ie .x-toolbar-cell .x-form-text{top:0}.x-toolbar-right td .x-form-field-trigger-wrap{text-align:left}.x-toolbar-cell .x-form-checkbox,.x-toolbar-cell .x-form-radio{margin-top:5px}.x-toolbar-cell .x-form-cb-label{vertical-align:bottom;top:1px}.ext-ie .x-toolbar-cell .x-form-checkbox,.ext-ie .x-toolbar-cell .x-form-radio{margin-top:4px}.ext-ie .x-toolbar-cell .x-form-cb-label{top:0}.x-grid3{position:relative;overflow:hidden}.x-grid-panel .x-panel-body{overflow:hidden!important}.x-grid-panel .x-panel-mc .x-panel-body{border:1px solid}.x-grid3 table{table-layout:fixed}.x-grid3-viewport{overflow:hidden}.x-grid3-hd-row td,.x-grid3-row td,.x-grid3-summary-row td{outline:0;-moz-user-focus:normal}.x-grid3-row td,.x-grid3-summary-row td{line-height:13px;vertical-align:top;padding-left:1px;padding-right:1px;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-grid3-cell{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-grid3-hd-row td{line-height:15px;vertical-align:middle;border-left:1px solid;border-right:1px solid}.x-grid3-hd-row .x-grid3-marker-hd{padding:3px}.x-grid3-row .x-grid3-marker{padding:3px}.x-grid3-cell-inner,.x-grid3-hd-inner{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;padding:3px 3px 3px 5px;white-space:nowrap}.x-action-col-cell .x-grid3-cell-inner{padding-top:1px;padding-bottom:1px}.x-action-col-icon{cursor:pointer}.x-grid3-hd-inner{position:relative;cursor:inherit;padding:4px 3px 4px 5px}.x-grid3-row-body{white-space:normal}.x-grid3-body-cell{outline:0 none}.ext-ie .x-grid3-cell-inner,.ext-ie .x-grid3-hd-inner{width:100%}.ext-strict .x-grid3-cell-inner,.ext-strict .x-grid3-hd-inner{width:auto}.x-grid-row-loading{background:no-repeat center center}.x-grid-page{overflow:hidden}.x-grid3-row{cursor:default;border:1px solid;width:100%}.x-grid3-row-over{border:1px solid;background:repeat-x left top}.x-grid3-resize-proxy{width:1px;left:0;cursor:e-resize;cursor:col-resize;position:absolute;top:0;height:100px;overflow:hidden;visibility:hidden;border:0 none;z-index:7}.x-grid3-resize-marker{width:1px;left:0;position:absolute;top:0;height:100px;overflow:hidden;visibility:hidden;border:0 none;z-index:7}.x-grid3-focus{position:absolute;left:0;top:0;width:1px;height:1px;line-height:1px;font-size:1px;outline:0 none;-moz-user-select:text;-khtml-user-select:text;-webkit-user-select:ignore}.x-grid3-header{background:repeat-x 0 bottom;cursor:default;padding:1px 0 0 0}.x-grid3-header-pop{border-left:1px solid;float:right;clear:none}.x-grid3-header-pop-inner{border-left:1px solid;width:14px;height:19px;background:transparent no-repeat center center}.ext-ie .x-grid3-header-pop-inner{width:15px}.ext-strict .x-grid3-header-pop-inner{width:14px}.x-grid3-header-inner{overflow:hidden;float:left}.x-grid3-header-offset{padding-left:1px;text-align:left}td.x-grid3-hd-over,td.sort-desc,td.sort-asc,td.x-grid3-hd-menu-open{border-left:1px solid;border-right:1px solid}td.x-grid3-hd-over .x-grid3-hd-inner,td.sort-desc .x-grid3-hd-inner,td.sort-asc .x-grid3-hd-inner,td.x-grid3-hd-menu-open .x-grid3-hd-inner{background:repeat-x left bottom}.x-grid3-sort-icon{background-repeat:no-repeat;display:none;height:4px;width:13px;margin-left:3px;vertical-align:middle}.sort-asc .x-grid3-sort-icon,.sort-desc .x-grid3-sort-icon{display:inline}.ext-strict .ext-ie .x-grid3-header-inner,.ext-strict .ext-ie6 .x-grid3-hd{position:relative}.ext-strict .ext-ie6 .x-grid3-hd-inner{position:static}.x-grid3-scroller{overflow:auto;position:relative}.x-grid3-cell-text,.x-grid3-hd-text{display:block;padding:3px 5px 3px 5px;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-grid3-split{background-position:center;background-repeat:no-repeat;cursor:e-resize;cursor:col-resize;display:block;font-size:1px;height:16px;overflow:hidden;position:absolute;top:2px;width:6px;z-index:3}.x-dd-drag-proxy .x-grid3-hd-inner{background:repeat-x left bottom;width:120px;padding:3px;border:1px solid;overflow:hidden}.col-move-top,.col-move-bottom{width:9px;height:9px;position:absolute;top:0;line-height:1px;font-size:1px;overflow:hidden;visibility:hidden;z-index:20000;background:transparent no-repeat left top}.x-grid3-row-selected{border:1px dotted}.x-grid3-locked td.x-grid3-row-marker,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker{background:repeat-x 0 bottom!important;vertical-align:middle!important;padding:0;border-top:1px solid;border-bottom:none!important;border-right:1px solid!important;text-align:center}.x-grid3-locked td.x-grid3-row-marker div,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker div{padding:0 4px;text-align:center}.x-grid3-dirty-cell{background:transparent no-repeat 0 0}.x-grid3-topbar,.x-grid3-bottombar{overflow:hidden;display:none;position:relative}.x-grid3-topbar .x-toolbar{border-right:0 none}.x-grid3-bottombar .x-toolbar{border-right:0 none;border-bottom:0 none;border-top:1px solid}.x-props-grid .x-grid3-cell{padding:1px}.x-props-grid .x-grid3-td-name .x-grid3-cell-inner{background:transparent repeat-y -16px!important;padding-left:12px}.x-props-grid .x-grid3-body .x-grid3-td-name{padding:1px;padding-right:0;border:0 none;border-right:1px solid}.x-grid3-col-dd{border:0 none;padding:0;background-color:transparent}.x-dd-drag-ghost .x-grid3-dd-wrap{padding:1px 3px 3px 1px}.x-grid3-hd{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-grid3-hd-btn{display:none;position:absolute;width:14px;background:no-repeat left center;right:0;top:0;z-index:2;cursor:pointer}.x-grid3-hd-over .x-grid3-hd-btn,.x-grid3-hd-menu-open .x-grid3-hd-btn{display:block}a.x-grid3-hd-btn:hover{background-position:-14px center}.x-grid3-body .x-grid3-td-expander{background:transparent repeat-y right}.x-grid3-body .x-grid3-td-expander .x-grid3-cell-inner{padding:0!important;height:100%}.x-grid3-row-expander{width:100%;height:18px;background-position:4px 2px;background-repeat:no-repeat;background-color:transparent}.x-grid3-row-collapsed .x-grid3-row-expander{background-position:4px 2px}.x-grid3-row-expanded .x-grid3-row-expander{background-position:-21px 2px}.x-grid3-row-collapsed .x-grid3-row-body{display:none!important}.x-grid3-row-expanded .x-grid3-row-body{display:block!important}.x-grid3-body .x-grid3-td-checker{background:transparent repeat-y right}.x-grid3-body .x-grid3-td-checker .x-grid3-cell-inner,.x-grid3-header .x-grid3-td-checker .x-grid3-hd-inner{padding:0!important;height:100%}.x-grid3-row-checker,.x-grid3-hd-checker{width:100%;height:23px;background-position:2px 2px;background-repeat:no-repeat;background-color:transparent}.x-grid3-row .x-grid3-row-checker{background-position:2px 6px}.x-grid3-row-selected .x-grid3-row-checker,.x-grid3-hd-checker-on .x-grid3-hd-checker,.x-grid3-row-checked .x-grid3-row-checker{background-position:-23px 6px}.x-grid3-hd-checker{background-position:2px 3px}.ext-border-box .x-grid3-hd-checker{background-position:2px 3px}.x-grid3-hd-checker-on .x-grid3-hd-checker{background-position:-23px 3px}.ext-border-box .x-grid3-hd-checker-on .x-grid3-hd-checker{background-position:-23px 3px}.x-grid3-body .x-grid3-td-numberer{background:transparent repeat-y right}.x-grid3-body .x-grid3-td-numberer .x-grid3-cell-inner{padding:3px 5px 0 0!important;text-align:right}.x-grid3-body .x-grid3-td-row-icon{background:transparent repeat-y right;vertical-align:top;text-align:center}.x-grid3-body .x-grid3-td-row-icon .x-grid3-cell-inner{padding:0!important;background-position:center center;background-repeat:no-repeat;width:16px;height:16px;margin-left:2px;margin-top:3px}.x-grid3-body .x-grid3-row-selected .x-grid3-td-numberer,.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker,.x-grid3-body .x-grid3-row-selected .x-grid3-td-expander{background:transparent repeat-y right}.x-grid3-body .x-grid3-check-col-td .x-grid3-cell-inner{padding:1px 0 0 0!important}.x-grid3-check-col{width:100%;height:16px;background-position:center center;background-repeat:no-repeat;background-color:transparent}.x-grid3-check-col-on{width:100%;height:16px;background-position:center center;background-repeat:no-repeat;background-color:transparent}.x-grid-group-hd{border-bottom:2px solid;cursor:pointer;padding-top:6px}.x-grid-group-hd div.x-grid-group-title{background:transparent no-repeat 3px 3px;padding:4px 4px 4px 17px}.x-grid-group-collapsed .x-grid-group-body{display:none}.ext-ie6 .x-grid3 .x-editor .x-form-text,.ext-ie7 .x-grid3 .x-editor .x-form-text{position:relative;top:-1px}.ext-ie .x-props-grid .x-editor .x-form-text{position:static;top:0}.x-grid-empty{padding:10px}.ext-ie7 .x-grid-panel .x-panel-bbar{position:relative}.ext-ie7 .x-grid-panel .x-panel-mc .x-panel-bbar{position:static}.ext-ie6 .x-grid3-header{position:relative}.ext-webkit .x-grid-panel .x-panel-bwrap{-webkit-user-select:none}.ext-webkit .x-tbar-page-number{-webkit-user-select:ignore}.x-grid-with-col-lines .x-grid3-row td.x-grid3-cell{padding-right:0;border-right:1px solid}.x-pivotgrid .x-grid3-header-offset table{width:100%;border-collapse:collapse}.x-pivotgrid .x-grid3-header-offset table td{padding:4px 3px 4px 5px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px;line-height:13px;font-family:tahoma}.x-pivotgrid .x-grid3-row-headers{display:block;float:left}.x-pivotgrid .x-grid3-row-headers table{height:100%;width:100%;border-collapse:collapse}.x-pivotgrid .x-grid3-row-headers table td{height:18px;padding:2px 7px 0 0;text-align:right;text-overflow:ellipsis;font-size:11px;font-family:tahoma}.ext-gecko .x-pivotgrid .x-grid3-row-headers table td{height:21px}.x-grid3-header-title{top:0;left:0;position:absolute;text-align:center;vertical-align:middle;font-family:tahoma;font-size:11px;padding:1px;display:table-cell}.x-grid3-header-title span{position:absolute;top:50%;left:0;width:100%;margin-top:-6px}.x-dd-drag-proxy{position:absolute;left:0;top:0;visibility:hidden;z-index:15000}.x-dd-drag-ghost{opacity:.85;filter:alpha(opacity=85);border:1px solid;padding:3px;padding-left:20px;white-space:nowrap}.x-dd-drag-repair .x-dd-drag-ghost{opacity:.4;filter:alpha(opacity=40);border:0 none;padding:0;background-color:transparent}.x-dd-drag-repair .x-dd-drop-icon{visibility:hidden}.x-dd-drop-icon{position:absolute;top:3px;left:3px;display:block;width:16px;height:16px;background-color:transparent;background-position:center;background-repeat:no-repeat;z-index:1}.x-view-selector{position:absolute;left:0;top:0;width:0;border:1px dotted;opacity:.5;filter:alpha(opacity=50)}.ext-strict .ext-ie .x-tree .x-panel-bwrap{position:relative;overflow:hidden}.x-tree-icon,.x-tree-ec-icon,.x-tree-elbow-line,.x-tree-elbow,.x-tree-elbow-end,.x-tree-elbow-plus,.x-tree-elbow-minus,.x-tree-elbow-end-plus,.x-tree-elbow-end-minus{border:0 none;height:18px;margin:0;padding:0;vertical-align:top;width:16px;background-repeat:no-repeat}.x-tree-node-collapsed .x-tree-node-icon,.x-tree-node-expanded .x-tree-node-icon,.x-tree-node-leaf .x-tree-node-icon{border:0 none;height:18px;margin:0;padding:0;vertical-align:top;width:16px;background-position:center;background-repeat:no-repeat}.ext-ie .x-tree-node-indent img,.ext-ie .x-tree-node-icon,.ext-ie .x-tree-ec-icon{vertical-align:middle!important}.ext-strict .ext-ie8 .x-tree-node-indent img,.ext-strict .ext-ie8 .x-tree-node-icon,.ext-strict .ext-ie8 .x-tree-ec-icon{vertical-align:top!important}input.x-tree-node-cb{margin-left:1px;height:19px;vertical-align:bottom}.ext-ie input.x-tree-node-cb{margin-left:0;margin-top:1px;width:16px;height:16px;vertical-align:middle}.ext-strict .ext-ie8 input.x-tree-node-cb{margin:1px 1px;height:14px;vertical-align:bottom}.ext-strict .ext-ie8 input.x-tree-node-cb+a{vertical-align:bottom}.ext-opera input.x-tree-node-cb{height:14px;vertical-align:middle}.x-tree-noicon .x-tree-node-icon{width:0;height:0}.x-tree-no-lines .x-tree-elbow{background-color:transparent}.x-tree-no-lines .x-tree-elbow-end{background-color:transparent}.x-tree-no-lines .x-tree-elbow-line{background-color:transparent}.x-tree-arrows .x-tree-elbow{background-color:transparent}.x-tree-arrows .x-tree-elbow-plus{background:transparent no-repeat 0 0}.x-tree-arrows .x-tree-elbow-minus{background:transparent no-repeat -16px 0}.x-tree-arrows .x-tree-elbow-end{background-color:transparent}.x-tree-arrows .x-tree-elbow-end-plus{background:transparent no-repeat 0 0}.x-tree-arrows .x-tree-elbow-end-minus{background:transparent no-repeat -16px 0}.x-tree-arrows .x-tree-elbow-line{background-color:transparent}.x-tree-arrows .x-tree-ec-over .x-tree-elbow-plus{background-position:-32px 0}.x-tree-arrows .x-tree-ec-over .x-tree-elbow-minus{background-position:-48px 0}.x-tree-arrows .x-tree-ec-over .x-tree-elbow-end-plus{background-position:-32px 0}.x-tree-arrows .x-tree-ec-over .x-tree-elbow-end-minus{background-position:-48px 0}.x-tree-elbow-plus,.x-tree-elbow-minus,.x-tree-elbow-end-plus,.x-tree-elbow-end-minus{cursor:pointer}.ext-ie ul.x-tree-node-ct{font-size:0;line-height:0}.x-tree-node{white-space:nowrap}.x-tree-node-el{line-height:18px;cursor:pointer}.x-tree-node a,.x-dd-drag-ghost a{text-decoration:none;-khtml-user-select:none;-moz-user-select:none;-webkit-user-select:ignore;-kthml-user-focus:normal;-moz-user-focus:normal;outline:0 none}.x-tree-node a span,.x-dd-drag-ghost a span{text-decoration:none;padding:1px 3px 1px 2px}.x-tree-node .x-tree-node-disabled .x-tree-node-icon{opacity:.5;filter:alpha(opacity=50)}.x-tree-node .x-tree-node-inline-icon{background-color:transparent}.x-tree-node a:hover,.x-dd-drag-ghost a:hover{text-decoration:none}.x-tree-node div.x-tree-drag-insert-below{border-bottom:1px dotted}.x-tree-node div.x-tree-drag-insert-above{border-top:1px dotted}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below{border-bottom:0 none}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above{border-top:0 none}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a{border-bottom:2px solid}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a{border-top:2px solid}.x-tree-node .x-tree-drag-append a span{border:1px dotted}.x-dd-drag-ghost .x-tree-node-indent,.x-dd-drag-ghost .x-tree-ec-icon{display:none!important}.x-date-picker{border:1px solid;border-top:0 none;position:relative}.x-date-picker a{outline:0 none}.x-date-inner,.x-date-inner td,.x-date-inner th{border-collapse:separate}.x-date-middle,.x-date-left,.x-date-right{background:repeat-x 0 -83px;overflow:hidden}.x-date-middle .x-btn-tc,.x-date-middle .x-btn-tl,.x-date-middle .x-btn-tr,.x-date-middle .x-btn-mc,.x-date-middle .x-btn-ml,.x-date-middle .x-btn-mr,.x-date-middle .x-btn-bc,.x-date-middle .x-btn-bl,.x-date-middle .x-btn-br{background:transparent!important;vertical-align:middle}.x-date-middle .x-btn-mc em.x-btn-arrow{background:transparent no-repeat right 0}.x-date-right,.x-date-left{width:18px}.x-date-right{text-align:right}.x-date-middle{padding-top:2px;padding-bottom:2px;width:130px}.x-date-right a,.x-date-left a{display:block;width:16px;height:16px;background-position:center;background-repeat:no-repeat;cursor:pointer;opacity:.6;filter:alpha(opacity=60)}.x-date-right a:hover,.x-date-left a:hover{opacity:1;filter:alpha(opacity=100)}.x-item-disabled .x-date-right a:hover,.x-item-disabled .x-date-left a:hover{opacity:.6;filter:alpha(opacity=60)}.x-date-right a{margin-right:2px;text-decoration:none!important}.x-date-left a{margin-left:2px;text-decoration:none!important}table.x-date-inner{width:100%;table-layout:fixed}.ext-webkit table.x-date-inner{width:175px}.x-date-inner th{width:25px}.x-date-inner th{background:repeat-x left top;text-align:right!important;border-bottom:1px solid;cursor:default;padding:0;border-collapse:separate}.x-date-inner th span{display:block;padding:2px;padding-right:7px}.x-date-inner td{border:1px solid;text-align:right;padding:0}.x-date-inner a{padding:2px 5px;display:block;text-decoration:none;text-align:right}.x-date-inner .x-date-active{cursor:pointer;color:black}.x-date-inner .x-date-selected a{background:repeat-x left top;border:1px solid;padding:1px 4px}.x-date-inner .x-date-today a{border:1px solid;padding:1px 4px}.x-date-inner .x-date-prevday a,.x-date-inner .x-date-nextday a{text-decoration:none!important}.x-date-bottom{padding:4px;border-top:1px solid;background:repeat-x left top}.x-date-inner a:hover,.x-date-inner .x-date-disabled a:hover{text-decoration:none!important}.x-item-disabled .x-date-inner a:hover{background:0}.x-date-inner .x-date-disabled a{cursor:default}.x-date-menu .x-menu-item{padding:1px 24px 1px 4px;white-space:nowrap}.x-date-menu .x-menu-item .x-menu-item-icon{width:10px;height:10px;margin-right:5px;background-position:center -4px!important}.x-date-mp{position:absolute;left:0;top:0;display:none}.x-date-mp td{padding:2px;font:normal 11px arial,helvetica,tahoma,sans-serif}td.x-date-mp-month,td.x-date-mp-year,td.x-date-mp-ybtn{border:0 none;text-align:center;vertical-align:middle;width:25%}.x-date-mp-ok{margin-right:3px}.x-date-mp-btns button{text-decoration:none;text-align:center;text-decoration:none!important;border:1px solid;padding:1px 3px 1px;cursor:pointer}.x-date-mp-btns{background:repeat-x left top}.x-date-mp-btns td{border-top:1px solid;text-align:center}td.x-date-mp-month a,td.x-date-mp-year a{display:block;padding:2px 4px;text-decoration:none;text-align:center}td.x-date-mp-month a:hover,td.x-date-mp-year a:hover{text-decoration:none;cursor:pointer}td.x-date-mp-sel a{padding:1px 3px;background:repeat-x left top;border:1px solid}.x-date-mp-ybtn a{overflow:hidden;width:15px;height:15px;cursor:pointer;background:transparent no-repeat;display:block;margin:0 auto}.x-date-mp-ybtn a.x-date-mp-next{background-position:0 -120px}.x-date-mp-ybtn a.x-date-mp-next:hover{background-position:-15px -120px}.x-date-mp-ybtn a.x-date-mp-prev{background-position:0 -105px}.x-date-mp-ybtn a.x-date-mp-prev:hover{background-position:-15px -105px}.x-date-mp-ybtn{text-align:center}td.x-date-mp-sep{border-right:1px solid}.x-tip{position:absolute;top:0;left:0;visibility:hidden;z-index:20002;border:0 none}.x-tip .x-tip-close{height:15px;float:right;width:15px;margin:0 0 2px 2px;cursor:pointer;display:none}.x-tip .x-tip-tc{background:transparent no-repeat 0 -62px;padding-top:3px;overflow:hidden}.x-tip .x-tip-tl{background:transparent no-repeat 0 0;padding-left:6px;overflow:hidden}.x-tip .x-tip-tr{background:transparent no-repeat right 0;padding-right:6px;overflow:hidden}.x-tip .x-tip-bc{background:transparent no-repeat 0 -121px;height:3px;overflow:hidden}.x-tip .x-tip-bl{background:transparent no-repeat 0 -59px;padding-left:6px}.x-tip .x-tip-br{background:transparent no-repeat right -59px;padding-right:6px}.x-tip .x-tip-mc{border:0 none}.x-tip .x-tip-ml{background:no-repeat 0 -124px;padding-left:6px}.x-tip .x-tip-mr{background:transparent no-repeat right -124px;padding-right:6px}.ext-ie .x-tip .x-tip-header,.ext-ie .x-tip .x-tip-tc{font-size:0;line-height:0}.ext-border-box .x-tip .x-tip-header,.ext-border-box .x-tip .x-tip-tc{line-height:1px}.x-tip .x-tip-header-text{padding:0;margin:0 0 2px 0}.x-tip .x-tip-body{margin:0!important;line-height:14px;padding:0}.x-tip .x-tip-body .loading-indicator{margin:0}.x-tip-draggable .x-tip-header,.x-tip-draggable .x-tip-header-text{cursor:move}.x-form-invalid-tip .x-tip-tc{background:repeat-x 0 -12px;padding-top:6px}.x-form-invalid-tip .x-tip-bc{background:repeat-x 0 -18px;height:6px}.x-form-invalid-tip .x-tip-bl{background:no-repeat 0 -6px}.x-form-invalid-tip .x-tip-br{background:no-repeat right -6px}.x-form-invalid-tip .x-tip-body{padding:2px}.x-form-invalid-tip .x-tip-body{padding-left:24px;background:transparent no-repeat 2px 2px}.x-tip-anchor{position:absolute;width:9px;height:10px;overflow:hidden;background:transparent no-repeat 0 0}.x-tip-anchor-bottom{background-position:-9px 0}.x-tip-anchor-right{background-position:-18px 0;width:10px}.x-tip-anchor-left{background-position:-28px 0;width:10px}.x-menu{z-index:15000;background:repeat-y}.x-menu-floating{border:1px solid}.x-menu a{text-decoration:none!important}.ext-ie .x-menu{overflow:hidden}.x-menu-list{padding:2px;background-color:transparent;border:0 none;overflow:hidden;overflow-y:hidden}.ext-strict .ext-ie .x-menu-list{position:relative}.x-menu li{line-height:100%}.x-menu li.x-menu-sep-li{font-size:1px;line-height:1px}.x-menu-list-item{white-space:nowrap;display:block;padding:1px}.x-menu-item{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore}.x-menu-item-arrow{background:transparent no-repeat right}.x-menu-sep{display:block;font-size:1px;line-height:1px;margin:2px 3px;border-bottom:1px solid;overflow:hidden}.x-menu-focus{position:absolute;left:-1px;top:-1px;width:1px;height:1px;line-height:1px;font-size:1px;outline:0 none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore;overflow:hidden;display:block}a.x-menu-item{cursor:pointer;display:block;line-height:16px;outline-color:-moz-use-text-color;outline-style:none;outline-width:0;padding:3px 21px 3px 27px;position:relative;text-decoration:none;white-space:nowrap}.x-menu-item-active{background-repeat:repeat-x;background-position:left bottom;border-style:solid;border-width:1px 0;margin:0 1px;padding:0}.x-menu-item-active a.x-menu-item{border-style:solid;border-width:0 1px;margin:0 -1px}.x-menu-item-icon{border:0 none;height:16px;padding:0;vertical-align:top;width:16px;position:absolute;left:3px;top:3px;margin:0;background-position:center}.ext-ie .x-menu-item-icon{left:-24px}.ext-strict .x-menu-item-icon{left:3px}.ext-ie6 .x-menu-item-icon{left:-24px}.ext-ie .x-menu-item-icon{vertical-align:middle}.x-menu-check-item .x-menu-item-icon{background:transparent no-repeat center}.x-menu-group-item .x-menu-item-icon{background-color:transparent}.x-menu-item-checked .x-menu-group-item .x-menu-item-icon{background:transparent no-repeat center}.x-date-menu .x-menu-list{padding:0}.x-menu-date-item{padding:0}.x-menu .x-color-palette,.x-menu .x-date-picker{margin-left:26px;margin-right:4px}.x-menu .x-date-picker{border:1px solid;margin-top:2px;margin-bottom:2px}.x-menu-plain .x-color-palette,.x-menu-plain .x-date-picker{margin:0;border:0 none}.x-date-menu{padding:0!important}.ext-strict .ext-ie6 .x-menu-sep-li{padding:3px 4px}.ext-strict .ext-ie6 .x-menu-sep{margin:0;height:1px}.ext-webkit .x-menu-sep{height:1px}.ext-ie .x-date-menu{height:199px}.ext-strict .ext-ie .x-date-menu,.ext-border-box .ext-ie8 .x-date-menu{height:197px}.ext-strict .ext-ie7 .x-date-menu{height:195px}.ext-strict .ext-ie8 .x-date-menu{height:auto}.x-cycle-menu .x-menu-item-checked{border:1px dotted!important;padding:0}.x-menu .x-menu-scroller{width:100%;background-repeat:no-repeat;background-position:center;height:8px;line-height:8px;cursor:pointer;margin:0;padding:0}.x-menu .x-menu-scroller-active{height:6px;line-height:6px}.x-menu-list-item-indent{padding-left:27px}.x-box-tl{background:transparent no-repeat 0 0}.x-box-tc{height:8px;background:transparent repeat-x 0 0;overflow:hidden}.x-box-tr{background:transparent no-repeat right -8px}.x-box-ml{background:transparent repeat-y 0;padding-left:4px;overflow:hidden}.x-box-mc{background:repeat-x 0 -16px;padding:4px 10px}.x-box-mc h3{margin:0 0 4px 0}.x-box-mr{background:transparent repeat-y right;padding-right:4px;overflow:hidden}.x-box-bl{background:transparent no-repeat 0 -16px}.x-box-bc{background:transparent repeat-x 0 -8px;height:8px;overflow:hidden}.x-box-br{background:transparent no-repeat right -24px}.x-box-tl,.x-box-bl{padding-left:8px;overflow:hidden}.x-box-tr,.x-box-br{padding-right:8px;overflow:hidden}.x-combo-list{border:1px solid;overflow:hidden}.x-combo-list-inner{overflow:auto;position:relative;overflow-x:hidden}.x-combo-list-hd{border-bottom:1px solid;padding:3px}.x-resizable-pinned .x-combo-list-inner{border-bottom:1px solid}.x-combo-list-item{padding:2px;border:1px solid;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.x-combo-list .x-combo-selected{border:1px dotted!important;cursor:pointer}.x-combo-list .x-toolbar{border-top:1px solid;border-bottom:0 none}.x-panel{border-style:solid;border-width:0}.x-panel-header{overflow:hidden;padding:5px 3px 4px 5px;border:1px solid;line-height:15px;background:transparent repeat-x 0 -1px}.x-panel-body{border:1px solid;border-top:0 none;overflow:hidden;position:relative}.x-panel-bbar .x-toolbar,.x-panel-tbar .x-toolbar{border:1px solid;border-top:0 none;overflow:hidden;padding:2px}.x-panel-tbar-noheader .x-toolbar,.x-panel-mc .x-panel-tbar .x-toolbar{border-top:1px solid;border-bottom:0 none}.x-panel-body-noheader,.x-panel-mc .x-panel-body{border-top:1px solid}.x-panel-header{overflow:hidden}.x-panel-tl .x-panel-header{padding:5px 0 4px 0;border:0 none;background:transparent no-repeat}.x-panel-tl .x-panel-icon,.x-window-tl .x-panel-icon{padding-left:20px!important;background-repeat:no-repeat;background-position:0 4px}.x-panel-inline-icon{width:16px;height:16px;background-repeat:no-repeat;background-position:0 0;vertical-align:middle;margin-right:4px;margin-top:-1px;margin-bottom:-1px}.x-panel-tc{background:transparent repeat-x 0 0;overflow:hidden}.ext-strict .ext-ie7 .x-panel-tc{overflow:visible}.x-panel-tl{background:transparent no-repeat 0 0;padding-left:6px;border-bottom:1px solid}.x-panel-tr{background:transparent no-repeat right 0;padding-right:6px}.x-panel-bc{background:transparent repeat-x 0 bottom}.x-panel-bl{background:transparent no-repeat 0 bottom;padding-left:0}.x-panel-br{background:transparent no-repeat right bottom;padding-right:0}.x-panel-mc{border:0 none;padding:0;margin:0;padding-top:6px}.x-panel-mc .x-panel-body{background-color:transparent;border:0 none}.x-panel-ml{background:repeat-y 0 0;padding-left:6px}.x-panel-mr{background:transparent repeat-y right 0;padding-right:6px}.x-panel-bc .x-panel-footer{padding-bottom:6px}.x-panel-nofooter .x-panel-bc,.x-panel-nofooter .x-window-bc{height:6px;font-size:0;line-height:0}.x-panel-bwrap{overflow:hidden;left:0;top:0}.x-panel-body{overflow:hidden}.x-panel-collapsed .x-resizable-handle{display:none}.ext-gecko .x-panel-animated div{overflow:hidden!important}.x-plain-body{overflow:hidden}.x-plain-bbar .x-toolbar{overflow:hidden;padding:2px}.x-plain-tbar .x-toolbar{overflow:hidden;padding:2px}.x-plain-bwrap{overflow:hidden}.x-plain{overflow:hidden}.x-tool{overflow:hidden;width:15px;height:15px;float:right;cursor:pointer;background:transparent no-repeat;margin-left:2px}.x-tool-toggle{background-position:0 -60px}.x-tool-toggle-over{background-position:-15px -60px}.x-panel-collapsed .x-tool-toggle{background-position:0 -75px}.x-panel-collapsed .x-tool-toggle-over{background-position:-15px -75px}.x-tool-close{background-position:0 -0}.x-tool-close-over{background-position:-15px 0}.x-tool-minimize{background-position:0 -15px}.x-tool-minimize-over{background-position:-15px -15px}.x-tool-maximize{background-position:0 -30px}.x-tool-maximize-over{background-position:-15px -30px}.x-tool-restore{background-position:0 -45px}.x-tool-restore-over{background-position:-15px -45px}.x-tool-gear{background-position:0 -90px}.x-tool-gear-over{background-position:-15px -90px}.x-tool-prev{background-position:0 -105px}.x-tool-prev-over{background-position:-15px -105px}.x-tool-next{background-position:0 -120px}.x-tool-next-over{background-position:-15px -120px}.x-tool-pin{background-position:0 -135px}.x-tool-pin-over{background-position:-15px -135px}.x-tool-unpin{background-position:0 -150px}.x-tool-unpin-over{background-position:-15px -150px}.x-tool-right{background-position:0 -165px}.x-tool-right-over{background-position:-15px -165px}.x-tool-left{background-position:0 -180px}.x-tool-left-over{background-position:-15px -180px}.x-tool-down{background-position:0 -195px}.x-tool-down-over{background-position:-15px -195px}.x-tool-up{background-position:0 -210px}.x-tool-up-over{background-position:-15px -210px}.x-tool-refresh{background-position:0 -225px}.x-tool-refresh-over{background-position:-15px -225px}.x-tool-plus{background-position:0 -240px}.x-tool-plus-over{background-position:-15px -240px}.x-tool-minus{background-position:0 -255px}.x-tool-minus-over{background-position:-15px -255px}.x-tool-search{background-position:0 -270px}.x-tool-search-over{background-position:-15px -270px}.x-tool-save{background-position:0 -285px}.x-tool-save-over{background-position:-15px -285px}.x-tool-help{background-position:0 -300px}.x-tool-help-over{background-position:-15px -300px}.x-tool-print{background-position:0 -315px}.x-tool-print-over{background-position:-15px -315px}.x-tool-expand{background-position:0 -330px}.x-tool-expand-over{background-position:-15px -330px}.x-tool-collapse{background-position:0 -345px}.x-tool-collapse-over{background-position:-15px -345px}.x-tool-resize{background-position:0 -360px}.x-tool-resize-over{background-position:-15px -360px}.x-tool-move{background-position:0 -375px}.x-tool-move-over{background-position:-15px -375px}.x-panel-ghost{z-index:12000;overflow:hidden;position:absolute;left:0;top:0;opacity:.65;filter:alpha(opacity=65)}.x-panel-ghost ul{margin:0;padding:0;overflow:hidden;font-size:0;line-height:0;border:1px solid;border-top:0 none;display:block}.x-panel-ghost *{cursor:move!important}.x-panel-dd-spacer{border:2px dashed}.x-panel-btns{padding:5px;overflow:hidden}.x-panel-btns td.x-toolbar-cell{padding:3px}.x-panel-btns .x-btn-focus .x-btn-left{background-position:0 -147px}.x-panel-btns .x-btn-focus .x-btn-right{background-position:0 -168px}.x-panel-btns .x-btn-focus .x-btn-center{background-position:0 -189px}.x-panel-btns .x-btn-over .x-btn-left{background-position:0 -63px}.x-panel-btns .x-btn-over .x-btn-right{background-position:0 -84px}.x-panel-btns .x-btn-over .x-btn-center{background-position:0 -105px}.x-panel-btns .x-btn-click .x-btn-center{background-position:0 -126px}.x-panel-btns .x-btn-click .x-btn-right{background-position:0 -84px}.x-panel-btns .x-btn-click .x-btn-left{background-position:0 -63px}.x-panel-fbar td,.x-panel-fbar span,.x-panel-fbar input,.x-panel-fbar div,.x-panel-fbar select,.x-panel-fbar label{white-space:nowrap}.x-panel-reset .x-panel-body html,.x-panel-reset .x-panel-body address,.x-panel-reset .x-panel-body blockquote,.x-panel-reset .x-panel-body body,.x-panel-reset .x-panel-body dd,.x-panel-reset .x-panel-body div,.x-panel-reset .x-panel-body dl,.x-panel-reset .x-panel-body dt,.x-panel-reset .x-panel-body fieldset,.x-panel-reset .x-panel-body form,.x-panel-reset .x-panel-body frame,frameset,.x-panel-reset .x-panel-body h1,.x-panel-reset .x-panel-body h2,.x-panel-reset .x-panel-body h3,.x-panel-reset .x-panel-body h4,.x-panel-reset .x-panel-body h5,.x-panel-reset .x-panel-body h6,.x-panel-reset .x-panel-body noframes,.x-panel-reset .x-panel-body ol,.x-panel-reset .x-panel-body p,.x-panel-reset .x-panel-body ul,.x-panel-reset .x-panel-body center,.x-panel-reset .x-panel-body dir,.x-panel-reset .x-panel-body hr,.x-panel-reset .x-panel-body menu,.x-panel-reset .x-panel-body pre{display:block}.x-panel-reset .x-panel-body li{display:list-item}.x-panel-reset .x-panel-body head{display:none}.x-panel-reset .x-panel-body table{display:table}.x-panel-reset .x-panel-body tr{display:table-row}.x-panel-reset .x-panel-body thead{display:table-header-group}.x-panel-reset .x-panel-body tbody{display:table-row-group}.x-panel-reset .x-panel-body tfoot{display:table-footer-group}.x-panel-reset .x-panel-body col{display:table-column}.x-panel-reset .x-panel-body colgroup{display:table-column-group}.x-panel-reset .x-panel-body td,.x-panel-reset .x-panel-body th{display:table-cell}.x-panel-reset .x-panel-body caption{display:table-caption}.x-panel-reset .x-panel-body th{font-weight:bolder;text-align:center}.x-panel-reset .x-panel-body caption{text-align:center}.x-panel-reset .x-panel-body body{margin:8px}.x-panel-reset .x-panel-body h1{font-size:2em;margin:.67em 0}.x-panel-reset .x-panel-body h2{font-size:1.5em;margin:.75em 0}.x-panel-reset .x-panel-body h3{font-size:1.17em;margin:.83em 0}.x-panel-reset .x-panel-body h4,.x-panel-reset .x-panel-body p,.x-panel-reset .x-panel-body blockquote,.x-panel-reset .x-panel-body ul,.x-panel-reset .x-panel-body fieldset,.x-panel-reset .x-panel-body form,.x-panel-reset .x-panel-body ol,.x-panel-reset .x-panel-body dl,.x-panel-reset .x-panel-body dir,.x-panel-reset .x-panel-body menu{margin:1.12em 0}.x-panel-reset .x-panel-body h5{font-size:.83em;margin:1.5em 0}.x-panel-reset .x-panel-body h6{font-size:.75em;margin:1.67em 0}.x-panel-reset .x-panel-body h1,.x-panel-reset .x-panel-body h2,.x-panel-reset .x-panel-body h3,.x-panel-reset .x-panel-body h4,.x-panel-reset .x-panel-body h5,.x-panel-reset .x-panel-body h6,.x-panel-reset .x-panel-body b,.x-panel-reset .x-panel-body strong{font-weight:bolder}.x-panel-reset .x-panel-body blockquote{margin-left:40px;margin-right:40px}.x-panel-reset .x-panel-body i,.x-panel-reset .x-panel-body cite,.x-panel-reset .x-panel-body em,.x-panel-reset .x-panel-body var,.x-panel-reset .x-panel-body address{font-style:italic}.x-panel-reset .x-panel-body pre,.x-panel-reset .x-panel-body tt,.x-panel-reset .x-panel-body code,.x-panel-reset .x-panel-body kbd,.x-panel-reset .x-panel-body samp{font-family:monospace}.x-panel-reset .x-panel-body pre{white-space:pre}.x-panel-reset .x-panel-body button,.x-panel-reset .x-panel-body textarea,.x-panel-reset .x-panel-body input,.x-panel-reset .x-panel-body select{display:inline-block}.x-panel-reset .x-panel-body big{font-size:1.17em}.x-panel-reset .x-panel-body small,.x-panel-reset .x-panel-body sub,.x-panel-reset .x-panel-body sup{font-size:.83em}.x-panel-reset .x-panel-body sub{vertical-align:sub}.x-panel-reset .x-panel-body sup{vertical-align:super}.x-panel-reset .x-panel-body table{border-spacing:2px}.x-panel-reset .x-panel-body thead,.x-panel-reset .x-panel-body tbody,.x-panel-reset .x-panel-body tfoot{vertical-align:middle}.x-panel-reset .x-panel-body td,.x-panel-reset .x-panel-body th{vertical-align:inherit}.x-panel-reset .x-panel-body s,.x-panel-reset .x-panel-body strike,.x-panel-reset .x-panel-body del{text-decoration:line-through}.x-panel-reset .x-panel-body hr{border:1px inset}.x-panel-reset .x-panel-body ol,.x-panel-reset .x-panel-body ul,.x-panel-reset .x-panel-body dir,.x-panel-reset .x-panel-body menu,.x-panel-reset .x-panel-body dd{margin-left:40px}.x-panel-reset .x-panel-body ul,.x-panel-reset .x-panel-body menu,.x-panel-reset .x-panel-body dir{list-style-type:disc}.x-panel-reset .x-panel-body ol{list-style-type:decimal}.x-panel-reset .x-panel-body ol ul,.x-panel-reset .x-panel-body ul ol,.x-panel-reset .x-panel-body ul ul,.x-panel-reset .x-panel-body ol ol{margin-top:0;margin-bottom:0}.x-panel-reset .x-panel-body u,.x-panel-reset .x-panel-body ins{text-decoration:underline}.x-panel-reset .x-panel-body br:before{content:"\A"}.x-panel-reset .x-panel-body :before,.x-panel-reset .x-panel-body:after{white-space:pre-line}.x-panel-reset .x-panel-body center{text-align:center}.x-panel-reset .x-panel-body :link,.x-panel-reset .x-panel-body:visited{text-decoration:underline}.x-panel-reset .x-panel-body :focus{outline:invert dotted thin}.x-panel-reset .x-panel-body BDO[DIR="ltr"]{direction:ltr;unicode-bidi:bidi-override}.x-panel-reset .x-panel-body BDO[DIR="rtl"]{direction:rtl;unicode-bidi:bidi-override}.x-window .x-window-handle{opacity:0;filter:alpha(opacity=0)}.x-window-proxy{border:1px solid;z-index:12000;overflow:hidden;position:absolute;left:0;top:0;display:none;opacity:.5;filter:alpha(opacity=50)}.x-window-header{overflow:hidden}.x-window-bwrap{z-index:1;position:relative;left:0;top:0}.x-window-tl .x-window-header{padding:5px 0 4px 0}.x-window-header-text{cursor:pointer}.x-window-tc{background:transparent repeat-x 0 0;overflow:hidden}.x-window-tl{background:transparent no-repeat 0 0;padding-left:6px;z-index:1;position:relative}.x-window-tr{background:transparent no-repeat right 0;padding-right:6px}.x-window-bc{background:transparent repeat-x 0 bottom}.x-window-bc .x-window-footer{padding-bottom:6px;font-size:0;line-height:0}.x-window-bl{background:transparent no-repeat 0 bottom;padding-left:6px}.x-window-br{background:transparent no-repeat right bottom;padding-right:6px}.x-window-mc{border:1px solid;padding:0;margin:0}.x-window-ml{background:transparent repeat-y 0 0;padding-left:6px}.x-window-mr{background:transparent repeat-y right 0;padding-right:6px}.x-window-body{overflow:hidden}.x-window-bwrap{overflow:hidden}.x-window-maximized .x-window-bl,.x-window-maximized .x-window-br,.x-window-maximized .x-window-ml,.x-window-maximized .x-window-mr,.x-window-maximized .x-window-tl,.x-window-maximized .x-window-tr{padding:0}.x-window-maximized .x-window-footer{padding-bottom:0}.x-window-maximized .x-window-tc{padding-left:3px;padding-right:3px}.x-window-maximized .x-window-mc{border-left:0 none;border-right:0 none}.x-window-tbar .x-toolbar,.x-window-bbar .x-toolbar{border-left:0 none;border-right:0 none}.x-window-bbar .x-toolbar{border-top:1px solid;border-bottom:0 none}.x-window-draggable,.x-window-draggable .x-window-header-text{cursor:move}.x-window-maximized .x-window-draggable,.x-window-maximized .x-window-draggable .x-window-header-text{cursor:default}.x-window-body{background-color:transparent}.x-panel-ghost .x-window-tl{border-bottom:1px solid}.x-panel-collapsed .x-window-tl{border-bottom:1px solid}.x-window-maximized-ct{overflow:hidden}.x-window-maximized .x-window-handle{display:none}.x-window-sizing-ghost ul{border:0 none!important}.x-dlg-focus{outline:0 none;width:0;height:0;overflow:hidden;position:absolute;top:0;left:0}.ext-webkit .x-dlg-focus{width:1px;height:1px}.x-dlg-mask{z-index:10000;display:none;position:absolute;top:0;left:0;opacity:.50;filter:alpha(opacity=50)}body.ext-ie6.x-body-masked select{visibility:hidden}body.ext-ie6.x-body-masked .x-window select{visibility:visible}.x-window-plain .x-window-mc{border:1px solid}.x-window-plain .x-window-body{border:1px solid;background:transparent!important}.x-html-editor-wrap{border:1px solid}.x-html-editor-tb .x-btn-text{background:transparent no-repeat}.x-html-editor-tb .x-edit-bold,.x-menu-item img.x-edit-bold{background-position:0 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-italic,.x-menu-item img.x-edit-italic{background-position:-16px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-underline,.x-menu-item img.x-edit-underline{background-position:-32px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-forecolor,.x-menu-item img.x-edit-forecolor{background-position:-160px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-backcolor,.x-menu-item img.x-edit-backcolor{background-position:-176px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-justifyleft,.x-menu-item img.x-edit-justifyleft{background-position:-112px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-justifycenter,.x-menu-item img.x-edit-justifycenter{background-position:-128px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-justifyright,.x-menu-item img.x-edit-justifyright{background-position:-144px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-insertorderedlist,.x-menu-item img.x-edit-insertorderedlist{background-position:-80px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-insertunorderedlist,.x-menu-item img.x-edit-insertunorderedlist{background-position:-96px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-increasefontsize,.x-menu-item img.x-edit-increasefontsize{background-position:-48px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-decreasefontsize,.x-menu-item img.x-edit-decreasefontsize{background-position:-64px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-sourceedit,.x-menu-item img.x-edit-sourceedit{background-position:-192px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tb .x-edit-createlink,.x-menu-item img.x-edit-createlink{background-position:-208px 0;background-image:url(../images/default/editor/tb-sprite.gif)}.x-html-editor-tip .x-tip-bd .x-tip-bd-inner{padding:5px;padding-bottom:1px}.x-html-editor-tb .x-toolbar{position:static!important}.x-panel-noborder .x-panel-body-noborder{border-width:0}.x-panel-noborder .x-panel-header-noborder{border-width:0 0 1px;border-style:solid}.x-panel-noborder .x-panel-tbar-noborder .x-toolbar{border-width:0 0 1px;border-style:solid}.x-panel-noborder .x-panel-bbar-noborder .x-toolbar{border-width:1px 0 0 0;border-style:solid}.x-window-noborder .x-window-mc{border-width:0}.x-window-plain .x-window-body-noborder{border-width:0}.x-tab-panel-noborder .x-tab-panel-body-noborder{border-width:0}.x-tab-panel-noborder .x-tab-panel-header-noborder{border-width:0 0 1px 0}.x-tab-panel-noborder .x-tab-panel-footer-noborder{border-width:1px 0 0 0}.x-tab-panel-bbar-noborder .x-toolbar{border-width:1px 0 0 0;border-style:solid}.x-tab-panel-tbar-noborder .x-toolbar{border-width:0 0 1px;border-style:solid}.x-border-layout-ct{position:relative}.x-border-panel{position:absolute;left:0;top:0}.x-tool-collapse-south{background-position:0 -195px}.x-tool-collapse-south-over{background-position:-15px -195px}.x-tool-collapse-north{background-position:0 -210px}.x-tool-collapse-north-over{background-position:-15px -210px}.x-tool-collapse-west{background-position:0 -180px}.x-tool-collapse-west-over{background-position:-15px -180px}.x-tool-collapse-east{background-position:0 -165px}.x-tool-collapse-east-over{background-position:-15px -165px}.x-tool-expand-south{background-position:0 -210px}.x-tool-expand-south-over{background-position:-15px -210px}.x-tool-expand-north{background-position:0 -195px}.x-tool-expand-north-over{background-position:-15px -195px}.x-tool-expand-west{background-position:0 -165px}.x-tool-expand-west-over{background-position:-15px -165px}.x-tool-expand-east{background-position:0 -180px}.x-tool-expand-east-over{background-position:-15px -180px}.x-tool-expand-north,.x-tool-expand-south{float:right;margin:3px}.x-tool-expand-east,.x-tool-expand-west{float:none;margin:3px 2px}.x-accordion-hd .x-tool-toggle{background-position:0 -255px}.x-accordion-hd .x-tool-toggle-over{background-position:-15px -255px}.x-panel-collapsed .x-accordion-hd .x-tool-toggle{background-position:0 -240px}.x-panel-collapsed .x-accordion-hd .x-tool-toggle-over{background-position:-15px -240px}.x-accordion-hd{padding-top:4px;padding-bottom:3px;border-top:0 none;background:transparent repeat-x 0 -9px}.x-layout-collapsed{position:absolute;left:-10000px;top:-10000px;visibility:hidden;width:20px;height:20px;overflow:hidden;border:1px solid;z-index:20}.ext-border-box .x-layout-collapsed{width:22px;height:22px}.x-layout-collapsed-over{cursor:pointer}.x-layout-collapsed-west .x-layout-collapsed-tools,.x-layout-collapsed-east .x-layout-collapsed-tools{position:absolute;top:0;left:0;width:20px;height:20px}.x-layout-split{position:absolute;height:5px;width:5px;line-height:1px;font-size:1px;z-index:3;background-color:transparent}.ext-strict .ext-ie6 .x-layout-split{background-color:#fff!important;filter:alpha(opacity=1)}.x-layout-split-h{background-image:url(../images/default/s.gif);background-position:left}.x-layout-split-v{background-image:url(../images/default/s.gif);background-position:top}.x-column-layout-ct{overflow:hidden}.x-column{float:left;padding:0;margin:0;overflow:hidden}.x-column-inner{overflow:hidden}.x-layout-mini{position:absolute;top:0;left:0;display:block;width:5px;height:35px;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}.x-layout-mini-over,.x-layout-collapsed-over .x-layout-mini{opacity:1;filter:none}.x-layout-split-west .x-layout-mini{top:48%}.x-layout-split-east .x-layout-mini{top:48%}.x-layout-split-north .x-layout-mini{left:48%;height:5px;width:35px}.x-layout-split-south .x-layout-mini{left:48%;height:5px;width:35px}.x-layout-cmini-west .x-layout-mini{top:48%}.x-layout-cmini-east .x-layout-mini{top:48%}.x-layout-cmini-north .x-layout-mini{left:48%;height:5px;width:35px}.x-layout-cmini-south .x-layout-mini{left:48%;height:5px;width:35px}.x-layout-cmini-west,.x-layout-cmini-east{border:0 none;width:5px!important;padding:0;background-color:transparent}.x-layout-cmini-north,.x-layout-cmini-south{border:0 none;height:5px!important;padding:0;background-color:transparent}.x-viewport,.x-viewport body{margin:0;padding:0;border:0 none;overflow:hidden;height:100%}.x-abs-layout-item{position:absolute;left:0;top:0}.ext-ie input.x-abs-layout-item,.ext-ie textarea.x-abs-layout-item{margin:0}.x-box-layout-ct{overflow:hidden}.x-box-inner{overflow:hidden;position:relative;left:0;top:0}.x-box-item{position:absolute;left:0;top:0}.x-progress-wrap{border:1px solid;overflow:hidden}.x-progress-inner{height:18px;background:repeat-x;position:relative}.x-progress-bar{height:18px;float:left;width:0;background:repeat-x left center;border-top:1px solid;border-bottom:1px solid;border-right:1px solid}.x-progress-text{padding:1px 5px;overflow:hidden;position:absolute;left:0;text-align:center}.x-progress-text-back{line-height:16px}.ext-ie .x-progress-text-back{line-height:15px}.ext-strict .ext-ie7 .x-progress-text-back{width:100%}.x-list-header{background:repeat-x 0 bottom;cursor:default;height:22px}.x-list-header-inner div{display:block;float:left;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap}.x-list-header-inner div em{display:block;border-left:1px solid;padding:4px 4px;overflow:hidden;-moz-user-select:none;-khtml-user-select:none;line-height:14px}.x-list-body{overflow:auto;overflow-x:hidden;overflow-y:auto;float:left;width:100%}.x-list-body dt{display:block;float:left;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.x-list-body dt em{display:block;padding:3px 4px;overflow:hidden;-moz-user-select:none;-khtml-user-select:none}.x-list-resizer{border-left:1px solid;border-right:1px solid;position:absolute;left:0;top:0}.x-list-header-inner em.sort-asc{background:transparent no-repeat center 0;border-style:solid;border-width:0 1px 1px;padding-bottom:3px}.x-list-header-inner em.sort-desc{background:transparent no-repeat center -23px;border-style:solid;border-width:0 1px 1px;padding-bottom:3px}.x-slider-inner{position:relative;left:0;top:0;overflow:visible}.x-slider-focus{position:absolute;left:0;top:0;width:1px;height:1px;line-height:1px;font-size:1px;outline:0 none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:ignore;display:block;overflow:hidden}.x-slider-horz{padding-left:7px;background:transparent no-repeat 0 -22px}.x-slider-horz .x-slider-end{padding-right:7px;background:transparent no-repeat right -44px}.x-slider-horz .x-slider-inner{background:transparent repeat-x 0 0;height:22px}.x-slider-horz .x-slider-thumb{width:14px;height:15px;position:absolute;left:0;top:3px;background:transparent no-repeat 0 0}.x-slider-horz .x-slider-thumb-over{background-position:-14px -15px}.x-slider-horz .x-slider-thumb-drag{background-position:-28px -30px}.x-slider-vert{padding-top:7px;background:transparent no-repeat -44px 0;width:22px}.x-slider-vert .x-slider-end{padding-bottom:7px;background:transparent no-repeat -22px bottom}.x-slider-vert .x-slider-inner{background:transparent repeat-y 0 0}.x-slider-vert .x-slider-thumb{width:15px;height:14px;position:absolute;left:3px;bottom:0;background:transparent no-repeat 0 0}.x-slider-vert .x-slider-thumb-over{background-position:-15px -14px}.x-slider-vert .x-slider-thumb-drag{background-position:-30px -28px}.x-window-dlg .x-window-body{border:0 none!important;padding:5px 10px;overflow:hidden!important}.x-window-dlg .x-window-mc{border:0 none!important}.x-window-dlg .ext-mb-input{margin-top:4px;width:95%}.x-window-dlg .ext-mb-textarea{margin-top:4px}.x-window-dlg .x-progress-wrap{margin-top:4px}.ext-ie .x-window-dlg .x-progress-wrap{margin-top:6px}.x-window-dlg .x-msg-box-wait{background:transparent no-repeat left;display:block;width:300px;padding-left:18px;line-height:18px}.x-window-dlg .ext-mb-icon{float:left;width:47px;height:32px}.x-window-dlg .x-dlg-icon .ext-mb-content{margin-left:47px}.x-window-dlg .ext-mb-info,.x-window-dlg .ext-mb-warning,.x-window-dlg .ext-mb-question,.x-window-dlg .ext-mb-error{background:transparent no-repeat top left}.ext-gecko2 .ext-mb-fix-cursor{overflow:auto} + + \ No newline at end of file diff --git a/src/webui/webui.c b/src/webui/webui.c index 6c1041e7..997b8829 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -39,8 +40,13 @@ #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" +#include "dvb/dvb_support.h" +#include "imagecache.h" +#include "tcp.h" /** * @@ -71,17 +77,28 @@ static int page_root(http_connection_t *hc, const char *remain, void *opaque) { if(is_client_simple(hc)) { - http_redirect(hc, "/simple.html"); + http_redirect(hc, "simple.html"); } else { - http_redirect(hc, "/extjs.html"); + http_redirect(hc, "extjs.html"); } return 0; } +static int +page_root2(http_connection_t *hc, const char *remain, void *opaque) +{ + if (!tvheadend_webroot) return 1; + char *tmp = malloc(strlen(tvheadend_webroot) + 2); + sprintf(tmp, "%s/", tvheadend_webroot); + http_redirect(hc, tmp); + free(tmp); + return 0; +} + /** * Static download of a file from the filesystem */ -static int +int page_static_file(http_connection_t *hc, const char *remain, void *opaque) { int ret = 0; @@ -125,7 +142,7 @@ page_static_file(http_connection_t *hc, const char *remain, void *opaque) ret = 500; break; } - if (write(hc->hc_fd, buf, c) != c) { + if (tvh_write(hc->hc_fd, buf, c)) { ret = 500; break; } @@ -139,28 +156,23 @@ page_static_file(http_connection_t *hc, const char *remain, void *opaque) * HTTP stream loop */ static void -http_stream_run(http_connection_t *hc, streaming_queue_t *sq, - th_subscription_t *s, muxer_container_type_t mc) +http_stream_run(http_connection_t *hc, streaming_queue_t *sq, + const char *name, muxer_container_type_t mc) { streaming_message_t *sm; int run = 1; + int started = 0; muxer_t *mux = NULL; int timeouts = 0; struct timespec ts; struct timeval tp; int err = 0; socklen_t errlen = sizeof(err); - const char *name; - mux = muxer_create(s->ths_service, mc); + mux = muxer_create(mc); if(muxer_open_stream(mux, hc->hc_fd)) run = 0; - if(s->ths_channel) - name = s->ths_channel->ch_name; - else - name = "Live Stream"; - /* reduce timeout on write() for streaming */ tp.tv_sec = 5; tp.tv_usec = 0; @@ -180,11 +192,11 @@ http_stream_run(http_connection_t *hc, streaming_queue_t *sq, //Check socket status getsockopt(hc->hc_fd, SOL_SOCKET, SO_ERROR, (char *)&err, &errlen); if(err) { - tvhlog(LOG_DEBUG, "webui", "Client hung up, exit streaming"); - run = 0; + tvhlog(LOG_DEBUG, "webui", "Stop streaming %s, client hung up", hc->hc_url_orig); + run = 0; }else if(timeouts >= 20) { - tvhlog(LOG_WARNING, "webui", "Timeout waiting for packets"); - run = 0; + tvhlog(LOG_WARNING, "webui", "Stop streaming %s, timeout waiting for packets", hc->hc_url_orig); + run = 0; } } pthread_mutex_unlock(&sq->sq_mutex); @@ -198,49 +210,72 @@ http_stream_run(http_connection_t *hc, streaming_queue_t *sq, switch(sm->sm_type) { case SMT_MPEGTS: case SMT_PACKET: - if(!muxer_write_pkt(mux, sm->sm_data)) - sm->sm_data = NULL; - + if(started) { + muxer_write_pkt(mux, sm->sm_type, sm->sm_data); + sm->sm_data = NULL; + } break; case SMT_START: - tvhlog(LOG_DEBUG, "webui", "Start streaming %s", hc->hc_url_orig); + if(!started) { + tvhlog(LOG_DEBUG, "webui", "Start streaming %s", hc->hc_url_orig); + http_output_content(hc, muxer_mime(mux, sm->sm_data)); - http_output_content(hc, muxer_mime(mux, sm->sm_data)); - muxer_init(mux, sm->sm_data, name); + if(muxer_init(mux, sm->sm_data, name) < 0) + run = 0; + + started = 1; + } else if(muxer_reconfigure(mux, sm->sm_data) < 0) { + tvhlog(LOG_WARNING, "webui", "Unable to reconfigure stream %s", hc->hc_url_orig); + } break; case SMT_STOP: - muxer_close(mux); - run = 0; + if(sm->sm_code != SM_CODE_SOURCE_RECONFIGURED) { + tvhlog(LOG_WARNING, "webui", "Stop streaming %s, %s", hc->hc_url_orig, + streaming_code2txt(sm->sm_code)); + run = 0; + } break; case SMT_SERVICE_STATUS: if(getsockopt(hc->hc_fd, SOL_SOCKET, SO_ERROR, &err, &errlen)) { - tvhlog(LOG_DEBUG, "webui", "Client hung up, exit streaming"); - run = 0; + tvhlog(LOG_DEBUG, "webui", "Stop streaming %s, client hung up", + hc->hc_url_orig); + run = 0; } break; + case SMT_SKIP: + case SMT_SPEED: case SMT_SIGNAL_STATUS: + case SMT_TIMESHIFT_STATUS: break; case SMT_NOSTART: - tvhlog(LOG_DEBUG, "webui", "Couldn't start stream for %s", hc->hc_url_orig); + tvhlog(LOG_WARNING, "webui", "Couldn't start streaming %s, %s", + hc->hc_url_orig, streaming_code2txt(sm->sm_code)); run = 0; break; case SMT_EXIT: - muxer_close(mux); + tvhlog(LOG_WARNING, "webui", "Stop streaming %s, %s", hc->hc_url_orig, + streaming_code2txt(sm->sm_code)); run = 0; break; } + streaming_msg_free(sm); - if(mux->m_errors) + if(mux->m_errors) { + tvhlog(LOG_WARNING, "webui", "Stop streaming %s, muxer reported errors", hc->hc_url_orig); run = 0; + } } + if(started) + muxer_close(mux); + muxer_destroy(mux); } @@ -263,7 +298,7 @@ http_channel_playlist(http_connection_t *hc, channel_t *channel) htsbuf_qprintf(hq, "#EXTM3U\n"); htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", channel->ch_name); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, - access_ticket_create(buf)); + access_ticket_create(buf)); http_output_content(hc, "audio/x-mpegurl"); @@ -290,7 +325,7 @@ http_tag_playlist(http_connection_t *hc, channel_tag_t *tag) snprintf(buf, sizeof(buf), "/stream/channelid/%d", ctm->ctm_channel->ch_id); htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ctm->ctm_channel->ch_name); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, - access_ticket_create(buf)); + access_ticket_create(buf)); } http_output_content(hc, "audio/x-mpegurl"); @@ -321,7 +356,7 @@ http_tag_list_playlist(http_connection_t *hc) snprintf(buf, sizeof(buf), "/playlist/tagid/%d", ct->ct_identifier); htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ct->ct_name); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, - access_ticket_create(buf)); + access_ticket_create(buf)); } http_output_content(hc, "audio/x-mpegurl"); @@ -350,7 +385,7 @@ http_channel_list_playlist(http_connection_t *hc) htsbuf_qprintf(hq, "#EXTINF:-1,%s\n", ch->ch_name); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, - access_ticket_create(buf)); + access_ticket_create(buf)); } http_output_content(hc, "audio/x-mpegurl"); @@ -397,7 +432,7 @@ http_dvr_list_playlist(http_connection_t *hc) snprintf(buf, sizeof(buf), "/dvrfile/%d", de->de_id); htsbuf_qprintf(hq, "http://%s%s?ticket=%s\n", host, buf, - access_ticket_create(buf)); + access_ticket_create(buf)); } http_output_content(hc, "audio/x-mpegurl"); @@ -499,6 +534,8 @@ page_http_playlist(http_connection_t *hc, const char *remain, void *opaque) r = http_tag_list_playlist(hc); else if(!strcmp(components[0], "channels")) r = http_channel_list_playlist(hc); + else if(!strcmp(components[0], "channels.m3u")) + r = http_channel_list_playlist(hc); else if(!strcmp(components[0], "recordings")) r = http_dvr_list_playlist(hc); else { @@ -512,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 */ @@ -526,6 +604,10 @@ http_stream_service(http_connection_t *hc, service_t *service) dvr_config_t *cfg; muxer_container_type_t mc; int flags; + const char *str; + size_t qsize; + const char *name; + char addrbuf[50]; mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux")); if(mc == MC_UNKNOWN) { @@ -533,29 +615,37 @@ http_stream_service(http_connection_t *hc, service_t *service) mc = cfg->dvr_mc; } - if(mc == MC_PASS) { - streaming_queue_init(&sq, SMT_PACKET); + if ((str = http_arg_get(&hc->hc_req_args, "qsize"))) + qsize = atoll(str); + else + qsize = 1500000; + + if(mc == MC_PASS || mc == MC_RAW) { + streaming_queue_init2(&sq, SMT_PACKET, qsize); gh = NULL; tsfix = NULL; st = &sq.sq_st; flags = SUBSCRIPTION_RAW_MPEGTS; } else { - streaming_queue_init(&sq, 0); + streaming_queue_init2(&sq, 0, qsize); gh = globalheaders_create(&sq.sq_st); tsfix = tsfix_create(gh); st = tsfix; flags = 0; } - pthread_mutex_lock(&global_lock); - s = subscription_create_from_service(service, "HTTP", st, flags); - pthread_mutex_unlock(&global_lock); - + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50); + s = subscription_create_from_service(service, "HTTP", st, flags, + addrbuf, + hc->hc_username, + http_arg_get(&hc->hc_args, "User-Agent")); if(s) { - http_stream_run(hc, &sq, s, mc); + name = strdupa(service->s_ch ? + service->s_ch->ch_name : service->s_nicename); + pthread_mutex_unlock(&global_lock); + http_stream_run(hc, &sq, name, mc); pthread_mutex_lock(&global_lock); subscription_unsubscribe(s); - pthread_mutex_unlock(&global_lock); } if(gh) @@ -570,6 +660,37 @@ http_stream_service(http_connection_t *hc, service_t *service) } +/** + * Subscribes to a service and starts the streaming loop + */ +#if ENABLE_LINUXDVB +static int +http_stream_tdmi(http_connection_t *hc, th_dvb_mux_instance_t *tdmi) +{ + th_subscription_t *s; + streaming_queue_t sq; + const char *name; + char addrbuf[50]; + streaming_queue_init(&sq, SMT_PACKET); + + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50); + s = dvb_subscription_create_from_tdmi(tdmi, "HTTP", &sq.sq_st, + addrbuf, + hc->hc_username, + http_arg_get(&hc->hc_args, "User-Agent")); + name = strdupa(tdmi->tdmi_identifier); + pthread_mutex_unlock(&global_lock); + http_stream_run(hc, &sq, name, MC_RAW); + pthread_mutex_lock(&global_lock); + subscription_unsubscribe(s); + + streaming_queue_deinit(&sq); + + return 0; +} +#endif + + /** * Subscribes to a channel and starts the streaming loop */ @@ -581,10 +702,17 @@ 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; muxer_container_type_t mc; + char *str; + size_t qsize; + const char *name; + char addrbuf[50]; mc = muxer_container_txt2type(http_arg_get(&hc->hc_req_args, "mux")); if(mc == MC_UNKNOWN) { @@ -592,33 +720,55 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) mc = cfg->dvr_mc; } - if(mc == MC_PASS) { - streaming_queue_init(&sq, SMT_PACKET); + if ((str = http_arg_get(&hc->hc_req_args, "qsize"))) + qsize = atoll(str); + else + qsize = 1500000; + + if(mc == MC_PASS || mc == MC_RAW) { + streaming_queue_init2(&sq, SMT_PACKET, qsize); gh = NULL; tsfix = NULL; st = &sq.sq_st; flags = SUBSCRIPTION_RAW_MPEGTS; } else { - streaming_queue_init(&sq, 0); + 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; } - pthread_mutex_lock(&global_lock); - s = subscription_create_from_channel(ch, priority, "HTTP", st, flags); - pthread_mutex_unlock(&global_lock); + tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrbuf, 50); + s = subscription_create_from_channel(ch, priority, "HTTP", st, flags, + addrbuf, + hc->hc_username, + http_arg_get(&hc->hc_args, "User-Agent")); if(s) { - http_stream_run(hc, &sq, s, mc); + name = strdupa(ch->ch_name); + pthread_mutex_unlock(&global_lock); + http_stream_run(hc, &sq, name, mc); pthread_mutex_lock(&global_lock); subscription_unsubscribe(s); - pthread_mutex_unlock(&global_lock); } if(gh) globalheaders_destroy(gh); + +#if ENABLE_LIBAV + if(tr) + transcoder_destroy(tr); +#endif + if(tsfix) tsfix_destroy(tsfix); @@ -632,6 +782,7 @@ http_stream_channel(http_connection_t *hc, channel_t *ch) * Handle the http request. http://tvheadend/stream/channelid/ * http://tvheadend/stream/channel/ * http://tvheadend/stream/service/ + * http://tvheadend/stream/mux/ */ static int http_stream(http_connection_t *hc, const char *remain, void *opaque) @@ -639,6 +790,9 @@ http_stream(http_connection_t *hc, const char *remain, void *opaque) char *components[2]; channel_t *ch = NULL; service_t *service = NULL; +#if ENABLE_LINUXDVB + th_dvb_mux_instance_t *tdmi = NULL; +#endif hc->hc_keep_alive = 0; @@ -654,7 +808,7 @@ http_stream(http_connection_t *hc, const char *remain, void *opaque) http_deescape(components[1]); - pthread_mutex_lock(&global_lock); + scopedgloballock(); if(!strcmp(components[0], "channelid")) { ch = channel_find_by_identifier(atoi(components[1])); @@ -662,59 +816,26 @@ http_stream(http_connection_t *hc, const char *remain, void *opaque) ch = channel_find_by_name(components[1], 0, 0); } else if(!strcmp(components[0], "service")) { service = service_find_by_identifier(components[1]); +#if ENABLE_LINUXDVB + } else if(!strcmp(components[0], "mux")) { + tdmi = dvb_mux_find_by_identifier(components[1]); +#endif } - pthread_mutex_unlock(&global_lock); - if(ch != NULL) { return http_stream_channel(hc, ch); } else if(service != NULL) { return http_stream_service(hc, service); +#if ENABLE_LINUXDVB + } else if(tdmi != NULL) { + return http_stream_tdmi(hc, tdmi); +#endif } else { http_error(hc, HTTP_STATUS_BAD_REQUEST); return HTTP_STATUS_BAD_REQUEST; } } - -/** - * Static download of a file from an embedded filebundle - */ -#if 0 -static int -page_static_bundle(http_connection_t *hc, const char *remain, void *opaque) -{ - const struct filebundle *fb = opaque; - const struct filebundle_entry *fbe; - const char *content = NULL, *postfix; - - if(remain == NULL) - return 404; - - postfix = strrchr(remain, '.'); - if(postfix != NULL) { - postfix++; - if(!strcmp(postfix, "js")) - content = "text/javascript; charset=UTF-8"; - } - - for(fbe = fb->entries; fbe->filename != NULL; fbe++) { - if(!strcmp(fbe->filename, remain)) { - - http_send_header(hc, 200, content, fbe->size, - fbe->original_size == -1 ? NULL : "gzip", NULL, 10, 0, - NULL); - /* ignore return value */ - if(write(hc->hc_fd, fbe->data, fbe->size) != fbe->size) - return -1; - return 0; - } - } - return 404; -} -#endif - - /** * Download a recorded file */ @@ -743,7 +864,7 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) } fname = strdup(de->de_filename); - content = muxer_container_mimetype(de->de_mc, 1); + content = muxer_container_type2mime(de->de_mc, 1); postfix = muxer_container_suffix(de->de_mc, 1); pthread_mutex_unlock(&global_lock); @@ -783,18 +904,18 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) content_len = file_end - file_start+1; sprintf(range_buf, "bytes %"PRId64"-%"PRId64"/%"PRId64"", - file_start, file_end, st.st_size); + file_start, file_end, st.st_size); if(file_start > 0) lseek(fd, file_start, SEEK_SET); if(de->de_title != NULL) { snprintf(disposition, sizeof(disposition), - "attachment; filename=%s.%s", lang_str_get(de->de_title, NULL), postfix); + "attachment; filename=%s.%s", lang_str_get(de->de_title, NULL), postfix); i = 20; while(disposition[i]) { if(disposition[i] == ' ') - disposition[i] = '_'; + disposition[i] = '_'; i++; } @@ -803,17 +924,17 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) } http_send_header(hc, range ? HTTP_STATUS_PARTIAL_CONTENT : HTTP_STATUS_OK, - content, content_len, NULL, NULL, 10, - range ? range_buf : NULL, - disposition[0] ? disposition : NULL); + content, content_len, NULL, NULL, 10, + range ? range_buf : NULL, + disposition[0] ? disposition : NULL); if(!hc->hc_no_output) { while(content_len > 0) { chunk = MIN(1024 * 1024 * 1024, content_len); r = sendfile(hc->hc_fd, fd, NULL, chunk); if(r == -1) { - close(fd); - return -1; + close(fd); + return -1; } content_len -= r; } @@ -822,7 +943,47 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) return 0; } +/** + * Fetch image cache image + */ +/** + * Static download of a file from the filesystem + */ +static int +page_imagecache(http_connection_t *hc, const char *remain, void *opaque) +{ + uint32_t id; + int fd; + char buf[8192]; + struct stat st; + ssize_t c; + if(remain == NULL) + return 404; + + if(sscanf(remain, "%d", &id) != 1) + return HTTP_STATUS_BAD_REQUEST; + + if ((fd = imagecache_open(id)) < 0) + return 404; + if (fstat(fd, &st)) { + close(fd); + return 404; + } + + http_send_header(hc, 200, NULL, st.st_size, 0, NULL, 10, 0, NULL); + + while (1) { + c = read(fd, buf, sizeof(buf)); + if (c <= 0) + break; + if (tvh_write(hc->hc_fd, buf, c)) + break; + } + close(fd); + + return 0; +} /** * @@ -830,7 +991,8 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) static void webui_static_content(const char *http_path, const char *source) { - http_path_add(http_path, strdup(source), page_static_file, ACCESS_WEB_INTERFACE); + http_path_add(http_path, strdup(source), page_static_file, + ACCESS_WEB_INTERFACE); } @@ -852,6 +1014,10 @@ int page_statedump(http_connection_t *hc, const char *remain, void *opaque); void webui_init(void) { + if (tvheadend_webui_debug) + tvhlog(LOG_INFO, "webui", "Running web interface in debug mode"); + + http_path_add("", NULL, page_root2, ACCESS_WEB_INTERFACE); http_path_add("/", NULL, page_root, ACCESS_WEB_INTERFACE); http_path_add("/dvrfile", NULL, page_dvrfile, ACCESS_WEB_INTERFACE); @@ -862,6 +1028,8 @@ webui_init(void) http_path_add("/stream", NULL, http_stream, ACCESS_STREAMING); + http_path_add("/imagecache", NULL, page_imagecache, ACCESS_WEB_INTERFACE); + webui_static_content("/static", "src/webui/static"); webui_static_content("/docs", "docs/html"); webui_static_content("/docresources", "docs/docresources"); diff --git a/src/webui/webui.h b/src/webui/webui.h index cbbec569..0d50b554 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -20,6 +20,7 @@ #define WEBUI_H_ #include "htsmsg.h" +#include "http.h" void webui_init(void); @@ -30,6 +31,8 @@ void extjs_start(void); size_t html_escaped_len(const char *src); const char* html_escape(char *dst, const char *src, size_t len); +int page_static_file(http_connection_t *hc, const char *remain, void *opaque); + #if ENABLE_LINUXDVB void extjs_list_dvb_adapters(htsmsg_t *array); void extjs_start_dvb(void); @@ -54,5 +57,4 @@ void comet_mailbox_add_message(htsmsg_t *m, int isdebug); void comet_flush(void); - #endif /* WEBUI_H_ */ diff --git a/src/wrappers.c b/src/wrappers.c index ce38af94..5b08d3ce 100644 --- a/src/wrappers.c +++ b/src/wrappers.c @@ -1,6 +1,7 @@ #include #include /* See NOTES */ #include +#include #include "tvheadend.h" int @@ -29,3 +30,42 @@ tvh_socket(int domain, int type, int protocol) pthread_mutex_unlock(&fork_lock); return fd; } + +int +tvh_pipe(int flags, th_pipe_t *p) +{ + int fd[2], err; + pthread_mutex_lock(&fork_lock); + err = pipe(fd); + if (err != -1) { + fcntl(fd[0], F_SETFD, fcntl(fd[0], F_GETFD) | FD_CLOEXEC); + fcntl(fd[1], F_SETFD, fcntl(fd[1], F_GETFD) | FD_CLOEXEC); + fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | flags); + fcntl(fd[1], F_SETFL, fcntl(fd[1], F_GETFL) | flags); + p->rd = fd[0]; + p->wr = fd[1]; + } + pthread_mutex_unlock(&fork_lock); + return err; +} + +int +tvh_write(int fd, const void *buf, size_t len) +{ + ssize_t c; + + while (len) { + c = write(fd, buf, len); + if (c < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) { + usleep(100); + continue; + } + break; + } + len -= c; + buf += c; + } + + return len ? 1 : 0; +} diff --git a/support/apt-update b/support/apt-update new file mode 100755 index 00000000..32279462 --- /dev/null +++ b/support/apt-update @@ -0,0 +1,80 @@ +#!/bin/bash +# +# Upload packages to launchpad. Note: you must configure dput for tvh-X +# as target PPA, and you should also define DEBFULLNAME and DEBEMAIL +# environment variables +# + +# Terminate +function die +{ + echo >&2 "ERROR: $@" + exit 1 +} + +# CMD +CMD=$(basename "$0") +DIR=$(cd $(dirname "$0"); pwd) + +# Configuration +TVH_ROOT=$(cd "$(dirname "$0")"/..; pwd) +[ -z "$TVH_DIST" ] && TVH_DIST="wheezy lucid oneiric precise quantal raring" +[ -z "$TVH_ARCH" ] && TVH_ARCH="i386 amd64" + +# Options +[ ! -z "$1" ] && REL=$1 || REL=master +[ ! -z "$2" ] && PPA=$2 || PPA=unstable + +# Setup +cd "$TVH_ROOT" || exit 1 +NOW=`date -R` +CHANGELOG=./debian/changelog +VERFILE=./src/version.c + +# Checkout +git checkout $REL || die "could not checkout $REL" + +# Get version +VER=$("./support/version" $VERFILE) + +# Export git tree +TMPDIR=/tmp/$CMD-$$ +trap "rm -rf $TMPDIR" EXIT +mkdir -p "$TMPDIR" +git archive --prefix=tvheadend/ HEAD | tar -C "${TMPDIR}" -x ||\ + die "failed to archive git tree" +cd "$TMPDIR/tvheadend" || die "failed to enter archived tree" + +# Fetch scan files +./support/getmuxlist || die "failed to fetch dvb-scan files" +cd .. + +# For each distro +for d in $TVH_DIST; do + V=${VER}~${d} + mv tvheadend "tvheadend-${V}" + cd "tvheadend-${V}" + + # Create changelog + ./support/changelog "$CHANGELOG" "$d" "$VER" || exit 1 + + # Build source package + dpkg-buildpackage -I.git* -S -sgpg -pgpg || exit 1 + + # Build + if [ "$CMD" == "pbuilder" ]; then + + for a in $TVH_ARCH; do + pbuilder-dist $d $a ../tvheadend_${V}.dsc + done + + # Upload + else + [ ! -f "$HOME/.dput.cf" ] && DPUT_OPT="$DPUT_OPT -c $DIR/dput.cf" + dput $DPUT_OPT tvh-${PPA} ../tvheadend_${V}_source.changes || exit 1 + fi + + # Rename back + cd .. + mv "tvheadend-${V}" tvheadend +done diff --git a/support/changelog b/support/changelog index dcfc8788..33f1c919 100755 --- a/support/changelog +++ b/support/changelog @@ -9,17 +9,18 @@ DIST=$2 VER=$3 # Defaults -[ -z "$DEBEMAIL" ] && DEBEMAIL="andreas@lonelycoder.com" +[ -z "$CHANGELOG" ] && CHANGELOG=$(dirname "$0")/../debian/changelog +[ -z "$DEBEMAIL" ] && DEBEMAIL="andreas@tvheadend.org" [ -z "$DEBFULLNAME" ] && DEBFULLNAME="Andreas Öman" -[ -z "$VER" ] && VER=$($(dirname $0)/version) +[ -z "$VER" ] && VER=$("$(dirname "$0")"/version) [ ! -z "$DIST" ] && VER=${VER}~${DIST} [ -z "$DIST" ] && DIST=unstable # Output NOW=$(date -R) -echo >${CHANGELOG} "tvheadend (${VER}) ${DIST}; urgency=low" -echo >>${CHANGELOG} -echo >>${CHANGELOG} " * The full changelog can be found at " -echo >>${CHANGELOG} " http://www.lonelycoder.com/tvheadend/download" -echo >>${CHANGELOG} -echo >>${CHANGELOG} " -- ${DEBFULLNAME} <${DEBEMAIL}> ${NOW}" +echo >"${CHANGELOG}" "tvheadend (${VER}) ${DIST}; urgency=low" +echo >>"${CHANGELOG}" +echo >>"${CHANGELOG}" " * The full changelog can be found at " +echo >>"${CHANGELOG}" " https://tvheadend.org/projects/tvheadend/wiki/Releases" +echo >>"${CHANGELOG}" +echo >>"${CHANGELOG}" " -- ${DEBFULLNAME} <${DEBEMAIL}> ${NOW}" diff --git a/support/configure.inc b/support/configure.inc old mode 100644 new mode 100755 index db5ad178..9b745c03 --- a/support/configure.inc +++ b/support/configure.inc @@ -9,12 +9,13 @@ # Defaults # ########################################################################### +CONFIGURE_ARGS="$*" + # System setup [ -z "$PLATFORM" ] && PLATFORM=linux [ -z "$CPU" ] && CPU=generic [ -z "$ARCH" ] && ARCH=`uname -m` [ -z "$OSENV" ] && OSENV=posix -[ -z "$CC" ] && CC=cc [ -z "$PYTHON" ] && PYTHON=python # Paths @@ -30,7 +31,7 @@ [ -z "$LDFLAGS" ] && LDFLAGS= # Environment -[ -z "$ROOTDIR" ] && ROOTDIR=$(cd $(dirname $0); pwd) +[ -z "$ROOTDIR" ] && ROOTDIR=$(cd "$(dirname "$0")"; pwd) [ -z "$BUILDDIR" ] && BUILDDIR=$ROOTDIR/build.$PLATFORM [ -z "$TMPDIR" ] && TMPDIR=/tmp @@ -43,7 +44,7 @@ # ########################################################################### # Output -TAB=" \\033[40G" +TAB=" %-50s" # Text conversion function toupper @@ -51,9 +52,10 @@ function toupper echo "$@" | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ } +# Terminate function die { - [ -z "$1" ] || echo "ERROR: $1" + echo >&2 "ERROR: $@" exit 1 } @@ -147,36 +149,38 @@ function disabled_or_auto # Show help function show_help { - local opt= val= + local opt= val= fmt="%-30s" echo "Usage: $0 [options]" echo "" echo "Miscellaneous" - echo -e " --help${TAB}Print this message" + printf " $fmt Print this message\n" "--help" echo "" echo "Installation Paths" - echo -e " --prefix=DIR${TAB}Installation root [$prefix]" - echo -e " --bindir=DIR${TAB}Install binaries in DIR [$bindir]" - echo -e " --libdir=DIR${TAB}Install libraries in DIR [$libdir]" - echo -e " --mandir=DIR${TAB}Install man pages in DIR [$mandir]" - echo -e " --datadir=DIR${TAB}Install data files in DIR [$datadir]" + printf " $fmt Installation root [$prefix]\n" "--prefix=DIR$" + printf " $fmt Install binaries in DIR [$bindir]\n" "--bindir=DIR" + printf " $fmt Install libraries in DIR [$libdir]\n" "--libdir=DIR" + printf " $fmt Install man pages in DIR [$mandir]\n" "--mandir=DIR" + printf " $fmt Install data files in DIR [$datadir]\n" "--datadir=DIR" echo "" echo "Compiler/Arch" - echo -e " --cc=CC${TAB}Build using compile [$CC]" - echo -e " --cpu=CPU${TAB}Build and optimize for specific CPU" - echo -e " --arch=ARCH${TAB}Build for architecture [$ARCH]" - echo -e " --platform=PLATFORM${TAB}Build for platform [$PLATFORM]" - echo -e " --python=PYTHON${TAB}Use python binary [$PYTHON]" + printf " $fmt Build using compiler [$CC]\n" "--cc=CC" + printf " $fmt Build using C flags\n" "--cflags=CFLAGS" + printf " $fmt Build and optimize for specific CPU\n" "--cpu=CPU" + printf " $fmt Build for architecture [$ARCH]\n" "--arch=ARCH" + printf " $fmt Build for platform [$PLATFORM]\n" "--platform=PLATFORM" + printf " $fmt Use python binary [$PYTHON]\n" "--python=PYTHON" echo "" echo "Options" for opt in ${OPTIONS[*]}; do val=${opt#*:} opt=${opt%:*} if [ "$val" == "yes" ]; then - echo -e " --disable-${opt}${TAB}Enable $opt [$val]" + printf " $fmt Disable ${opt}\n" "--disable-${opt}" elif [ "$val" == "no" ]; then - echo -e " --enable-${opt}${TAB}Enable $opt [$val]" + printf " $fmt Enable ${opt}\n" "--enable-${opt}" else - echo -e " --(en|dis)able-${opt}${TAB}Enable $opt [$val]" + printf " $fmt Disable ${opt}\n" "--disable-${opt}" + printf " $fmt Enable ${opt}\n" "--enable-${opt}" fi done exit 0 @@ -197,7 +201,7 @@ function parse_args *dir|prefix) eval "$opt=$val" ;; - cc|arch|cpu|platform|python) + cc|cflags|arch|cpu|platform|python) eval "`toupper $opt`=$val" ;; enable-*) @@ -224,14 +228,14 @@ function check_pkg local ver=$* # Version test - ver=$(echo $ver | sed 's/>=/ --atleast-version /'\ + cver=$(echo $ver | sed 's/>=/ --atleast-version /'\ | sed 's/<=/ --max-version /'\ | sed 's/==/ --exact-version /') - echo -ne "checking for pkg $pkg $ver ...${TAB}" + printf "$TAB" "checking for pkg $pkg $ver ..." # Check for package - if pkg-config $pkg $ver; then + if pkg-config $pkg $cver; then echo "ok" enable_pkg $pkg else @@ -252,10 +256,10 @@ function check_cc cat >$TMPDIR/$$.c < /dev/null + $CC $CFLAGS $LDFLAGS $TMPDIR/$$.c -o $TMPDIR/$$.bin $opt &> /dev/null RET=$? rm -f $TMPDIR/$$.{c,bin} return $RET @@ -268,7 +272,7 @@ function check_cc_header local nam=$2 [ -z "$nam" ] && nam=$hdr - echo -ne "checking for cc $hdr.h ...${TAB}" + printf "$TAB" "checking for cc $hdr.h ..." # Enable if supported if check_cc "#include <$1.h>"; then @@ -287,7 +291,7 @@ function check_cc_snippet local snp=$2 local opt=$3 - echo -ne "checking for cc $nam ...${TAB}" + printf "$TAB" "checking for cc $nam ..." # Check if supported if check_cc "$snp" "$opt"; then @@ -306,7 +310,7 @@ function check_cc_option local nam=$2 [ -z "$nam" ] && nam=$opt - echo -ne "checking for cc -m$opt ...${TAB}" + printf "$TAB" "checking for cc -m$opt ..." # Enable if supported if check_cc "" -m${opt}; then @@ -318,6 +322,25 @@ function check_cc_option fi } +# Check compiler library +function check_cc_lib +{ + local opt=$1 + local nam=$2 + [ -z "$nam" ] && nam=$opt + + printf "$TAB" "checking for cc -l$opt ..." + + # Enable if supported + if check_cc "" -l${opt}; then + echo "ok" + enable $nam + else + echo "fail" + return 1 + fi +} + # ########################################################################### # Python tests # ########################################################################### @@ -342,7 +365,7 @@ function check_py_import local nam=$2 [ -z "$nam" ] && nam=py_${hdr} - echo -ne "checking for py module $hdr ...${TAB}" + printf "$TAB" "checking for py module $hdr ..." # Enable if supported if check_py "import $hdr"; then @@ -363,7 +386,7 @@ function check_bin local bin=$1 local nam=$2 [ -z "$nam" ] && nam=bin_${bin} - echo -ne "checking for $bin ...${TAB}" + printf "$TAB" "checking for $bin ..." if which $bin &> /dev/null; then echo "ok" @@ -381,17 +404,20 @@ function check_bin # Print config function print_config { - local pkg= + local pkg= fmt=" %-40s %s\n" # Compiler settings echo "" echo "Compiler:" - echo -e " Using C compiler:${TAB}${CC}" - echo -e " Build for arch:${TAB}${ARCH}" + printf "$fmt" "Using C compiler:" "${CC}" + if [ "${CFLAGS}" != "" ]; then + printf "$fmt" "Using C flags:" "${CFLAGS}" + fi + printf "$fmt" "Build for arch:" "${ARCH}" echo "" echo "Binaries:" - echo -e " Using PYTHON:${TAB}${PYTHON}" + printf "$fmt" "Using PYTHON:" "${PYTHON}" echo "" # Options @@ -400,9 +426,9 @@ function print_config k=${opt%:*} v=${opt#*:} if [ "$v" == "yes" ]; then - echo -e " $k:${TAB}yes" + printf "$fmt" "$k" "yes" else - echo -e " $k:${TAB}no" + printf "$fmt" "$k" "no" fi done echo "" @@ -410,17 +436,17 @@ function print_config # Packages echo "Packages:" for pkg in ${PACKAGES[*]}; do - echo -e " ${pkg}:${TAB}$(pkg-config --modversion $pkg)" + printf "$fmt" "${pkg}" "$(pkg-config --modversion $pkg)" done echo "" # Installation echo "Installation paths:" - echo -e " Prefix:${TAB}${prefix}" - echo -e " Binaries:${TAB}${bindir}" - echo -e " Libraries:${TAB}${libdir}" - echo -e " Data files:${TAB}${datadir}" - echo -e " Man pages:${TAB}${mandir}" + printf "$fmt" "Prefix:" "${prefix}" + printf "$fmt" "Binaries:" "${bindir}" + printf "$fmt" "Libraries:" "${libdir}" + printf "$fmt" "Data files:" "${datadir}" + printf "$fmt" "Man pages:" "${mandir}" echo "" } @@ -430,17 +456,19 @@ function write_config local pkg= opt= k= v= # Create build directory - mkdir -p ${BUILDDIR} - BUILDDIR=`cd ${BUILDDIR} && pwd` + mkdir -p "${BUILDDIR}" + BUILDDIR=`cd "${BUILDDIR}" && pwd` # Create make include - CONFIG_MK=${ROOTDIR}/.config.mk - cat > ${CONFIG_MK} < "${CONFIG_MK}" < ${CONFIG_H} < "${CONFIG_H}" <>${CONFIG_MK} <>"${CONFIG_MK}" <>${CONFIG_H} <>"${CONFIG_H}" <>${CONFIG_MK} <>"${CONFIG_MK}" <>${CONFIG_H} <>"${CONFIG_H}" <>${CONFIG_MK} <>"${CONFIG_MK}" < -#include -#include - -#include "filebundle.h" - -filebundle_entry_t *filebundle_root = NULL; - -const char *tvheadend_dataroot(void) -{ - static char cwd[256] = { 0 }; - if (!*cwd) { - assert(getcwd(cwd, 254)); - } - return cwd; -} diff --git a/support/dput.cf b/support/dput.cf new file mode 100644 index 00000000..600781aa --- /dev/null +++ b/support/dput.cf @@ -0,0 +1,21 @@ +# +# Tvheadend PPAs +# + +[tvh-unstable] +fqdn = apt.tvheadend.org +method = scp +incoming = /srv/reprepro/unstable/incoming +allow_unsigned_uploads = 0 + +[tvh-beta] +fqdn = apt.tvheadend.org +method = scp +incoming = /srv/reprepro/beta/incoming +allow_unsigned_uploads = 0 + +[tvh-stable] +fqdn = apt.tvheadend.org +method = scp +incoming = /srv/reprepro/stable/incoming +allow_unsigned_uploads = 0 diff --git a/support/epgdump b/support/epgdump index 1c440db5..d0db4a30 100755 --- a/support/epgdump +++ b/support/epgdump @@ -15,7 +15,7 @@ # along with this program. If not, see . # """ -Dump EPG in human readable format for analysis +Dump Electronic Program Guide (EPG) in human readable format for analysis """ # System libs @@ -26,6 +26,12 @@ import pprint sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'lib', 'py')) import tvh.htsmsg as htsmsg +if len(sys.argv) < 2: + sys.exit('Usage: %s epg-path' % sys.argv[0]) + +if not os.path.exists(sys.argv[1]): + sys.exit('ERROR: epg "%s" was not found!' % sys.argv[1]) + # Open file fp = open(sys.argv[1], 'rb') for msg in htsmsg.deserialize(fp, True): diff --git a/support/getmuxlist b/support/getmuxlist index c2b1b7a8..ec39018b 100755 --- a/support/getmuxlist +++ b/support/getmuxlist @@ -1,41 +1,20 @@ #!/bin/bash # -# Retrieve the latest dvb-apps scan files +# Fetch DVB scan files # -URL=http://linuxtv.org/hg/dvb-apps/archive/tip.tar.bz2 -TMP=/tmp/getmuxlist.$$ -TVH=$(cd $(dirname $0)/..; pwd)/data/dvb-scan +# Arguments +DIR=$1 +[ -z "$DIR" ] && DIR=$(dirname "$0")/../data/dvb-scan -function die -{ - [ ! -z "$1" ] && echo $1 || echo - rm -rf $TMP - rm -rf $TVH - exit 1 -} +# Update +if [ -d "${DIR}/.git" ]; then + (cd "${DIR}"; git pull) &> /dev/null -# Get files -rm -rf $TMP -mkdir -p $TMP -cd $TMP -echo -n "fetching scan files ... " -(wget -O - -q $URL | tar xj) 2> /dev/null ||\ -(curl $URL | tar xj) 2> /dev/null -cd dvb-apps* 2> /dev/null || die "failed" -echo "done" +# Fetch +elif [ ! -d "${DIR}" ]; then + URL=http://linuxtv.org/git/dtv-scan-tables.git + git clone $URL "${DIR}" &> /dev/null +fi -# Copy to TVH -echo -n "moving into tvh data/ directory ... " -rm -rf $TVH -mkdir -p $TVH -mv ./util/scan/* $TVH -echo "done" - -# Cleanup -echo -n "cleaning up ... " -for f in $TVH/*; do - [ -f $f ] && rm -f $f -done -rm -rf $TMP -echo "done" +# Note: will not update existing set (if not .git) diff --git a/support/htspmon b/support/htspmon index dd4380c2..787f45c7 100755 --- a/support/htspmon +++ b/support/htspmon @@ -53,6 +53,10 @@ try: msg = htsp.hello() log.info('connected to %s [%s]' % (msg['servername'], msg['serverversion'])) log.info('using protocol v%d' % htsp._version) + cap = [] + if 'servercapability' in msg: + cap = msg['servercapability'] + log.info('capabilities [%s]' % ','.join(cap)) # Authenticate if opts.user: diff --git a/support/launchpad-ppa b/support/launchpad-ppa deleted file mode 100755 index 43f8f453..00000000 --- a/support/launchpad-ppa +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# -# Upload packages to launchpad. Note: you must configure dput for tvh-X -# as target PPA, and you should also define DEBFULLNAME and DEBEMAIL -# environment variables -# - -# CMD -CMD=$(basename $0) -echo $CMD - -# Configuration -TVH_ROOT=$(cd $(dirname $0)/..; pwd) -[ -z "$TVH_DIST" ] && TVH_DIST="hardy lucid natty oneiric precise wheezy" -[ -z "$TVH_ARCH" ] && TVH_ARCH="i386 amd64" - -# Options -[ ! -z "$1" ] && REL=$1 || REL=master -[ ! -z "$2" ] && PPA=$2 || PPA=unstable - -# Setup -cd $TVH_ROOT || exit 1 -git checkout $1 && git checkout . || exit 1 -NOW=`date -R` -CHANGELOG=$TVH_ROOT/debian/changelog -VERFILE=$TVH_ROOT/src/version.c - -# Create version file -VER=$($TVH_ROOT/support/version $VERFILE) - -# Fetch scan files -./support/getmuxlist - -# For each distro -for d in $TVH_DIST; do - V=${VER}~${d} - echo $V - - # Create changelog - $TVH_ROOT/support/changelog "$CHANGELOG" "$d" "$VER" - - # Build source package - dpkg-buildpackage -I.git* -S -sgpg -pgpg || exit 1 - - # Build - if [ "$CMD" == "pbuilder" ]; then - - for a in $TVH_ARCH; do - pbuilder-dist $d $a ../tvheadend_${V}.dsc - done - - # Upload - else - dput tvh-$PPA ../tvheadend_${V}_source.changes || exit 1 - fi - -done - -# Cleanup -git checkout . diff --git a/support/mkbundle b/support/mkbundle index 8818c592..7e32d657 100755 --- a/support/mkbundle +++ b/support/mkbundle @@ -46,11 +46,15 @@ for path in args: t = ents while True: (d,n) = rsplit(n) + if d.startswith('.'): + fs = [] + break if d not in t: t[d] = {} t = t[d] if not n: break for f in fs: + if f.startswith('.'): continue t[f] = None # Output a file diff --git a/support/pbuilder b/support/pbuilder index 5cd9a3a8..cba50a26 120000 --- a/support/pbuilder +++ b/support/pbuilder @@ -1 +1 @@ -launchpad-ppa \ No newline at end of file +apt-update \ No newline at end of file diff --git a/support/posix.mk b/support/posix.mk index 62819206..03e772da 100644 --- a/support/posix.mk +++ b/support/posix.mk @@ -1,5 +1,5 @@ -MAN = man/tvheadend.1 -ICON = support/gnome/tvheadend.svg +MAN = $(ROOTDIR)/man/tvheadend.1 +ICON = $(ROOTDIR)/support/gnome/tvheadend.svg INSTICON= ${DESTDIR}$(prefix)/share/icons/hicolor/scalable/apps @@ -10,10 +10,12 @@ install: ${PROG} ${MAN} for bundle in ${BUNDLES}; do \ mkdir -p ${DESTDIR}${datadir}/tvheadend/$$bundle ;\ - cp -r $$bundle/* ${DESTDIR}${datadir}/tvheadend/$$bundle ;\ + cp -r $(ROOTDIR)/$$bundle/* ${DESTDIR}${datadir}/tvheadend/$$bundle ;\ done + find ${DESTDIR}${datadir}/tvheadend -name .git -exec rm -rf {} \; &>/dev/null || /bin/true uninstall: - rm -f ${DESTDIR}${bindir)/tvheadend - rm -f ${DESTDIR}${mandir)/tvheadend.1 + rm -f ${DESTDIR}${bindir}/tvheadend + rm -f ${DESTDIR}${mandir}/tvheadend.1 + rm -rf ${DESTDIR}${datadir}/tvheadend diff --git a/support/tarball b/support/tarball new file mode 100755 index 00000000..3c3c0886 --- /dev/null +++ b/support/tarball @@ -0,0 +1,53 @@ +#!/bin/bash +# +# Build tarball of the current directory +# + +# Terminate +function die +{ + echo >&2 "ERROR: $@" + exit 1 +} + +# Switch dir +SRCDIR=$(dirname "$0")/.. +cd "$SRCDIR" + +# Arguments +REL=$1 + +# Checkout +if [ ! -z "$REL" ]; then + git checkout $REL || die "could not checkout $REL" +fi +git clean -dfx || die "could not clean git tree" + +# Version +VER=$(./support/version) +echo $VER | grep -q dirty && die "git tree is not clean" +VER1=$(echo $VER | sed 's/~.*//') +echo $VER1 + +# Temp directory +TMPDIR=/tmp/tvhtar-$$ +mkdir -p $TMPDIR +trap "rm -rf $TMPDIR" EXIT + +# Copy +DSTDIR=$TMPDIR/tvheadend-$VER1 +mkdir $DSTDIR +git archive HEAD | tar -x -C $DSTDIR + +# Remove stuff we don't need +rm -rf $DSTDIR/.gitignore + +# Fix changelog (store version) +$DSTDIR/support/changelog $DSTDIR/debian/changelog "" $VER + +# Build tarball +TARFILE=$(cd "$SRCDIR"/..; pwd)/tvheadend-$VER1.tar.gz +tar -C $TMPDIR -zcf "$TARFILE" tvheadend-$VER1 + +# Done +echo "Created $TARFILE" diff --git a/support/version b/support/version index 8f74a65b..ba9bfedb 100755 --- a/support/version +++ b/support/version @@ -8,9 +8,17 @@ FILE=$1 # Calculate version if [ -d ".git" ]; then - VER=$(cd $(dirname $0); git describe --dirty --match "v*" 2> /dev/null | sed "s/^v//" | sed "s/-\([0-9]*\)-\(g[0-9a-f]*\)/.\1~\2/") + VER=$(cd "$(dirname "$0")"/..; git describe --dirty --match "v*" 2> /dev/null) + if [ $? -ne 0 ]; then + # Git describe failed, maybe "--dirty" option is not available + # Adding "-unknown" postfix to mark this situation + VER=$(cd "$(dirname "$0")/.."; git describe --match "v*" 2> /dev/null)-unknown + fi + VER=$(echo $VER | sed "s/^v//" | sed "s/-\([0-9]*\)-\(g[0-9a-f]*\)/.\1~\2/") +elif [ -f "$(dirname "$0")/../debian/changelog" ]; then + VER=$(head -1 "$(dirname "$0")/../debian/changelog" | awk '{ print $2 }' | tr -d '()' | cut -d '-' -f 1) else - VER=$(head -1 $(dirname $0)/../debian/changelog | awk '{ print $2 }' | tr -d '()' | cut -d '-' -f 1) + VER="0.0.0~unknown" fi # Output @@ -21,14 +29,14 @@ fi # Leave (probably ppa build) if [ -z "$VER" -a -s "$FILE" ]; then - cat $FILE + cat "$FILE" exit fi # Update? NEW_VER="const char *tvheadend_version = \"$VER\";" -OLD_VER=$(cat $FILE 2> /dev/null) +OLD_VER=$(cat "$FILE" 2> /dev/null) if [ "$NEW_VER" != "$OLD_VER" ]; then - echo $NEW_VER > $FILE + echo $NEW_VER > "$FILE" fi echo $VER