diff options
-rw-r--r-- | COPYING | 340 | ||||
-rw-r--r-- | HISTORY | 6 | ||||
-rw-r--r-- | Makefile | 107 | ||||
-rw-r--r-- | README | 90 | ||||
-rw-r--r-- | VDR.patch | 250 | ||||
-rw-r--r-- | siinfo.c | 583 | ||||
-rw-r--r-- | siinfo.h | 36 | ||||
-rw-r--r-- | teletext-chars.h | 80 | ||||
-rw-r--r-- | teletext-tables.h | 81 | ||||
-rw-r--r-- | teletext.c | 724 | ||||
-rw-r--r-- | teletext.h | 101 | ||||
-rw-r--r-- | ttxtsubs.c | 357 | ||||
-rw-r--r-- | ttxtsubs.h | 1 | ||||
-rw-r--r-- | ttxtsubsdisplay.c | 331 | ||||
-rw-r--r-- | ttxtsubsdisplay.h | 35 | ||||
-rw-r--r-- | ttxtsubsdisplayer.c | 256 | ||||
-rw-r--r-- | ttxtsubsdisplayer.h | 47 | ||||
-rw-r--r-- | ttxtsubsfilter.c | 158 | ||||
-rw-r--r-- | ttxtsubsfilter.h | 27 | ||||
-rw-r--r-- | ttxtsubsreceiver.c | 253 | ||||
-rw-r--r-- | ttxtsubsreceiver.h | 34 | ||||
-rw-r--r-- | ttxtsubsrecorder.c | 167 | ||||
-rw-r--r-- | ttxtsubsrecorder.h | 23 | ||||
-rw-r--r-- | utils.h | 2 | ||||
-rw-r--r-- | vdrttxtsubshooks.c | 44 | ||||
-rw-r--r-- | vdrttxtsubshooks.h | 36 |
26 files changed, 4169 insertions, 0 deletions
@@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 2 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, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. @@ -0,0 +1,6 @@ +VDR Plugin 'ttxtsubs' Revision History +-------------------------------------- + +2003-03-07: Version 0.0.1 + +- Initial revision. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9f9c5fa --- /dev/null +++ b/Makefile @@ -0,0 +1,107 @@ +# +# Makefile for a Video Disk Recorder plugin +# +# $Id: Makefile,v 1.16 2003/03/07 03:01:17 ragge Exp $ + +# The official name of this plugin. +# This name will be used in the '-P...' option of VDR to load the plugin. +# By default the main source file also carries this name. +# +PLUGIN = ttxtsubs + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'static const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$6 }' | sed -e 's/[";]//g') + +### The C++ compiler and options: + +CXX ?= g++ +CXXFLAGS ?= -O2 -Wall -Woverloaded-virtual + +### The directory environment: + +DVBDIR = ../../../../DVB +VDRDIR = ../../.. +LIBDIR = ../../lib +TMPDIR = /tmp + +### Allow user defined options to overwrite defaults: + +-include $(VDRDIR)/Make.config + +### The version number of VDR (taken from VDR's "config.h"): + +VDRVERSION = $(shell grep 'define VDRVERSION ' $(VDRDIR)/config.h | awk '{ print $$3 }' | sed -e 's/"//g') + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) +BUPPACKAGE = vdr-$(ARCHIVE)-bup-$(shell date +%y%m%d-%H%M%S) + +### Includes and Defines (add further entries here): + +INCLUDES += -I$(VDRDIR)/include -I$(DVBDIR)/include + +DEFINES += -DPLUGIN_NAME_I18N='"$(PLUGIN)"' + +### The object files (add further files here): + +OBJS = $(PLUGIN).o ttxtsubsdisplayer.o ttxtsubsdisplay.o teletext.o siinfo.o \ + ttxtsubsfilter.o ttxtsubsrecorder.o ttxtsubsreceiver.o + +SOURCEFILES = *.c *.h [A-Z]???* + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) $< + +# Dependencies: + +MAKEDEP = g++ -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Targets: + +all: libvdr-$(PLUGIN).so + +libvdr-$(PLUGIN).so: $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) -o $@ + @cp $@ $(LIBDIR)/$@.$(VDRVERSION) + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a $(SOURCEFILES) $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +bup: clean patch + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a $(SOURCEFILES) RCS $(TMPDIR)/$(ARCHIVE) + @tar czf $(BUPPACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(BUPPACKAGE).tgz + @echo making backup... + @sh RCS/backup.sh $(BUPPACKAGE).tgz + @echo done. + +clean: + @-rm -f $(OBJS) $(DEPFILE) *.so *.tgz core* *~ + +ci: patch + ci -u $(SOURCEFILES) + +patch: + co -l xVDR.patch.temp vdrttxtsubshooks.c vdrttxtsubshooks.h + (cd ../../../; diff -upr ./DIST/* .) | grep -v "Only in ." > xVDR.patch.temp + cp -p ../../../vdrttxtsubshooks.c . + cp -p ../../../vdrttxtsubshooks.h . + ci -u xVDR.patch.temp vdrttxtsubshooks.c vdrttxtsubshooks.h @@ -0,0 +1,90 @@ +This is a "plugin" for the Video Disk Recorder (VDR). + +Written by: Ragnar Sundblad <ragge@nada.kth.se> + +Project's homepage: - + +Latest version available at: ftp://ftp.nada.kth.se/pub/home/ragge/vdr/ + +See the file COPYING for license information. + + +Description: + +This plugin implements displaying, recording and replaying teletext +based subtitles using the on screen display. + + +Long Description: + +The teletext decoder/displayer is currently very basic and limited. It +can't show colours and it doesn't take into account the editors +attempts to place the text on different places on the screen. + +The DVB drivers from the Metzlers as of late February 2003 and +Convergence/linuxtv.org as of March 5 2003 have firmware fixes against +OSD problems. (Thanks a lot guys!) An upgrade to either of those is +strongly recommended. If the plugin is used with earlier firmware, +data may get lost when the OSD is updated possibly resulting in bad +video and audio, especially when recording or playing recordings. + +Both the live displayer and the recorder finds subtitling PID and page +for the selected language, and optionally for the hearing impaired, by +scanning the DVB Service Information. You don't have to set up channel +Tpids. Though, if you have set a Sid it will be used to speed up +Service Information scanning. (Can't say it makes much of a +difference, though). + +The recorder writes a filtered subset of the teletext data with only +the subtitle pages and an index page at page 100 (or subsequent page +if that page number is used for subtitling). The filtered stream is +written as a private_stream_1 with subtype 0x1f (the last teletext +subtype). An ordinary teletext PES stream recorded for example by +setting Dpid1 to a teletext PID will typically have a subtype of +0x10. + +The recording player scans the index page to find the selected +language. (If it can't find an index page it will use the page number +from the current channel. This may be removed in the future.) The +index page itself can't be viewed with this plugin. + +If you can't select your language, look up its 3 letter acronym in +<http://www.loc.gov/standards/iso639-2/englangn_ascii.html> and/or +check the output from ttxtsubs, and enter the language code in your +VDR setup.conf at ttxtsubs.language. This will hopefully be easier in +a future release. + +Consider this code alpha quality. The code could use some major +cleanup. The program design was from the beginning just not, as it +started as a test hack, and it has from then been incompletely +reworked a few times. + + +Installation: + +This plugin is written for VDR 1.1.25. It currently needs a few hooks +in, and changes to, VDR to work. A patch file is included. The patches +should be harmless if you don't have the plugin loaded, so even if you +patch VDR and later decide not to use this plugin you don't +necessarily have to reinstall VDR. It is probably possible to build +the plugin for other version of VDR, but don't count on it. + +Steps: + +As usual, unpack the plugin and make a link: + cd ..../vdr-1.1.25/PLUGINS/src + tar zxvf ..../vdr-ttxtsubs-0.0.1.tgz + ln -s ttxtsubs-0.0.1 ttxtsubs + +Go back to your VDR directory and install the patches and the new files: + cd ../../ + cp PLUGINS/src/ttxtsubs-0.0.1/vdrttxtsubshooks.[ch] . + patch -b < PLUGINS/src/ttxtsubs-0.0.1/VDR.patch + +Rebuild VDR: + make clean + make include-dir + make + make plugins + +Now you should be able to start VDR with "-P ttxtsubs". diff --git a/VDR.patch b/VDR.patch new file mode 100644 index 0000000..3c825c3 --- /dev/null +++ b/VDR.patch @@ -0,0 +1,250 @@ +diff -upr ./DIST/vdr-1.1.25/Makefile ./Makefile +--- ./DIST/vdr-1.1.25/Makefile 2003-01-06 13:28:09.000000000 +0100 ++++ ./Makefile 2003-03-07 03:19:33.000000000 +0100 +@@ -36,7 +36,8 @@ OBJS = audio.o channels.o ci.o config.o + dvbplayer.o dvbspu.o eit.o eitscan.o font.o i18n.o interface.o keys.o\ + lirc.o menu.o menuitems.o osdbase.o osd.o player.o plugin.o rcu.o\ + receiver.o recorder.o recording.o remote.o remux.o ringbuffer.o sources.o\ +- spu.o status.o svdrp.o thread.o timers.o tools.o transfer.o vdr.o videodir.o ++ spu.o status.o svdrp.o thread.o timers.o tools.o transfer.o vdr.o videodir.o\ ++ vdrttxtsubshooks.o + + OSDFONT = -adobe-helvetica-medium-r-normal--23-*-100-100-p-*-iso8859-1 + FIXFONT = -adobe-courier-bold-r-normal--25-*-100-100-m-*-iso8859-1 +diff -upr ./DIST/vdr-1.1.25/dvbplayer.c ./dvbplayer.c +--- ./DIST/vdr-1.1.25/dvbplayer.c 2003-02-15 11:38:59.000000000 +0100 ++++ ./dvbplayer.c 2003-03-07 03:52:55.000000000 +0100 +@@ -13,6 +13,7 @@ + #include "ringbuffer.h" + #include "thread.h" + #include "tools.h" ++#include "vdrttxtsubshooks.h" + + // --- cBackTrace ---------------------------------------------------------- + +@@ -318,6 +319,12 @@ void cDvbPlayer::StripAudioPackets(uchar + int l = b[i + 4] * 256 + b[i + 5] + 6; + switch (c) { + case 0xBD: // dolby ++#ifdef VDRTTXTSUBSHOOKS ++ if (b[i + 8] == 0x24 && b[i + 45] >= 0x10 && b[i + 45] < 0x20) { ++ break; // run these through the ring buffer to get somewhat correct ++ // timing for the subtitles ++ } else ++#endif + if (Except) + PlayAudio(&b[i], l); + // continue with deleting the data - otherwise it disturbs DVB replay +@@ -347,6 +354,40 @@ void cDvbPlayer::StripAudioPackets(uchar + } + } + ++#ifdef VDRTTXTSUBSHOOKS ++static void StripTtxtPackets(uchar *b, int Length) ++{ ++ for (int i = 0; i < Length - 6; i++) { ++ if (b[i] == 0x00 && b[i + 1] == 0x00 && b[i + 2] == 0x01) { ++ uchar c = b[i + 3]; ++ int l = b[i + 4] * 256 + b[i + 5] + 6; ++ switch (c) { ++ case 0xBD: // dolby ++ { ++ if (b[i + 8] == 0x24 && b[i + 45] >= 0x10 && b[i + 45] < 0x20) { ++ // EBU Teletext data, ETSI EN 300 472 ++ cVDRTtxtsubsHookListener::Hook()->PlayerTeletextData(&b[i], l); ++ } ++ // continue with deleting the data - otherwise it disturbs DVB replay ++ int n = l; ++ for (int j = i; j < Length && n--; j++) ++ b[j] = 0x00; ++ break; ++ } ++ default: ++ break; ++ } ++ if (l) ++ i += l - 1; // the loop increments, too! ++ } ++ /*XXX ++ else ++ esyslog("ERROR: broken packet header"); ++ XXX*/ ++ } ++} ++#endif ++ + bool cDvbPlayer::NextFile(uchar FileNumber, int FileOffset) + { + if (FileNumber > 0) +@@ -514,6 +555,11 @@ void cDvbPlayer::Action(void) + p = playFrame->Data(); + pc = playFrame->Count(); + } ++#ifdef VDRTTXTSUBSHOOKS ++ // pick out the teletext packets here ++ if(p) ++ StripTtxtPackets((uchar *) p, pc); ++#endif + if (p) { + int w = PlayVideo(p, pc); + if (w > 0) { +diff -upr ./DIST/vdr-1.1.25/menu.c ./menu.c +--- ./DIST/vdr-1.1.25/menu.c 2003-02-09 13:55:38.000000000 +0100 ++++ ./menu.c 2003-03-07 03:38:14.000000000 +0100 +@@ -2931,8 +2931,18 @@ cRecordControl::cRecordControl(cDevice * + fileName = strdup(Recording.FileName()); + cRecordingUserCommand::InvokeCommand(RUC_BEFORERECORDING, fileName); + const cChannel *ch = timer->Channel(); ++#ifdef VDRTTXTSUBSHOOKS ++ cTtxtSubsRecorderBase *subsRecorder = cVDRTtxtsubsHookListener::Hook() ++ ->NewTtxtSubsRecorder(device, ch); ++ recorder = new cRecorder(fileName, ch->Ca(), timer->Priority(), ch->Vpid(), ch->Apid1(), ch->Apid2(), ch->Dpid1(), ch->Dpid2(), subsRecorder); ++#else + recorder = new cRecorder(fileName, ch->Ca(), timer->Priority(), ch->Vpid(), ch->Apid1(), ch->Apid2(), ch->Dpid1(), ch->Dpid2()); ++#endif + if (device->AttachReceiver(recorder)) { ++#ifdef VDRTTXTSUBSHOOKS ++ if(subsRecorder) ++ subsRecorder->DeviceAttach(); ++#endif + Recording.WriteSummary(); + cStatus::MsgRecording(device, Recording.Name()); + } +diff -upr ./DIST/vdr-1.1.25/menu.h ./menu.h +--- ./DIST/vdr-1.1.25/menu.h 2003-01-12 15:54:05.000000000 +0100 ++++ ./menu.h 2003-03-07 03:33:28.000000000 +0100 +@@ -14,6 +14,7 @@ + #include "device.h" + #include "osd.h" + #include "dvbplayer.h" ++#include "vdrttxtsubshooks.h" + #include "recorder.h" + #include "recording.h" + +diff -upr ./DIST/vdr-1.1.25/osd.c ./osd.c +--- ./DIST/vdr-1.1.25/osd.c 2002-12-08 14:17:13.000000000 +0100 ++++ ./osd.c 2003-03-07 03:55:32.000000000 +0100 +@@ -12,6 +12,7 @@ + #include "device.h" + #include "i18n.h" + #include "status.h" ++#include "vdrttxtsubshooks.h" + + // --- cOsd ------------------------------------------------------------------ + +@@ -96,6 +97,9 @@ void cOsd::Open(int w, int h) + d *= lineHeight; + int x = (720 - w + charWidth) / 2; //TODO PAL vs. NTSC??? + int y = (576 - Setup.OSDheight * lineHeight) / 2 + d; ++#ifdef VDRTTXTSUBSHOOKS ++ cVDRTtxtsubsHookListener::Hook()->HideOSD(); ++#endif + //XXX + osd = OpenRaw(x, y); + //XXX TODO this should be transferred to the places where the individual windows are requested (there's too much detailed knowledge here!) +@@ -136,6 +140,9 @@ void cOsd::Close(void) + delete osd; + osd = NULL; + #endif ++#ifdef VDRTTXTSUBSHOOKS ++ cVDRTtxtsubsHookListener::Hook()->ShowOSD(); ++#endif + } + + void cOsd::Clear(void) +diff -upr ./DIST/vdr-1.1.25/recorder.c ./recorder.c +--- ./DIST/vdr-1.1.25/recorder.c 2003-01-25 17:23:36.000000000 +0100 ++++ ./recorder.c 2003-03-07 03:54:10.000000000 +0100 +@@ -10,6 +10,8 @@ + #include <stdarg.h> + #include <stdio.h> + #include <unistd.h> ++#include <stdint.h> ++#include "vdrttxtsubshooks.h" + #include "recorder.h" + + // The size of the array used to buffer video data: +@@ -19,7 +21,11 @@ + #define MINFREEDISKSPACE (512) // MB + #define DISKCHECKINTERVAL 100 // seconds + ++#ifdef VDRTTXTSUBSHOOKS ++cRecorder::cRecorder(const char *FileName, int Ca, int Priority, int VPid, int APid1, int APid2, int DPid1, int DPid2, cTtxtSubsRecorderBase *tsr) ++#else + cRecorder::cRecorder(const char *FileName, int Ca, int Priority, int VPid, int APid1, int APid2, int DPid1, int DPid2) ++#endif + :cReceiver(Ca, Priority, 5, VPid, APid1, APid2, DPid1, DPid2) + { + ringBuffer = NULL; +@@ -30,6 +36,9 @@ cRecorder::cRecorder(const char *FileNam + fileSize = 0; + active = false; + lastDiskSpaceCheck = time(NULL); ++#ifdef VDRTTXTSUBSHOOKS ++ ttxtSubsRecorder = tsr; ++#endif + isyslog("record %s", FileName); + + // Create directories if necessary: +@@ -57,6 +66,10 @@ cRecorder::cRecorder(const char *FileNam + cRecorder::~cRecorder() + { + Detach(); ++#ifdef VDRTTXTSUBSHOOKS ++ if(ttxtSubsRecorder) ++ delete ttxtSubsRecorder; ++#endif + delete index; + delete fileName; + delete remux; +@@ -130,6 +143,19 @@ void cRecorder::Action(void) + break; + } + fileSize += Result; ++#ifdef VDRTTXTSUBSHOOKS ++ // not sure if the pictureType test is needed, but it seems we can get ++ // incomplete pes packets from remux if we are not getting pictures? ++ if (ttxtSubsRecorder && pictureType != NO_PICTURE) { ++ uint8_t *subsp; ++ size_t len; ++ if(ttxtSubsRecorder->GetPacket(&subsp, &len)) { ++ safe_write(recordFile, subsp, len); ++ fileSize += len; ++ // fprintf(stderr, "cRecorder::Action: Wrote ttxtsubs data len %d\n", len); // XXX ++ } ++ } ++#endif + } + else + break; +diff -upr ./DIST/vdr-1.1.25/recorder.h ./recorder.h +--- ./DIST/vdr-1.1.25/recorder.h 2002-06-08 11:35:03.000000000 +0200 ++++ ./recorder.h 2003-03-07 03:50:33.000000000 +0100 +@@ -15,6 +15,7 @@ + #include "remux.h" + #include "ringbuffer.h" + #include "thread.h" ++#include "vdrttxtsubshooks.h" + + class cRecorder : public cReceiver, cThread { + private: +@@ -29,12 +30,19 @@ private: + time_t lastDiskSpaceCheck; + bool RunningLowOnDiskSpace(void); + bool NextFile(void); ++#ifdef VDRTTXTSUBSHOOKS ++ cTtxtSubsRecorderBase *ttxtSubsRecorder; ++#endif + protected: + virtual void Activate(bool On); + virtual void Receive(uchar *Data, int Length); + virtual void Action(void); + public: ++#ifdef VDRTTXTSUBSHOOKS ++ cRecorder(const char *FileName, int Ca, int Priority, int VPid, int APid1, int APid2, int DPid1, int DPid2, cTtxtSubsRecorderBase *tsr); ++#else + cRecorder(const char *FileName, int Ca, int Priority, int VPid, int APid1, int APid2, int DPid1, int DPid2); ++#endif + // Creates a new recorder that requires conditional access Ca, has + // the given Priority and will record the given PIDs into the file FileName. + virtual ~cRecorder(); diff --git a/siinfo.c b/siinfo.c new file mode 100644 index 0000000..f878be7 --- /dev/null +++ b/siinfo.c @@ -0,0 +1,583 @@ +#include <sys/ioctl.h> +#include <stdio.h> +#include <stdint.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> +#include <sys/poll.h> +#include <time.h> +#include <netinet/in.h> +#include <ctype.h> +#include <stdlib.h> +#include <string.h> + +#include "linux/dvb/dmx.h" +#include "siinfo.h" + +#define DESCR_TELETEXT 0x56 +#define DESCR_DVBSUBTITLES 0x59 + + +#define PACK __attribute__ ((__packed__)) + + +// ETSI 300468 6.2.40 +struct ttxt_descr { + uint8_t tag PACK; + uint8_t length PACK; + struct { + uint8_t lang[3] PACK; + uint8_t type_mag PACK; // 5 bits type, 3 bits mag + uint8_t page_no PACK; + } d[1] PACK; +}; + +struct sect_header { + uint8_t table_id PACK; + uint16_t syntax_len PACK; + uint16_t transport_stream_id PACK; + uint8_t ver_cur PACK; + uint8_t section_number PACK; + uint8_t last_section_number PACK; +}; + +// H.222.0 2.4.4.3, Table 2-25 +struct PAT_sect { + uint8_t table_id PACK; + uint16_t syntax_len PACK; + uint16_t transport_stream_id PACK; + uint8_t vers_curr PACK; + uint8_t sect_no PACK; + uint8_t last_sect PACK; + struct { + uint16_t program_number PACK; + uint16_t res_PMTPID PACK; + } d[1] PACK; +}; + +// H.222.0 2.4.4.8, Table 2-28 +struct PMT_stream { + uint8_t stream_type PACK; + uint16_t res_PID PACK; + uint16_t res_ES_info_len PACK; + uint8_t descrs[2] PACK; +}; + +struct PMT_sect { + uint8_t table_id PACK; + uint16_t syntax_len PACK; + uint16_t program_number PACK; + uint8_t vers_curr PACK; + uint8_t sect_no PACK; + uint8_t last_sect PACK; + uint16_t res_pcr PACK; + uint16_t res_program_info_length PACK; + struct PMT_stream s PACK; +}; + + +void dump_hex(char *msg, uint8_t *p, int len) +{ + int i; + printf("%s:", msg); + for(i = 0; i < len; i++) { + printf(" %02x", p[i]); + } + printf("\n"); + + printf("%s:", msg); + for(i = 0; i < len; i++) { + printf(" %c", isprint(p[i]) ? p[i] : '.'); + } + printf("\n"); +} + + +static int SetSectFilt(int fd, uint16_t pid, uint8_t tnr, uint8_t mask) +{ + int ret; + + struct dmx_sct_filter_params p; + + memset(&p, 0, sizeof(p)); + + p.filter.filter[0] = tnr; + p.filter.mask[0] = mask; + p.pid = pid; + p.flags = DMX_IMMEDIATE_START | DMX_CHECK_CRC; + + if ((ret = ioctl(fd, DMX_SET_FILTER, &p)) < 0) + perror("DMX SET FILTER:"); + + return ret; +} + + +/* + * PID - pid to collect on + * table_id - table id to filter out (H.222.0 table 2-26) + * sects - pointer to char *[256], points to array of table sections, initially empty + * numsects - number of sections in table + * returns - 0 if everything ok + */ +#define SECTSIZE 1024 +static int CollectSections(int card_no, int pid, int table_id, char **sects, int *numsects) +{ + int fd; + int ret = -1; + int last_section = 0; + int done = 0; + char *p = NULL; + int n; + char name[100]; + + snprintf(name, sizeof(name), "/dev/dvb/adapter%d/demux0", card_no); + + memset(sects, 0, sizeof(char*) * 256); + + if((fd = open(name, O_RDWR)) < 0){ + perror("DEMUX DEVICE 1: "); + return -1; + } + + if(SetSectFilt(fd, pid, table_id, 0xff)) { + ret = -1; + goto bail; + } + + do { + struct sect_header *h; + int i; + + if(p == NULL) + p = (char *) malloc(SECTSIZE); + + n = read(fd, p, SECTSIZE); + if(n < 8) + continue; + + h = (struct sect_header *) p; + + if(n != ((ntohs(h->syntax_len) & 0xfff) + 3)) { + fprintf(stderr, "bad section length: %x / %x!\n", n, ntohs(h->syntax_len) & 0xfff); + continue; + } + + if(!(h->ver_cur & 0x01)) // if not current + continue; + + last_section = h->last_section_number; + + if(!sects[h->section_number]) { // section_number + sects[h->section_number] = p; + p = NULL; + } + + for(i = 0; i <= last_section; i++) { + if(!sects[i]) { + break; + } + + if(i == last_section) { + *numsects = last_section + 1; + ret = 0; + done = 1; + } + } + + } while (!done); + + bail: + close(fd); + return ret; +} + +static void FreeSects(char **sects) +{ + int i; + + for(i = 0; i < 256; i++) { + if(sects[i]) + free(sects[i]); + } +} + + +static void addpageinfo(struct ttxtinfo *info, uint16_t pid, struct ttxt_descr *descr, int descr_index) +{ + struct ttxtpidinfo *pd; + struct ttxtpageinfo *pa; + + if(!info->p) { + info->p = (struct ttxtpidinfo *) malloc(sizeof(*(info->p))); + memset((char *) info->p, 0, sizeof(*(info->p))); + info->p[0].pid = pid; + info->pidcount++; + } + + if(pid != info->p[info->pidcount - 1].pid) { + info->pidcount++; + info->p = (struct ttxtpidinfo *) realloc(info->p, sizeof(info->p[0]) * info->pidcount); + memset((char *) &(info->p[info->pidcount - 1]), 0, sizeof(info->p[0])); + info->p[info->pidcount - 1].pid = pid; + } + + pd = &(info->p[info->pidcount - 1]); + + pd->pagecount++; + if(!pd->i) { + pd->i = (struct ttxtpageinfo *) malloc(sizeof(pd->i[0])); + } else { + pd->i = (struct ttxtpageinfo *) realloc(pd->i, sizeof(pd->i[0]) * pd->pagecount); + } + memset((char *) &(pd->i[pd->pagecount - 1]), 0, sizeof(pd->i[0])); + + pa = &(pd->i[pd->pagecount - 1]); + + pa->lang[0] = descr->d[descr_index].lang[0]; + pa->lang[1] = descr->d[descr_index].lang[1]; + pa->lang[2] = descr->d[descr_index].lang[2]; + pa->type = descr->d[descr_index].type_mag >> 3; + pa->mag = descr->d[descr_index].type_mag & 0x7; + pa->page = descr->d[descr_index].page_no; +} + + +static int HasVPID(int vpid, char **pmtsects, int numsects) +{ + int found = 0; + int i; + + for(i = 0; i < numsects && !found; i++) { + struct PMT_sect *psect; + char *sp; + char *end; + + psect = (struct PMT_sect *) pmtsects[i]; + end = pmtsects[i] + (psect->syntax_len & 0x3ff) - 7; + // skip extra program info + sp = ((char *) &(psect->s)) + (ntohs(psect->res_program_info_length) & 0xfff); + + while(sp < end && !found) { + struct PMT_stream *s = (struct PMT_stream *) sp; + + if(s->stream_type == 1 || s->stream_type == 2) { // Video stream + if((ntohs(s->res_PID) & 0x1fff) == vpid) { + //printf("Found ttxt by vpid: %d/%d\n", s->stream_type, (ntohs(s->res_PID) & 0x1fff)); + found = 1; + break; + } + } + sp += (ntohs(s->res_ES_info_len) & 0xfff) + 5; + } + } + + return found; +} + +static void ExtractTtxtInfo(char **pmtsects, int numsects, struct ttxtinfo *info, int *foundinfo) +{ + int i; + + for(i = 0; i < numsects; i++) { + struct PMT_sect *psect; + char *sp; + char *end; + + psect = (struct PMT_sect *) pmtsects[i]; + end = pmtsects[i] + (psect->syntax_len & 0x3ff) - 7; + // skip extra program info + sp = ((char *) &(psect->s)) + (ntohs(psect->res_program_info_length) & 0xfff); + + while(sp < end) { + struct PMT_stream *s = (struct PMT_stream *) sp; + + if(s->stream_type == 6) { // PES private data + uint8_t *descr; + + for(descr = s->descrs; descr < s->descrs + (ntohs(s->res_ES_info_len) & 0xfff); descr += descr[1] + 2) { + if(descr[0] == DESCR_TELETEXT) { + struct ttxt_descr *t = (struct ttxt_descr *) descr; + int i, count = t->length / sizeof(t->d[0]); + for(i = 0; i < count; i++) { + addpageinfo(info, ntohs(s->res_PID) & 0x1fff, t, i); + //printf("%c%c%c: type %d, page: %01x%02x\n", t->d[i].lang[0], t->d[i].lang[1], t->d[i].lang[2], t->d[i].type_mag >> 3, t->d[i].type_mag & 0x7, t->d[i].page_no); + *foundinfo = 1; + } + } + } + } + sp += (ntohs(s->res_ES_info_len) & 0xfff) + 5; + } + } +} + +/* + * if vpid != 0, we first search if we can find the vpid in this PMT, + * and if so we go get the ttxt pid + * return <> 0 on error + */ +static int FindTtxtInfoInPMT(int card_no, int pid, int vpid, struct ttxtinfo *info, int *foundinfo) +{ + int ret = -1; + char *pmtsects[256]; + int numsects; + + ret = CollectSections(card_no, pid, 0x02, pmtsects, &numsects); + if(ret) + goto bail; + + if(vpid != 0) { + if(!HasVPID(vpid, pmtsects, numsects)) { + goto bail; + } + } + + ExtractTtxtInfo(pmtsects, numsects, info, foundinfo); + + bail: + FreeSects(pmtsects); + return ret; +} + + +/* + * find the ttxt_info in the PMT via the PAT, try first with the SID + * and if that fails with the VPID + * return <> 0 on error; + */ +int GetTtxtInfo(int card_no, uint16_t sid, uint16_t vpid, struct ttxtinfo *info) +{ + int ret; + char *patsects[256]; + int numsects; + int i; + int j; + uint16_t pmt_pid = 0; + int foundinfo = 0; + + memset((char *) info, 0, sizeof(*info)); + + ret = CollectSections(card_no, 0, 0, patsects, &numsects); + if(ret) + goto bail; + + if(sid != 0) { + int found; + + for(i = 0, found = 0; i < numsects && !found; i++) { + int numdescrs; + struct PAT_sect *s = (struct PAT_sect *) patsects[i]; + + numdescrs = ((ntohs(s->syntax_len) & 0x3FF) - 7) / 4; + + for(j = 0; j < numdescrs && !found; j++) { + uint16_t pno = ntohs(s->d[j].program_number); + + if(pno == 0) + continue; // network pid + + if(pno == sid) { + pmt_pid = ntohs(s->d[j].res_PMTPID) & 0x1fff; + found = 1; + } + } + } + } + + if(pmt_pid != 0) { + ret = FindTtxtInfoInPMT(card_no, pmt_pid, 0, info, &foundinfo); + } else { + // SID not found, try searching VID in all SIDS + if(vpid != 0) { + int done; + for(i = 0, done = 0; i < numsects && !done; i++) { + int numdescrs; + struct PAT_sect *s = (struct PAT_sect *) patsects[i]; + + numdescrs = ((ntohs(s->syntax_len) & 0x3FF) - 7) / 4; + + for(j = 0; j < numdescrs && !done; j++) { + uint16_t pno = ntohs(s->d[j].program_number); + + if(pno == 0) + continue; // network pid + + pmt_pid = ntohs(s->d[j].res_PMTPID) & 0x1fff; + + ret = FindTtxtInfoInPMT(card_no, pmt_pid, vpid, info, &foundinfo); + if(ret) { + done = 1; + } + if(foundinfo) + done = 1; + } + } + } + } + +bail: + FreeSects(patsects); + return ret; +} + + +void FreeTtxtInfoData(struct ttxtinfo *info) +{ + int i; + + for(i = 0; i < info->pidcount; i++) { + free((void *) info->p[i].i); + } + + free((void *) info->p); +} + + +struct ttxtpidinfo *FindSubs(struct ttxtinfo *info, char *lang, int HI, int *pid, int *pageno) +{ + struct ttxtpidinfo *foundNonHIInfo = NULL; + int foundNonHIPid = 0; + int foundNonHIPage = -1; + + for(int i = 0; i < info->pidcount; i++) { + for(int j = 0; j < info->p[i].pagecount; j++) { + if(info->p[i].i[j].lang[0] == lang[0] && + info->p[i].i[j].lang[1] == lang[1] && + info->p[i].i[j].lang[2] == lang[2]) { + if((!HI && info->p[i].i[j].type == TTXT_SUBTITLE_PAGE) || + (HI && info->p[i].i[j].type == TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE)) { + *pid = info->p[i].pid; + *pageno = info->p[i].i[j].mag * 0x100 + info->p[i].i[j].page; + fprintf(stderr, "ttxtsubs: Found selected subtitles on PID %d, page %03x\n", *pid, + *pageno < 0x100 ? *pageno + 0x800 : *pageno); + return &(info->p[i]); + } else if(HI && info->p[i].i[j].type == TTXT_SUBTITLE_PAGE) { + foundNonHIPid = info->p[i].pid; + foundNonHIPage = info->p[i].i[j].mag * 0x100 + info->p[i].i[j].page; + foundNonHIInfo = &(info->p[i]); + } + } + } + } + + if(foundNonHIInfo) { + *pid = foundNonHIPid; + *pageno = foundNonHIPage; + fprintf(stderr, "ttxtsubs: Found non HI subtitles on PID %d, page %03x\n", *pid, + *pageno < 0x100 ? *pageno + 0x800 : *pageno); + return foundNonHIInfo; + } + + if(info->pidcount == 0) + fprintf(stderr, "ttxtsubs: No teletext subtitles on channel.\n"); + else { + fprintf(stderr, "ttxtsubs: Subtitles for language \"%c%c%c\" not found on channel, available languages:\n", lang[0], lang[1], lang[2]); + for(int i = 0; i < info->pidcount; i++) { + for(int j = 0; j < info->p[i].pagecount; j++) { + int page = info->p[i].i[j].mag * 0x100 + info->p[i].i[j].page; + int type = info->p[i].i[j].type; + if(page < 0x100) + page += 0x800; + fprintf(stderr, " %03x: %c%c%c %s\n", page, info->p[i].i[j].lang[0], info->p[i].i[j].lang[1], info->p[i].i[j].lang[2], + type == TTXT_INITIAL_PAGE ? "(Initial Page)" : + type == TTXT_SUBTITLE_PAGE ? "(Subtitles)" : + type == TTXT_ADDITIONAL_INFO_PAGE ? "(Additional Info Page)" : + type == TTXT_PROGRAMME_SCHEDULE_PAGE ? "(Programme Schedule Page)" : + type == TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE ? "(Hearing Impaired)" : "(Unknown type)"); + } + } + } + + *pid = 0; + *pageno = -1; + return NULL; +} + + +#if 0 +int XX_GetTtxtSubtitleInfo(uint16_t pid, int card_no, struct ttxtinfo *info) +{ + int len; + char buf[4096]; + struct PMT_sect *p = (struct PMT_sect *) buf; + char *sp; + + memset((char *) info, 0, sizeof(*info)); + + len = GetSection(pid, 2, 0xFF, card_no, buf, sizeof(buf)); + + //dump_hex("Sect: ", (uint8_t *) buf, len); + + if(len < 0) + return len; + + if(len != ((ntohs(p->syntax_len) & 0xfff) + 3)) { + fprintf(stderr, "bad lengt: %x / %x!\n", len, ntohs(p->syntax_len)); + return -1; + } + + // also skip extra program info + sp = ((char *) &(p->s)) + (ntohs(p->res_program_info_length) & 0xfff); + + while(sp < buf + len - 4) { + struct PMT_stream *s = (struct PMT_stream *) sp; + + //printf("type: %d, pid: %d, len: %d\n", s->stream_type, ntohs(s->res_PID) & 0x1fff, ntohs(s->res_ES_info_len) & 0xfff); + //dump_hex("descr: ", sp, ntohs(s->res_ES_info_len) & 0xfff); + + if(s->stream_type == 6) { // PES private data + uint8_t *descr; + + for(descr = s->descrs; descr < s->descrs + (ntohs(s->res_ES_info_len) & 0xfff); descr += descr[1] + 2) { + //dump_hex("descr: ", descr, descr[1] + 2); + if(descr[0] == DESCR_TELETEXT) { + struct ttxt_descr *t = (struct ttxt_descr *) descr; + int i, count = t->length / sizeof(t->d[0]); + for(i = 0; i < count; i++) { + addpageinfo(info, ntohs(s->res_PID) & 0x1fff, t, i); + //printf("%c%c%c: type %d, page: %01x%02x\n", t->d[i].lang[0], t->d[i].lang[1], t->d[i].lang[2], t->d[i].type_mag >> 3, + //t->d[i].type_mag & 0x7, t->d[i].page_no); + } + } + } + } + + sp += (ntohs(s->res_ES_info_len) & 0xfff) + 5; + } + + return 0; +} +#endif + +#if 0 +//XXX temporary for testing! +int get_subs(char *lang, int sid, int vpid, int cardno, int *pid, int *page) { + struct ttxtinfo info; + int i, j; + int result = -1; + + GetTtxtInfo(cardno, sid, vpid, &info); + + for(i = 0; i < info.pidcount; i++) { + //printf("PID: %d ", info.p[i].pid); + for(j = 0; j < info.p[i].pagecount; j++) { + //printf(" %c%c%c: %01x%02x (%d) ", + // info.p[i].i[j].lang[0], info.p[i].i[j].lang[1], info.p[i].i[j].lang[2], + // info.p[i].i[j].mag, info.p[i].i[j].page, info.p[i].i[j].type); + if(info.p[i].i[j].lang[0] == lang[0] && info.p[i].i[j].lang[1] == lang[1] && + info.p[i].i[j].lang[2] == lang[2] && + (info.p[i].i[j].type == TTXT_SUBTITLE_PAGE || info.p[i].i[j].type == TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE)) { + *pid = info.p[i].pid; + *page = info.p[i].i[j].mag * 0x100 + info.p[i].i[j].page; + result = 0; + } + } + //printf("\n"); + } + + return result; +} +#endif diff --git a/siinfo.h b/siinfo.h new file mode 100644 index 0000000..16ac445 --- /dev/null +++ b/siinfo.h @@ -0,0 +1,36 @@ + + +#define TTXT_INITIAL_PAGE 1 +#define TTXT_SUBTITLE_PAGE 2 +#define TTXT_ADDITIONAL_INFO_PAGE 3 +#define TTXT_PROGRAMME_SCHEDULE_PAGE 4 +#define TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE 5 + +struct ttxtpageinfo { + char lang[3]; + uint8_t type; + uint8_t mag; + uint8_t page; +}; + +struct ttxtpidinfo { + uint16_t pid; + int pagecount; + struct ttxtpageinfo *i; +}; + +struct ttxtinfo { + int pidcount; + struct ttxtpidinfo *p; +}; + +/* + * find the ttxt_info in the PMT via the PAT, try first with the SID + * and if that fails with the VPID + * return <> 0 on error; + */ +int GetTtxtInfo(int card_no, uint16_t sid, uint16_t vpid, struct ttxtinfo *info); + +void FreeTtxtInfoData(struct ttxtinfo *info); + +struct ttxtpidinfo *FindSubs(struct ttxtinfo *info, char *lang, int HI, int *pid, int *pageno); diff --git a/teletext-chars.h b/teletext-chars.h new file mode 100644 index 0000000..1ebc10b --- /dev/null +++ b/teletext-chars.h @@ -0,0 +1,80 @@ + +/* + * Character mappings + * An attempt to map Teletext characters into ISO-8859-1. + * Trying to use similar looking or similar meaning + * characters. + */ + + +/* + * G0 and G2 national option table given Triplet 1 bits 14-11 and Control bits from C12-14 + * ETSI EN 300 706 Table 33 + */ +uint8_t laG0_nat_opts_lookup[16][8] = { + {1, 4, 11, 5, 3, 8, 0, 1}, + {7, 4, 11, 5, 3, 1, 0, 1}, + {1, 4, 11, 5, 3, 8, 12, 1}, + {1, 1, 1, 1, 1, 10, 1, 9}, + {1, 4, 2, 6, 1, 1, 0, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, // 5 - reserved + {1, 1, 1, 1, 1, 1, 12, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, // 7 - reserved + {1, 1, 1, 1, 3, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, // 9 - reserved + {1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, // 11 - reserved + {1, 1, 1, 1, 1, 1, 1, 1}, // 12 - reserved + {1, 1, 1, 1, 1, 1, 1, 1}, // 13 - reserved + {1, 1, 1, 1, 1, 1, 1, 1}, // 14 - reserved + {1, 1, 1, 1, 1, 1, 1, 1} // 15 - reserved +}; + + +uint8_t laG0_nat_replace_map[128] = { +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6, 7, 8, +9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 0 +}; + + +/* + * Latin National Option Sub-Sets + * ETSI EN 300 706 Table 36 + */ + +uint8_t laG0_nat_opts[13][14] = { +{0, '#', 'u', 'c', 't', 'z', 'ý', 'í', 'r', 'é', 'á', 'e', 'ú', 's'}, // 0 - Czech/Slovak +{0, '£', '$', '@', '-', '½', '-', '|', '#', '-', '¼', '#', '¾', '÷'}, // 1 - English +{0, '#', 'õ', 'S', 'Ä', 'Ö', 'Z', 'Ü', 'Õ', 's', 'ä', 'ö', 'z', 'ü'}, // 2 - Estonian +{0, 'é', 'ï', 'à', 'ë', 'ê', 'ù', 'î', '#', 'è', 'â', 'ô', 'û', 'ç'}, // 3 - French +{0, '#', '$', '§', 'Ä', 'Ö', 'Ü', '^', '_', 'º', 'ä', 'ö', 'û', 'ß'}, // 4 - German +{0, '£', '$', 'é', 'º', 'ç', '-', '|', '#', 'ù', 'à', 'ò', 'è', 'ì'}, // 5 - Italian +{0, '#', '$', 'S', 'e', 'e', 'Z', 'c', 'u', 's', 'a', 'u', 'z', 'i'}, // 6 - Lettish/Lithuanian +{0, '#', 'n', 'a', 'Z', 'S', 'L', 'c', 'ó', 'e', 'z', 's', 'l', 'z'}, // 7 - Polish +{0, 'ç', '$', 'i', 'á', 'é', 'í', 'ó', 'ú', '¿', 'ü', 'ñ', 'è', 'à'}, // 8 - Portuguese/Spanish +{0, '#', '¤', 'T', 'Â', 'S', 'A', 'Î', 'i', 't', 'â', 's', 'a', 'î'}, // 9 - Rumanian +{0, '#', 'Ë', 'C', 'C', 'Z', 'D', 'S', 'ë', 'c', 'c', 'z', 'd', 's'}, // 10 - Serbian/Croation/Slovenian +{0, '#', '¤', 'É', 'Ä', 'Ö', 'Å', 'Ü', '_', 'é', 'ä', 'ö', 'å', 'ü'}, // 11 - Swedish/Finnish/Hungarian +{0, 'T', 'g', 'I', 'S', 'Ö', 'Ç', 'Ü', 'G', 'i', 'S', 'ö', 'ç', 'ü'} // 12 - Turkish +}; + +/* + * Latin G2 Set + * ETSI EN 300 706 Table 37 + */ +uint8_t laG2_la1_table[] = { +'?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', +'?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', +' ', '¡', '¢', '£', '$', '¥', '#', '§', '¤', '´', '"', '«', '-', '|', '-', '|', +'°', '±', '²', '³', '×', 'µ', '¶', '·', '÷', '\'', '"', '»', '¼', '½', '¾', '¿', +' ', '`', '´', '^', '~', '-', '?', '·', '¨', '.', 'º', '¸', '_', '"', '?', '?', +'-', '¹', '®', '©', 'T', '?', 'C', 'p', '?', ' ', ' ', ' ', '?', '?', '?', '?', +'?', 'Æ', 'D', 'a', '?', '?', ' ', 'I', 'L', 'L', 'Ø', 'Ö', 'o', 'P', 'n', 'n', +'k', 'æ', 'd', 'd', 'h', 'i', 'i', 'l', 'l', 'ø', 'ö', 'ß', 'p', 't', 'n', '?' +}; diff --git a/teletext-tables.h b/teletext-tables.h new file mode 100644 index 0000000..b1e6a13 --- /dev/null +++ b/teletext-tables.h @@ -0,0 +1,81 @@ +/* This file is copied from Ralph Metzler's vbidecode package. */ + +/* + tables.h - some data conversion tables for vbidecode +*/ + +#ifndef _TABLES_H +#define _TABLES_H + +unsigned char unhamtab[256] = { + 0x01, 0xff, 0x81, 0x01, 0xff, 0x00, 0x01, 0xff, + 0xff, 0x02, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x07, + 0xff, 0x00, 0x01, 0xff, 0x00, 0x80, 0xff, 0x00, + 0x06, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x03, 0xff, + 0xff, 0x0c, 0x01, 0xff, 0x04, 0xff, 0xff, 0x07, + 0x06, 0xff, 0xff, 0x07, 0xff, 0x07, 0x07, 0x87, + 0x06, 0xff, 0xff, 0x05, 0xff, 0x00, 0x0d, 0xff, + 0x86, 0x06, 0x06, 0xff, 0x06, 0xff, 0xff, 0x07, + 0xff, 0x02, 0x01, 0xff, 0x04, 0xff, 0xff, 0x09, + 0x02, 0x82, 0xff, 0x02, 0xff, 0x02, 0x03, 0xff, + 0x08, 0xff, 0xff, 0x05, 0xff, 0x00, 0x03, 0xff, + 0xff, 0x02, 0x03, 0xff, 0x03, 0xff, 0x83, 0x03, + 0x04, 0xff, 0xff, 0x05, 0x84, 0x04, 0x04, 0xff, + 0xff, 0x02, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x07, + 0xff, 0x05, 0x05, 0x85, 0x04, 0xff, 0xff, 0x05, + 0x06, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x03, 0xff, + 0xff, 0x0c, 0x01, 0xff, 0x0a, 0xff, 0xff, 0x09, + 0x0a, 0xff, 0xff, 0x0b, 0x8a, 0x0a, 0x0a, 0xff, + 0x08, 0xff, 0xff, 0x0b, 0xff, 0x00, 0x0d, 0xff, + 0xff, 0x0b, 0x0b, 0x8b, 0x0a, 0xff, 0xff, 0x0b, + 0x0c, 0x8c, 0xff, 0x0c, 0xff, 0x0c, 0x0d, 0xff, + 0xff, 0x0c, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x07, + 0xff, 0x0c, 0x0d, 0xff, 0x0d, 0xff, 0x8d, 0x0d, + 0x06, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x0d, 0xff, + 0x08, 0xff, 0xff, 0x09, 0xff, 0x09, 0x09, 0x89, + 0xff, 0x02, 0x0f, 0xff, 0x0a, 0xff, 0xff, 0x09, + 0x88, 0x08, 0x08, 0xff, 0x08, 0xff, 0xff, 0x09, + 0x08, 0xff, 0xff, 0x0b, 0xff, 0x0e, 0x03, 0xff, + 0xff, 0x0c, 0x0f, 0xff, 0x04, 0xff, 0xff, 0x09, + 0x0f, 0xff, 0x8f, 0x0f, 0xff, 0x0e, 0x0f, 0xff, + 0x08, 0xff, 0xff, 0x05, 0xff, 0x0e, 0x0d, 0xff, + 0xff, 0x0e, 0x0f, 0xff, 0x0e, 0x8e, 0xff, 0x0e, +}; + +// invert tab for last 42 bytes of data in packets +unsigned char invtab[256] = { + 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, + 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, + 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, + 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, + 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, + 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, + 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, + 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, + 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, + 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, + 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, + 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, + 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, + 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, + 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, + 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, + 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, + 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, + 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, + 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, + 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, + 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, + 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, + 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, + 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, + 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, + 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, + 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, + 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, + 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, + 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, + 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff, +}; + +#endif diff --git a/teletext.c b/teletext.c new file mode 100644 index 0000000..13ea178 --- /dev/null +++ b/teletext.c @@ -0,0 +1,724 @@ + +#include <stdio.h> +#include <stdint.h> +#include <sys/types.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> +#include <netinet/in.h> +#include <ctype.h> +#include <string.h> + +//#include <linux/dvb/dmx.h> +//#include <vdr/osd.h> + +#include "teletext.h" +#include "teletext-tables.h" +#include "teletext-chars.h" +#include "utils.h" + + +// FROM vbidecode +// unham 2 bytes into 1, report 2 bit errors but ignore them +unsigned char unham(unsigned char a,unsigned char b) +{ + unsigned char c1,c2; + + c1=unhamtab[a]; + c2=unhamtab[b]; +// if ((c1|c2)&0x40) +// fprintf(stderr,"bad ham!"); + return (c2<<4)|(0x0f&c1); +} + + +// ham 8/4 the byte in into two bytes pointed to by out +// should make a table instead +void ham8_4byte(uint8_t in, uint8_t *out) +{ + out[0] = ham8_4nibble(in & 0xF); + out[1] = ham8_4nibble(in >> 4); + + if (1) { // XXX debug + int a; + if(unham(out[0], out[1]) != in) { + fprintf(stderr, "ham8_4: 1 - result not correct %02x -> %02x %02x!\n", in, out[0], out[1]); + } + a = unhamtab[out[0]]; + a ^= 0x80; + if(a & 0xf0 || (a != (in & 0xF))) { + fprintf(stderr, "ham8_4: 2 - result not correct %02x -> %02x %02x, %02x!\n", in, out[0], out[1], a); + } + a = unhamtab[out[1]]; + a ^= 0x80; + if(a & 0xf0 || (a != (in >> 4))) { + fprintf(stderr, "ham8_4: 3 - result not correct %02x -> %02x %02x, %02x!\n", in, out[0], out[1], a); + } + } +} + +// should be a table instead +int parity(uint8_t x) { + int res = 1; + int count = x & 0xf0 ? 8 : 4; + + while(count--) { + if(x & 0x01) + res = !res; + x >>= 1; + } + + return res; +} + +// ham 8/4 the nibble in into the byte pointed to by out +// should make a table instead +// D4 P4 D3 P3 D2 P2 D1 P1 +// P4 = all the rest +uint8_t ham8_4nibble(uint8_t in) +{ + uint8_t o = 0; + + // insert the data bits + o |= (in << 4) & 0x80; + o |= (in << 3) & 0x20; + o |= (in << 2) & 0x08; + o |= (in << 1) & 0x02; + + if(parity(in & 0x0d)) // P1 = 1 . D1 . D3 . D4 + o |= 0x01; + if(parity(in & 0x0b)) // P2 = 1 . D1 . D2 . D4 + o |= 0x04; + if(parity(in & 0x07)) // P3 = 1 . D1 . D2 . D3 + o |= 0x10; + if(parity(o & 0xbf)) // P4 = 1 . P1 . D1 . ... . D3 . D4 + o |= 0x40; + + return o; +} + + +/* + * Map Latin G0 teletext characters into a ISO-8859-1 approximation. + * Trying to use similar looking or similar meaning characters. + * Gtriplet - 4 bits = triplet-1 bits 14-11 (14-8) of a X/28 or M/29 packet, if unknown - use 0 + * natopts - 3 bits = national_options field of a Y/0 packet (or triplet-1 bits 10-8 as above?) + * inchar - 7 bits = characted to remap + * Also strips parity + */ + +uint8_t ttxt_laG0_la1_char(int Gtriplet, int natopts, uint8_t inchar) +{ + int no = laG0_nat_opts_lookup[Gtriplet & 0xf][natopts & 0x7]; + uint8_t c = inchar & 0x7f; + + //dprint("\n%x/%c/%x/%x\n", c, c, laG0_nat_replace_map[c], laG0_nat_opts[no][laG0_nat_replace_map[c]]); + + if(!laG0_nat_replace_map[c]) + return c; + else + return laG0_nat_opts[no][laG0_nat_replace_map[c]]; +} + +/* + * Map Latin G2 teletext characters into a ISO-8859-1 approximation. + * Trying to use similar looking or similar meaning characters. + */ +uint8_t ttxt_laG2_la1_char(uint8_t inchar) +{ + return laG2_la1_table[inchar & 0x7f]; +} + +void dump_hex(char *msg, const uint8_t *p, int len) +{ + int i; + printf("%s:", msg); + for(i = 0; i < len; i++) { + printf(" %02x", p[i]); + } + printf("\n"); +} + + +void +print_code(uint8_t c, int natopts) +{ + if(c >= 0x20) { + dprint("%c", ttxt_laG0_la1_char(0, natopts, c)); + } else + switch(c) { + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: + dprint("<fc%d>", c); + break; + case 0x08: + dprint("<flash>"); + break; + case 0x09: + dprint("<Steady>"); + break; + case 0x0a: + dprint("]"); + break; + case 0x0b: + dprint("["); + break; + case 0x0c: + dprint("<normal size>"); + break; + case 0x0d: + dprint("<dh>"); + break; + case 0x0e: + dprint("<dbl width>"); + break; + case 0x0f: + dprint("<dbl size>"); + break; + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + dprint("<mosc%d>", c-0x10); + break; + case 0x18: + dprint("<conceal>"); + break; + case 0x19: + dprint("<cont mosaic>"); + break; + case 0x1a: + dprint("<sep mosaic>"); + break; + case 0x1b: + dprint("<charset switch>"); + break; + case 0x1c: + dprint("<black bg>"); + break; + case 0x1d: + dprint("<new bg>"); + break; + case 0x1e: + dprint("<hold mosaic>"); + break; + case 0x1f: + dprint("<rele mosaic>"); + break; + default: + dprint("<U%02x>", c); + } +} + +void +print_page(struct ttxt_page *p) +{ + int r, c, e; + + dprint("\n\n%03x %s%s%s%s%snatchars: %d\n", + (p->mag << 8) + p->no, + p->flags & erasepage ? "erasepage, " : "", + p->flags & newsflash ? "Newsflash, " : "", + p->flags & subtitle ? "Subtitle, " : "", + p->flags & suppress_header ? "SuppressHeader, " : "", + p->flags & inhibit_display ? "InihinitDisplay, " : "", + p->national_charset); + + for(r = 0; r < 26; r++) { + int do_display = 0; + + for(e = 39; e > 0; e--) { + if(p->data[r][e]) { + do_display = 1; + break; + } + } + + if(do_display) { + dprint("%02d ", r); + for(c = 0; c <= e; c++) { + char z = p->data[r][c] & 0x7f; + print_code(z, p->national_charset); + //dprint("%c", isprint(z) ? z : z == 0 ? '\'' : '.'); + } + dprint("\n"); + } + } +} + + +void print_line(char *p) +{ + int mp; + int mag; // X in ETSI EN 300 706 + int packet; // Y + struct ttxt_data_field *d = (struct ttxt_data_field *) p; + + mp = unham(invtab[d->mag_addr_ham[0]], invtab[d->mag_addr_ham[1]]); + mag = mp & 0x7; + packet = (mp >> 3) & 0x1f; + + dprint("I%d Y%02d M%d ", d->data_unit_id, packet, mag); + + // packet 0, change page no in this magazine + if(packet == 0) { + int no; + uint8_t buf[8]; + + for(int i = 0; i < 8; i++) + buf[i] = invtab[d->data[i]]; + + no = unham(buf[0], buf[1]); + + dprint("P%02x ", no); + + dprint("<"); + for(int i = 8; i < 40; i++) + print_code(invtab[d->data[i]] & 0x7f, 0); + dprint(">"); + + if(buf[3] & 0x80) dprint(", erase page"); + if(buf[5] & 0x20) dprint(", newsflash"); + if(buf[5] & 0x80) dprint(", subtitle"); + if(buf[6] & 0x02) dprint(", suppress header"); + if(buf[6] & 0x08) dprint(", update indicator"); + if(buf[6] & 0x20) dprint(", interrupted sequence"); + if(buf[6] & 0x80) dprint(", inhibit display"); + if(buf[7] & 0x02) dprint(", magazine serial"); + + dprint(", charset=%d", ((buf[7] & 0x80) >> 5) + ((buf[7] & 0x20) >> 4) + ((buf[7] & 0x08) >> 3)); + } else { + dprint("<"); + for(int i = 0; i < 40; i++) + print_code(invtab[d->data[i]] & 0x7f, 0); + dprint(">"); + } + + dprint("\n"); +} + + +#if 0 +void +show_page(struct ttxt_page *p) +{ + static cOsdBase *o = NULL; + static tWindowHandle w1 = 0; + static tWindowHandle w2 = 0; + static int open1 = 0, open2 = 0; + char *s1 = NULL, *s2 = NULL; + int i, j; + + for(i = 1; i < TTXT_ROWS; i++) { + char *s = NULL; + if(p->data[i][0]) { + int do_incr = 0; + dprint("Checking row %d\n", i); + for(j = 0; j < 40; j++) { + p->data[i][j] = p->data[i][j] & 0x7f; + switch(p->data[i][j]) { + case 0x0d: // double height + case 0x0f: // double size + do_incr = 1; + break; + case 0x0a: // box end + p->data[i][j] = '\0'; + break; + case 0x0b: // box begin + s = (char *) &(p->data[i][j+1]); + dprint("Using row %d\n", i); + break; + case '[': + p->data[i][j] = 'Ä'; + break; + case ']': + p->data[i][j] = 'Å'; + break; + case '\\': + p->data[i][j] = 'Ö'; + break; + case '{': + p->data[i][j] = 'ä'; + break; + case '}': + p->data[i][j] = 'å'; + break; + case '|': + p->data[i][j] = 'ö'; + break; + } + } + if(do_incr) + i++; + dprint("\n"); + } + + if(s) + if(!s1) + s1 = s; + else if(!s2) + s2 = s; + } + + if(s1 && !s2) { + s2 = s1; + s1 = NULL; + } + + if(s1) + dprint("S1: %s\n", s1); + else + dprint("S1: <none>\n"); + if(s2) + dprint("S2: %s\n", s2); + else + dprint("S2: <none>\n"); + + if(o != NULL) { + if(open1) { + o->Clear(w1); + open1 = 0; + } + if(open2) { + o->Clear(w2); + open2 = 0; + } + + o->Clear(); + delete o; + o = NULL; + } + + if(!s1 && !s2) + return; + + o = cOsd::OpenRaw(125, 450); + + if(o == NULL) + return; + + o->SetFont(fontOsd); + + if(s1) { + int w = o->Width((char *) s1); + dprint("w1: %d\n", w); + w = 450; + int h = 36; + w1 = o->Create(0, 0, w, h, 2); +#if 0 + o->AddColor(clrBackground, w1); + o->AddColor(clrWhite, w1); +#endif + o->Fill(0, 0, w, h, clrBackground, w1); + o->Text(15, 4, (char *) s1, clrWhite, clrBackground, w1); + open1 = 1; + } + + if(s2) { + int w = o->Width((char *) s2); + dprint("w2: %d\n", w); + w = 452; + int h = 36; + w2 = o->Create(0, 45, w, h, 2); +#if 0 + o->AddColor(clrBackground, w2); + o->AddColor(clrWhite, w2); +#endif + o->Fill(0, 45, w, 45 + h, clrBackground, w2); + o->Text(15, 45 + 4, (char *) s2, clrWhite, clrBackground, w2); + open2 = 1; + } + + o->Flush(); +} +#endif + +#if 0 +void +ttxt_packet_in(int data_unit_id, int mag, int pack, uint8_t *data) +{ + static int valid = 0; + static struct ttxt_page p; + int i; + + if(pack == 0) { + if(valid) + if(p.mag == TTXT_MAG && p.no == TTXT_PAGE) + if(p.subtitle) + //print_page(&p); + /* if(p.mag == 1) */{ + show_page(&p); + print_page(&p); + } + + memset(&p, 0, sizeof(p)); + p.data_unit_id = data_unit_id; + p.mag = mag; + p.no = unham(data[0], data[1]); + + if(data[3] & 0x80) { // Erase Page + memset(p.data, 0, sizeof(p.data)); + p.erasepage = 1; + } + if(data[5] & 0x20) // Newsflash + p.newsflash = 1; + if(data[5] & 0x80) // Subtitle + p.subtitle = 1; + if(data[6] & 0x02) // Suppress Header + p.suppress_header = 1; + //if(data[6] & 0x08) // Update Indicator + //if(data[6] & 0x20) // Interrupted Sequence + if(data[6] & 0x80) // Inhibit Display + p.inhibit_display = 1; + //if(data[7] & 0x02) // Magazine Serial + + p.national_charset = + ((data[7] & 0x80) >> 5) + ((data[7] & 0x20) >> 4) + ((data[7] & 0x08) >> 3); + + valid = 1; + + for(i = 0; i < 32; i++) + p.data[0][i] = data[i+8]; + } else if(pack < 26) { + for(i = 0; i < 42; i++) + p.data[pack][i] = data[i]; + } else { + if((valid && p.mag == TTXT_MAG && p.no == TTXT_PAGE) || + pack == 28) + dprint("mag: %d, pack: %d, page: %02x\n", p.mag, pack, p.no); + } +} +#endif + +#if 0 +void +ttxt_pes_data_in(struct ttxt_pes_data_field *t, int len) +{ + int i; + + if(len < 1) + return; + + if(t->data_identifier < 0x10 || t->data_identifier > 0x1F) { + dprint("Unknown data identifier: %02x\n", t->data_identifier); + return; + } + + for(i = 0; (uint8_t *) (&(t->d[i+1])) < (((uint8_t *) t) + len); i++) { + struct ttxt_data_field *d = &(t->d[i]); + int i; + uint8_t *p; + int uh; + int mag; + int pack; + + // if(d->data_unit_id != ttxt_subtitle) + // continue; + + p = (uint8_t *) &(d->mag_addr_ham); + for(i = 0; i < 42; i++) { + p[i] = invtab[p[i]]; + } + + uh = unham(d->mag_addr_ham[0], d->mag_addr_ham[1]); + mag = uh & 0x7; + pack = (uh >> 3) & 0x1f; + + // dump_hex("line", (uint8_t *) d, 20); + // dprint("id: %d, line: %d, mag %d, pack: %d\n", d->data_unit_id, TTXT_LINE_NO(*d), mag, pack); + + ttxt_packet_in(d->data_unit_id, mag, pack, d->data); + } +} + + +void +set_filt(int fd,uint16_t tt_pid, dmx_pes_type_t t) +{ + struct dmx_pes_filter_params pesFilterParams; + + memset(&pesFilterParams, 0, sizeof(pesFilterParams)); + + pesFilterParams.pid = tt_pid; + pesFilterParams.input = DMX_IN_FRONTEND; + pesFilterParams.output = DMX_OUT_TAP; + pesFilterParams.pes_type = t; + pesFilterParams.flags = DMX_IMMEDIATE_START; + + if (ioctl(fd, DMX_SET_PES_FILTER, &pesFilterParams) < 0) + perror("DMX SET PES FILTER:"); + + // XXX set buffer size too? +} +#endif + +#if 0 +#define BUFSIZE 2000 + +// external entry point +void ttxt_decode(int fd, int tpid, int *doRun) +{ + uint8_t buf[BUFSIZE]; + int n; + int hunt = 1; + + set_filt(fd, tpid, DMX_PES_OTHER); + + while (*doRun) { + restart: + if(hunt) { + int i; + + dprint("hunting...\n"); + n = read(fd, buf, 199); + if(n < 0) { + perror("ttxt_decode:read: "); + goto restart; + } + + for(i = 0; i < (n - 3); i++) { + if(buf[i] == 0 && buf[i+1] == 0 && buf[i+2] == 1 && buf[i+3] == 0xbd) { + struct ttxt_pes_header *p = (struct ttxt_pes_header *) (buf + i); + size_t skip = ntohs(p->len) + 6 - (n - i); + + if(skip > 0) { + read(fd, buf, skip < sizeof(buf) ? skip : sizeof(buf)); + if(n < 0) { + perror("ttxt_decode:read: "); + goto restart; + } + hunt = 0; + break; + } + } + } + } else { + struct ttxt_pes_header *p = (struct ttxt_pes_header *) buf; + + n = read(fd, buf, 6); + if(n < 0) { + perror("ttxt_decode:read: "); + goto restart; + } + + if(p->start[0] != 0 || p->start[1] != 0 || p->start[2] != 1 || + p->stream_id != 0xbd || // private_stream_1 + ntohs(p->len) > (sizeof(buf) - 6)) { + dprint("Lost PES packet sync, hunting...\n"); + hunt = 1; + continue; + } + + n = read(fd, buf + 6, ntohs(p->len)); + if(n < 0) { + perror("ttxt_decode:read: "); + goto restart; + } + + // dprint("len: %d\n", n); + + if(n != ntohs(p->len) || + p->header_len != 0x24) { // header should allways be 0x24 long (EN 300 472) + dprint("Error reading PES packet, hunting...\n"); + hunt = 1; + continue; + } + + ttxt_pes_data_in((struct ttxt_pes_data_field *) (buf + 45), n + 6 - 45); + } + + // fwrite(buf,1,n,stdout); + } + + dprint("ttxt_decode DONE!\n"); +} +#endif + + +#if 0 +int +main(int argc, char **argv) +{ + int fd; + int pid; + uint8_t buf[BUFSIZE]; + int n; + int hunt = 1; + + if (argc!=2) { + fprintf(stderr,"USAGE: %s PID\n", argv[0]); + return -1; + } + + pid = atoi(argv[1]); + + if((fd = open("/dev/dvb/adapter0/demux0", O_RDWR)) < 0){ + perror("Error opening demux device: "); + return -1; + } + + set_filt(fd, pid, DMX_PES_OTHER); + + for (;;) { + if(hunt) { + int i; + + dprint("hunting...\n"); + n = read(fd, buf, 199); + + for(i = 0; i < (n - 3); i++) { + if(buf[i] == 0 && buf[i+1] == 0 && buf[i+2] == 1 && buf[i+3] == 0xbd) { + struct ttxt_pes_header *p = (struct ttxt_pes_header *) (buf + i); + int skip = ntohs(p->len) + 6 - (n - i); + + if(skip > 0) { + read(fd, buf, skip < sizeof(buf) ? skip : sizeof(buf)); + hunt = 0; + break; + } + } + } + } else { + struct ttxt_pes_header *p = (struct ttxt_pes_header *) buf; + + n = read(fd, buf, 6); + + if(p->start[0] != 0 || p->start[1] != 0 || p->start[2] != 1 || + p->stream_id != 0xbd || // private_stream_1 + ntohs(p->len) > (sizeof(buf) - 6)) { + dprint("Lost PES packet sync, hunting...\n"); + hunt = 1; + continue; + } + + n = read(fd, buf + 6, ntohs(p->len)); + + // dprint("len: %d\n", n); + + if(n != ntohs(p->len) || + p->header_len != 0x24) { // header should allways be 0x24 long (EN 300 472) + dprint("Error reading PES packet, hunting...\n"); + hunt = 1; + continue; + } + + ttxt_pes_data_in((struct ttxt_pes_data_field *) (buf + 45), n + 6 - 45); + } + + // fwrite(buf,1,n,stdout); + } + + return 0; +} +#endif diff --git a/teletext.h b/teletext.h new file mode 100644 index 0000000..fa1b519 --- /dev/null +++ b/teletext.h @@ -0,0 +1,101 @@ + +#ifndef __TS_TELETEXT_H +#define __TS_TELETEXT_H + + +#define PACK __attribute__ ((__packed__)) + +struct ttxt_pes_header { + uint8_t start[3]; + uint8_t stream_id PACK; + uint16_t len PACK; + + // private_stream_1 packets (and others) + uint8_t flags[2] PACK; + uint8_t header_len PACK; +}; + + +enum { + ttxt_non_subtitle = 2, + ttxt_subtitle = 3, + ttxt_stuff = 0xff +}; + +#define TTXT_LINE_NO(x) ((x).par_loff & 0x1f) +struct ttxt_data_field { + uint8_t data_unit_id PACK; + uint8_t data_unit_length PACK; + uint8_t par_loff PACK; + uint8_t framing_code PACK; + uint8_t mag_addr_ham[2] PACK; + uint8_t data[40] PACK; +}; + + +struct ttxt_pes_data_field { + uint8_t data_identifier PACK; + struct ttxt_data_field d[1] PACK; +}; + + +// Really 25 + row 0 +#define TTXT_DISPLAYABLE_ROWS 26 + +enum pageflags { + erasepage = 0x01, + newsflash = 0x02, + subtitle = 0x04, + suppress_header = 0x08, + inhibit_display = 0x10 // rows 1 - 24 are not to be displayed +}; + +struct ttxt_page { + uint8_t mag; + uint8_t no; + uint8_t flags; + uint8_t national_charset; + uint8_t data[TTXT_DISPLAYABLE_ROWS][40]; +}; + +// FROM vbidecode +// unham 2 bytes into 1, report 2 bit errors but ignore them +unsigned char unham(unsigned char a,unsigned char b); + +#define UNHAM_INV(a, b) unham(invtab[a], invtab[b]) + +// ham 8/4 the byte in into two bytes pointed to by out +void ham8_4byte(uint8_t in, uint8_t *out); +// ham 8/4 the nibble in into the byte pointed to by out +uint8_t ham8_4nibble(uint8_t in); + +// odd parity status for last 7 bits of byte +int parity(uint8_t x); + +/* + * Map Latin G0 teletext characters into a ISO-8859-1 approximation. + * Trying to use similar looking or similar meaning characters. + * Gtriplet - 4 bits = triplet-1 bits 14-11 (14-8) of a X/28 or M/29 packet, if unknown - use 0 + * natopts - 3 bits = national_options field of a Y/0 packet (or triplet-1 bits 10-8 as above?) + * inchar - 7 bits = characted to remap + * Also strips parity + */ +uint8_t ttxt_laG0_la1_char(int Gtriplet, int natopts, uint8_t inchar); + +/* + * Map Latin G2 teletext characters into a ISO-8859-1 approximation. + * Trying to use similar looking or similar meaning characters. + */ +uint8_t ttxt_laG2_la1_char(uint8_t inchar); + + + +// debug stuff +void print_page(struct ttxt_page *p); +void print_line(char *p); +void dump_hex(char *msg, const uint8_t *p, int len); + +// invert tab for last 42 bytes in packets +extern unsigned char invtab[256]; + +#endif __TS_TELETEXT_H diff --git a/ttxtsubs.c b/ttxtsubs.c new file mode 100644 index 0000000..da9c2f1 --- /dev/null +++ b/ttxtsubs.c @@ -0,0 +1,357 @@ +/* + * ttxtsubs.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id: ttxtsubs.c,v 1.11 2003/03/07 05:52:58 ragge Exp $ + */ + +#include <vdr/plugin.h> +#include <vdr/status.h> +#include <vdr/vdrttxtsubshooks.h> +#include <vdr/menuitems.h> +#include <vdr/config.h> + +#include "ttxtsubsdisplayer.h" +#include "ttxtsubsrecorder.h" +#include "utils.h" +#include "siinfo.h" +#include "ttxtsubs.h" + +static const char *VERSION = "0.0.1"; +static const char *DESCRIPTION = "Teletext subtitles"; +//static const char *MAINMENUENTRY = "Ttxtsubs"; + +// ISO 639-2 language codes in VDR order +// XXX should be replaced by something that allows for other languages and for real language names! +// <http://www.loc.gov/standards/iso639-2/englangn_ascii.html> +char *gLanguages[] = +{ "eng", //English + "deu", //Deutsch + "slv", //Slovenian + "ita", //Italian + "dut", //"nld"? Dutch + "por", //Portuguese + "fre", //"fra"? French + "nor", //Norwegian + "fin", //Finnish + "pol", //Polish + "spa", //Spanish + "gre", //Greek + "swe", //Swedish + "rom", //Romanian + "hun", //Hungarian + // Not in translations! + "dan" //Danish + }; +int gNumLanguages = sizeof(gLanguages) / sizeof(gLanguages[0]); + +class cPluginTtxtsubs : public cPlugin, public cStatus, public cVDRTtxtsubsHookListener { +public: + cPluginTtxtsubs(void); + virtual ~cPluginTtxtsubs(); + + // -- cPlugin + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return DESCRIPTION; } + virtual const char *CommandLineHelp(void); + virtual bool ProcessArgs(int argc, char *argv[]); + virtual bool Start(void); + virtual void Housekeeping(void); + //virtual const char *MainMenuEntry(void) { return MAINMENUENTRY; } + virtual cOsdObject *MainMenuAction(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *Name, const char *Value); + + // -- Setup stuff + virtual char *Language(void) { return mLanguage; }; + virtual int HearingImpaired(void) { return mHearingImpaired; }; + virtual int Record(void) { return mRecord; }; + + virtual void SetLanguage(char *lang) { strncpy(mLanguage, lang, 4); mLanguage[3] = '\0'; }; + virtual void SetHearingImpaired(int hi) { mHearingImpaired = hi; }; + virtual void SetRecord(int record) { mRecord = record; }; + + // -- cStatus + protected: + virtual void ChannelSwitch(const cDevice *Device, int ChannelNumber); + virtual void Replaying(const cControl *Control, const char *Name); + // The given player control has started replaying Name. Name is the name of the + // recording, without any directory path. In case of a player that can't provide + // a name, Name can be a string that identifies the player type (like, e.g., "DVD"). + // If Name is NULL, the replay has ended. + // virtual void OsdClear(void) { ShowTtxt(); } + // virtual void OsdTitle(const char *Title) { HideTtxt(); } + // virtual void OsdStatusMessage(const char *Message) { HideTtxt(); } + // virtual void OsdHelpKeys(const char *Red, const char *Green, const char *Yellow, const char *Blue) { HideTtxt(); } + // virtual void OsdCurrentItem(const char *Text) { HideTtxt(); } + // virtual void OsdTextItem(const char *Text, bool Scroll) { HideTtxt(); } + // virtual void OsdChannel(const char *Text) { HideTtxt(); } + // virtual void OsdProgramme(time_t PresentTime, const char *PresentTitle, const char *PresentSubtitle, time_t FollowingTime, const char *FollowingTitle, const char *FollowingSubtitle) { HideTtxt(); } + + // -- cVDRTtxtsubsHookListener + virtual void HideOSD(void) { HideTtxt(); } + virtual void ShowOSD(void) { ShowTtxt(); } + virtual void PlayerTeletextData(uint8_t *p, int length); + virtual cTtxtSubsRecorderBase *NewTtxtSubsRecorder(cDevice *dev, const cChannel *ch); + + // -- internal + private: + void StartTtxtLive(const cDevice *Device, int pid, int page); + void StartTtxtPlay(int page); + void StopTtxt(void); + void ShowTtxt(void); + void HideTtxt(void); + +private: + // Add any member variables or functions you may need here. + cTtxtSubsDisplayer *mRec; + + // Setup items + char mLanguage[4]; + int mHearingImpaired; + int mRecord; + + // ugly hack for now + int mPage; +}; + +class cMenuSetupTtxtsubs : public cMenuSetupPage { + public: + cMenuSetupTtxtsubs(cPluginTtxtsubs *ttxtsubs); + protected: + virtual void Store(void); + private: + cPluginTtxtsubs *mTtxtsubs; + char mLanguage[4]; + int mLanguageNo; + int mHearingImpaired; + int mRecord; +}; + + +cPluginTtxtsubs::cPluginTtxtsubs(void) + : + mRec(NULL), + mHearingImpaired(0), + mRecord(1), + mPage(0x199) +{ + // Initialize any member variables here. + // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL + // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT! + + strncpy(mLanguage, "unk", 4); +} + +cPluginTtxtsubs::~cPluginTtxtsubs() +{ + // Clean up after yourself! +} + +const char *cPluginTtxtsubs::CommandLineHelp(void) +{ + // Return a string that describes all known command line options. + return NULL; +} + +bool cPluginTtxtsubs::ProcessArgs(int argc, char *argv[]) +{ + // Implement command line argument processing here if applicable. + return true; +} + +bool cPluginTtxtsubs::Start(void) +{ + // Start any background activities the plugin shall perform. + + if(!memcmp(mLanguage, "unk", 3)) { + int n = Setup.OSDLanguage; + if(n > gNumLanguages) { + strncpy(mLanguage, "eng", 4); + } else { + strncpy(mLanguage, gLanguages[n], 4); + mLanguage[3] = '\0'; + } + } + + //dprint("cPluginTtxtsubs::Start\n"); + + HookAttach(); + + return true; +} + +void cPluginTtxtsubs::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. +} + +cOsdObject *cPluginTtxtsubs::MainMenuAction(void) +{ + // Perform the action when selected from the main VDR menu. + return NULL; +} + +cMenuSetupPage *cPluginTtxtsubs::SetupMenu(void) +{ + return new cMenuSetupTtxtsubs(this); +} + +bool cPluginTtxtsubs::SetupParse(const char *Name, const char *Value) +{ + if(!strcasecmp(Name, "Language")) { strncpy(mLanguage, Value, 4); mLanguage[3] = '\0'; } + else if(!strcasecmp(Name, "HearingImpaired")) mHearingImpaired = atoi(Value); + else if(!strcasecmp(Name, "Record")) mRecord = atoi(Value); + else + return false; + return true; +} + +void cPluginTtxtsubs::ChannelSwitch(const cDevice *Device, int ChannelNumber) +{ + if(ChannelNumber) { + cChannel *c = Channels.GetByNumber(ChannelNumber); + if(c) { + struct ttxtinfo info; + int pid, page; + + if(GetTtxtInfo(Device->DeviceNumber(), c->Sid(), c->Vpid(), &info)) { + fprintf(stderr, "ttxtsubs: Error: GetTtxtInfo failed!\n"); + } else { + if(FindSubs(&info, mLanguage, mHearingImpaired, &pid, &page)) { + //fprintf(stderr, "CHANNELSWITCH, pid: %d page: %x\n", pid, page); + mPage = page; // XXX remember this for playback (temporary hack)! + StartTtxtLive(Device, pid, page); + } + FreeTtxtInfoData(&info); + } + } + } else + StopTtxt(); +} + +void cPluginTtxtsubs::Replaying(const cControl *Control, const char *Name) +{ + StopTtxt(); + StartTtxtPlay(mPage); // XXX should get page in some other way! +} + +void cPluginTtxtsubs::PlayerTeletextData(uint8_t *p, int length) +{ + cTtxtSubsPlayer *r = dynamic_cast<cTtxtSubsPlayer *>(mRec); + + if(!r) { + fprintf(stderr, "ttxtsubs: ERROR: not a cTtxtSubsPlayer!\n"); + return; + } + + r->PES_data(p, length); +} + +cTtxtSubsRecorderBase *cPluginTtxtsubs::NewTtxtSubsRecorder(cDevice *dev, const cChannel *ch) +{ + if(mRecord) + return new cTtxtSubsRecorder(dev, ch, mLanguage, mHearingImpaired); + else + return NULL; +} + + +// -- internal + +void cPluginTtxtsubs::StartTtxtLive(const cDevice *Device, int pid, int page) +{ + //dprint("cPluginTtxtsubs::StartTtxtLive\n"); + + if(!mRec) { + cTtxtSubsLiveReceiver *r; + //dprint("teletext subtitles started on pid %d\n", pid); + mRec = r = new cTtxtSubsLiveReceiver(pid, page); + if(!cDevice::PrimaryDevice()->AttachReceiver(r)) + fprintf(stderr, "ttxtsubs: Error: AttachReceiver failed!\n"); // + } else + fprintf(stderr, "ttxtsubs: Error: StartTtxtLive called when already started!\n"); +} + +void cPluginTtxtsubs::StartTtxtPlay(int backup_page) +{ + //dprint("cPluginTtxtsubs::StartTtxtPlay\n"); + + if(!mRec) { + dprint("ttxtsubs: teletext subtitles replayer started with initial page %03x\n", backup_page); + mRec = new cTtxtSubsPlayer(mLanguage, mHearingImpaired, backup_page); + } else + fprintf(stderr, "ttxtsubs: Error: StartTtxtPlay called when already started!\n"); +} + +void cPluginTtxtsubs::StopTtxt(void) +{ + //dprint("cPluginTtxtsubs::StopTtxt\n"); + + if(mRec) { + HideTtxt(); + delete mRec; + mRec = NULL; + } +} + +void cPluginTtxtsubs::ShowTtxt(void) +{ + if(mRec) + mRec->ShowDisplay(); +} + +void cPluginTtxtsubs::HideTtxt(void) +{ + if(mRec) + mRec->HideDisplay(); +} + +// ----- cMenuSetupTtxtsubs ----- + +cMenuSetupTtxtsubs::cMenuSetupTtxtsubs(cPluginTtxtsubs *ttxtsubs) + : + mTtxtsubs(ttxtsubs), + mLanguageNo(-1), + mHearingImpaired(mTtxtsubs->HearingImpaired()), + mRecord(mTtxtsubs->Record()) +{ + memcpy(mLanguage, ttxtsubs->Language(), 3); + mLanguage[3] = '\0'; + + for(int i = 0; i < gNumLanguages; i++) { + if(!memcmp(mLanguage, gLanguages[i], 3)) { + mLanguageNo = i; + break; + } + } + + if(mLanguageNo >= 0) + Add(new cMenuEditStraItem(tr("Language"), &mLanguageNo, gNumLanguages, gLanguages)); + else + Add(new cMenuEditStrItem(tr("Language"), mLanguage, 4, "abcdefghijklmnopqrstuvwxyz")); + + Add(new cMenuEditBoolItem(tr("Hearing Impaired"), &mHearingImpaired, tr("no"), tr("yes"))); + Add(new cMenuEditBoolItem(tr("Record Subtitles"), &mRecord, tr("no"), tr("yes"))); +} + +void cMenuSetupTtxtsubs::Store(void) +{ + if(mLanguageNo >= 0) { + SetupStore("Language", gLanguages[mLanguageNo]); + mTtxtsubs->SetLanguage(gLanguages[mLanguageNo]); + } else { + SetupStore("Language", mLanguage); + mTtxtsubs->SetLanguage(mLanguage); + } + + SetupStore("HearingImpaired", mHearingImpaired); + mTtxtsubs->SetHearingImpaired(mHearingImpaired); + + SetupStore("Record", mRecord); + mTtxtsubs->SetRecord(mRecord); +} + + +VDRPLUGINCREATOR(cPluginTtxtsubs); // Don't touch this! diff --git a/ttxtsubs.h b/ttxtsubs.h new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ttxtsubs.h @@ -0,0 +1 @@ + diff --git a/ttxtsubsdisplay.c b/ttxtsubsdisplay.c new file mode 100644 index 0000000..370b4c9 --- /dev/null +++ b/ttxtsubsdisplay.c @@ -0,0 +1,331 @@ + +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <sys/time.h> +#include <time.h> + +#include <vdr/osd.h> +#include <vdr/osdbase.h> +#include <vdr/thread.h> + +#include "ttxtsubsdisplay.h" +#include "utils.h" + +// pageState +enum { + invalid = 0, + collecting, + interimshow, + finished +}; + + +cTtxtSubsDisplay::cTtxtSubsDisplay(void) + : + mPageState(invalid), + mMag(0), + mNo(0), + mDoDisplay(0), + mOsd(NULL), + mOsdLock(), + mLastDataTime(NULL) +{ + memset(&page.data, 0, sizeof(page.data)); + mLastDataTime = (struct timeval *) calloc(1, sizeof(*mLastDataTime)); +} + + +cTtxtSubsDisplay::~cTtxtSubsDisplay(void) +{ + if(mLastDataTime) + free(mLastDataTime); + ClearOSD(); +} + + +void cTtxtSubsDisplay::SetPage(int Pageno) +{ + Clear(); + + mMag = (Pageno >> 8) & 0xF; + mNo = Pageno & 0xFF; +} + + +void cTtxtSubsDisplay::Hide(void) +{ + //dprint("cTtxtSubsDisplay::Hide\n"); + mDoDisplay = 0; + ClearOSD(); +} + + +void cTtxtSubsDisplay::Show(void) +{ + //dprint("cTtxtSubsDisplay::Show\n"); + mDoDisplay = 1; + ShowOSD(); +} + + +void cTtxtSubsDisplay::Clear(void) +{ + mPageState = invalid; + ClearOSD(); +} + + +void cTtxtSubsDisplay::TtxtData(const uint8_t *Data) +{ + int mp; + int mag; // X in ETSI EN 300 706 + int packet; // Y + struct ttxt_data_field *d; + + // idle time - if we have been sitting on a half finished page for a while - show it + if(Data == NULL && mPageState == collecting) { + struct timeval tv; + gettimeofday(&tv, NULL); + // add second diff to usecs + tv.tv_usec += 1000000 * (tv.tv_sec - mLastDataTime->tv_sec); + + if((tv.tv_usec - mLastDataTime->tv_usec) > 500000) { + //dprint("Interimshow!\n"); + mPageState = interimshow; + ClearOSD(); + ShowOSD(); + } + } + + if(Data == NULL) + return; + + //print_line((char *) Data); + + d = (struct ttxt_data_field *) Data; + + mp = unham(invtab[d->mag_addr_ham[0]], invtab[d->mag_addr_ham[1]]); + mag = mp & 0x7; + packet = (mp >> 3) & 0x1f; + + //dprint("cTtxtSubsDisplay::TtxtData: mag:%d packet: %d\n", mag, packet); // XXX + + if(packet == 0) { + int i, no; + uint8_t fi[8]; // flags field inverted + + for(i = 0; i < 8; i++) + fi[i] = invtab[d->data[i]]; + + if(mPageState == collecting) { + mPageState = finished; + ClearOSD(); + ShowOSD(); + } + if(mPageState == interimshow) + mPageState = finished; + + no = unham(fi[0], fi[1]); + + //dprint("cTtxtSubsDisplay::TtxtData: page:%d%02x, packet: %d\n", mag, no, packet); // XXX + //print_line((char *) Data); + + if(mag == mMag && no == mNo) { + //dprint("cTtxtSubsDisplay::TtxtData: page:%d%02x, packet: %d\n", mag, no, packet); // XXX + page.mag = mag; + page.no = no; + page.flags = 0; + page.national_charset = 0; + + if(fi[3] & 0x80) { // Newsflash + page.flags |= erasepage; + memset(&page.data, 0, sizeof(page.data)); // only if erasepage is set? + } + if(fi[5] & 0x20) // Subtitle + page.flags |= newsflash; + if(fi[5] & 0x80) // Suppress Header + page.flags |= subtitle; + // if(fi[6] & 0x08) // Update Indicator + // if(fi[6] & 0x20) // Interrupted Sequence + if(fi[6] & 0x80) // Inhibit Display + page.flags |= inhibit_display; + // if(fi[7] & 0x02) // Magazine Serial + + page.national_charset = ((fi[7] & 0x80) >> 5) + + ((fi[7] & 0x20) >> 4) + ((fi[7] & 0x08) >> 3); + + mPageState = collecting; + gettimeofday(mLastDataTime, NULL); + + for(i = 0; i < 32; i++) + page.data[0][i] = invtab[d->data[i+8]]; + } + } else if(mag == page.mag && packet < TTXT_DISPLAYABLE_ROWS && + (mPageState == collecting || mPageState == interimshow)) { + // mag == page.mag: The magazines can be sent interleaved + int i; + for(i = 0; i < 40; i++) + page.data[packet][i] = invtab[d->data[i]]; + + mPageState = collecting; + gettimeofday(mLastDataTime, NULL); + + //dprint("row: %d ", packet); + //dump_hex("", page.data[packet], 40); + } else { + // packets with national characters information: X/28/0 format 1, X/28/1, X/28/4, M/29/0 M/29/4, + if(packet == 28 || packet == 29) + dprint("mag: %d, packet: %d, page: %02x, state: %d\n", page.mag, packet, page.no, mPageState); + + //if(packet == 26) { + // dprint("mag: %d, packet: %d, page: %02x, state: %d\n", page.mag, packet, page.no, mPageState); + // dump_hex("", page.data[packet], 40); + //} + } +} + + +static char * +ttxt2la1(uint8_t *p, char *buf, int natopts) +{ + int i, j = 0, skip = 1; + for(i = 0; i < 40; i++) { + uint8_t c = p[i] & 0x7f; + + if(c == 0x0b) { // box begin + skip = 0; + if(j != 0) // if we have chars in buf + buf[j++] = ' '; + continue; + } + + if(c == 0x0a) { // box end + skip = 1; + continue; + } + + if(skip) + continue; + + if(j == 0 && c == 20) // skip leading spaces + continue; + + if(c >= 0x20) { + buf[j++] = ttxt_laG0_la1_char(0, natopts, c); + } + } + + while(j > 0 && buf[j-1] == ' ') // strip extra spaces + j--; + + buf[j++] = '\0'; + if(strlen(buf)) + return buf; + else + return NULL; +} + +#define SCREENLEFT 125 +#define SCREENBOT 540 +#if 0 +#define ROWINCR 45 +#define ROWH 36 +#define TEXTY 4 +#else +#define ROWINCR 43 +#define ROWH 34 +#define TEXTY 3 +#endif +#define TEXTX 15 +#define SCREENTOP (SCREENBOT - MAXTTXTROWS * ROWINCR) + +void cTtxtSubsDisplay::ShowOSD(void) +{ + int i, y; + int rowcount = 0; + char buf[TTXT_DISPLAYABLE_ROWS][41]; + int doneWidthWorkaround = 0; + + cMutexLock lock(&mOsdLock); + + if(!mDoDisplay) { + //dprint("NOT displaying subtitles because of other OSD activities!\n"); + return; + } + + if(mPageState != interimshow && mPageState != finished) { + //dprint("NOT displaying subtitles because page state: %d!\n", mPageState); + return; + } + + if(mOsd != NULL) + ClearOSD(); + + //print_page(&page); + + for(i = 1; i < 24; i++) { + if(page.data[i][0] != 0) // anything on this line? + if(ttxt2la1(page.data[i], buf[rowcount], page.national_charset)) + rowcount++; + } + +#if 0 + mOsd = cOsd::OpenRaw(SCREENLEFT, SCREENTOP); +#else + mOsd = cDevice::PrimaryDevice()->NewOsd(SCREENLEFT, SCREENTOP); +#endif + if(mOsd == NULL) { + dprint("Error: cOsd::OpenRaw returned NULL!\n"); + return; + } + + mOsd->SetFont(fontOsd); + + if(rowcount > MAXTTXTROWS) + rowcount = MAXTTXTROWS; + + y = SCREENBOT - SCREENTOP - ROWH - (ROWINCR * (rowcount-1)); + for(i = 0; i < rowcount; i++) { + tWindowHandle wind; + int w = 0; // XXX should be text width - not %4! + + // XXX Width calculations doesn't work before we have created a window... + if(!doneWidthWorkaround) { + wind = mOsd->Create(0, y, 4, ROWH, 2); + w = mOsd->Width(buf[i]) + 2 * TEXTX; + mOsd->Clear(wind); + mOsd->Hide(wind); + doneWidthWorkaround = 1; + } else + w = mOsd->Width(buf[i]) + 2 * TEXTX; + + if(w % 4) + w += 4 - (w % 4); + + wind = mOsd->Create(0, y, w, ROWH, 2); + //dprint("W: %d\n", w); + mOsd->Fill(0, y, w, y + ROWH, clrBackground, wind); + mOsd->Text(TEXTX, y + TEXTY, buf[i], clrWhite, clrBackground, wind); + + y += ROWINCR; + } + + mOsd->Flush(); +} + + +void cTtxtSubsDisplay::ClearOSD(void) +{ + // dprint("\nClearOSD!\n"); + cMutexLock lock(&mOsdLock); + + if(mOsd) { + + mOsd->Clear(ALL_WINDOWS); + mOsd->Hide(ALL_WINDOWS); + mOsd->Flush(); + delete mOsd; + mOsd = NULL; + } +} diff --git a/ttxtsubsdisplay.h b/ttxtsubsdisplay.h new file mode 100644 index 0000000..42905ff --- /dev/null +++ b/ttxtsubsdisplay.h @@ -0,0 +1,35 @@ + +#include "teletext.h" + +struct ttxt_data_field; +struct timeval; + +class cOsdBase; + +#define MAXTTXTROWS 5 + +class cTtxtSubsDisplay { + public: + cTtxtSubsDisplay(void); + ~cTtxtSubsDisplay(void); + + void SetPage(int Pageno); + void Hide(void); + void Show(void); + void Clear(void); + void TtxtData(const uint8_t *); + + protected: + void ShowOSD(); + void ClearOSD(void); + + private: + int mPageState; + int mMag; + int mNo; + int mDoDisplay; + struct ttxt_page page; + cOsdBase *mOsd; + cMutex mOsdLock; + struct timeval *mLastDataTime; +}; diff --git a/ttxtsubsdisplayer.c b/ttxtsubsdisplayer.c new file mode 100644 index 0000000..6983cd2 --- /dev/null +++ b/ttxtsubsdisplayer.c @@ -0,0 +1,256 @@ + +#include "ttxtsubsdisplayer.h" +#include "ttxtsubsdisplay.h" +#include "utils.h" +#include "ttxtsubs.h" +#include "siinfo.h" + +// ----- class cTtxtSubsDisplayer ----- + +cTtxtSubsDisplayer::cTtxtSubsDisplayer(int textpage) + : + mDisp(NULL), + mGetMutex(), + mGetCond(), + mRingBuf(18800, true), + mRun(0) +{ + mDisp = new cTtxtSubsDisplay(); + mDisp->SetPage(textpage); + ShowDisplay(); + + mRun = 1; + this->Start(); // start thread +} + +cTtxtSubsDisplayer::~cTtxtSubsDisplayer(void) +{ + // stop thread + mRun = false; + mGetCond.Broadcast(); + this->Cancel(5); + + if(mDisp) { + delete mDisp; + mDisp = NULL; + } +} + +// handled messages in ring buffer - runs in a thread of its own +void cTtxtSubsDisplayer::Action(void) +{ + while(mRun) { + const cFrame *f; + + f = mRingBuf.Get(); + + if(f) { + mDisp->TtxtData(f->Data()); + mRingBuf.Drop(f); + } else { + // wait for more data + mGetMutex.Lock(); + mGetCond.TimedWait(mGetMutex, 500); + mDisp->TtxtData(NULL); // give display some idle time + mGetMutex.Unlock(); + } + } +} + +void cTtxtSubsDisplayer::ShowDisplay(void) +{ + mDisp->Show(); +} + +void cTtxtSubsDisplayer::HideDisplay(void) +{ + mDisp->Hide(); +} + + +// ----- class cTtxtSubsLiveReceiver ----- + +cTtxtSubsLiveReceiver::cTtxtSubsLiveReceiver(int Pid, int textpage) + : + cReceiver(0, -1, 1, Pid), + cTtxtSubsDisplayer(textpage) +{ +} + +void cTtxtSubsLiveReceiver::Activate(bool On) +{ + //dprint("cTtxtSubsLiveReceiver::Activate: On:%d\n", On); +} + +// Take TS packets and break out the teletext data +// Buffer the data for processing in a separate thread +// XXX We should do some filtering here to avoid unneccessary load! +void cTtxtSubsLiveReceiver::Receive(uchar *Data, int Length) +{ + int i; + + if(Length != 188) // should never happen + return; + + if(Data[1] & 0x80) // transport_error_indicator + return; + + // payload_unit_start_indicator + for(i = (Data[1] & 0x40) ? 1 : 0; i < 4; i++) { + if(0xff == Data[4 + i*46]) // stuffing data + continue; + + cFrame *f = new cFrame(Data + 4 + i*46, 46); + mRingBuf.Put(f); + mGetCond.Broadcast(); + } +} + +// ----- class cTtxtSubsPlayer ----- + +cTtxtSubsPlayer::cTtxtSubsPlayer(char *lang, int HI, int backup_textpage) + : + cTtxtSubsDisplayer(backup_textpage), + mHearingImpaired(HI), + mHasFilteredStream(0), + mFoundLangPage(0), + mLangInfoState(0) +{ + memcpy(mLanguage, lang, 3); + mLanguage[3] = '\0'; +} + +// Take PES packets and break out the teletext data +// Buffer the data for processing in a separate thread +// XXX We should do some filtering here to avoid unneccessary load! +void cTtxtSubsPlayer::PES_data(uchar *p, int Length) +{ + int i; + + //printf("cTtxtSubsPlayer: len: %d\n", Length); // XXX + + if(Length < 46 || p[0] != 0 || p[1] != 0 || p[2] != 1 || p[3] != 0xbd || p[8] != 0x24 || + p[45] < 0x10 || p[45] >= 0x20) { + fprintf(stderr, "cTtxtSubsPlayer::PES_data: bad indata!\n"); + } + + // Recorded teletext typically has payload type 0x10. + // We use the payload type 0x1f to indicate that we have our own + // filtered stream, if we find that we only use that. + + if(p[45] == 0x1f && !mHasFilteredStream) + mHasFilteredStream = 1; + + if(mHasFilteredStream && p[45] != 0x1f) + return; // other teletext data, ignore + + if(mHasFilteredStream && !mFoundLangPage) + SearchLanguagePage(p, Length); + + // payload_unit_start_indicator + for(i = 1; (i*46) < Length ; i++) { + if(0xff == p[i*46]) // stuffing data + continue; + + cFrame *f = new cFrame(p + i*46, 46); + mRingBuf.Put(f); + mGetCond.Broadcast(); + } +} + +static void copy_inv_strip_par(uint8_t *dest, uint8_t *src, size_t len) +{ + size_t i; + for(i = 0; i < len; i++) + dest[i] = invtab[src[i]] & 0x7f; +} + +void cTtxtSubsPlayer::SearchLanguagePage(uint8_t *p, int len) +{ + char *infoline = "Subtitles Index Page"; + int foundlines = 0; + unsigned int foundNonHIPage = 0; + int foundNonHI = 0; // 1 = found non hearing impaired + + if(len < (3*46)) + return; + + struct ttxt_data_field *d; + for(d = ((struct ttxt_data_field *) p) + 1; ((uint8_t *) d) < (p + len + 45); d++) { + int mp = UNHAM_INV(d->mag_addr_ham[0], d->mag_addr_ham[1]); + //int mag = mp & 0x7; + int packet = (mp >> 3) & 0x1f; + uint8_t buf[40]; + + if(d->data_unit_id != 0x02) + break; // a subtitle line + + switch(foundlines) { + case 0: + if(packet != 0) // need a Y0 first + return; + foundlines++; + break; + case 1: + if(packet != 1) // need a Y1 + return; + copy_inv_strip_par(buf, d->data, sizeof(buf)); + if(memcmp((char *) buf, infoline, strlen(infoline))) + return; + foundlines++; + break; + case 2: + mLangInfoState++; + if(mLangInfoState == 3) + fprintf(stderr, "ttxtsubs: Language \"%c%c%c\" not found in recording, available languages:\n", + mLanguage[0], mLanguage[1], mLanguage[2]); + + if(packet < 2) // need a Y2 or more + return; + copy_inv_strip_par(buf, d->data, sizeof(buf)); + for(size_t i = 0; i < 40; i += 8) { + if(mLangInfoState == 3 && buf[i] >= 'a' && buf[i] <= 'z') + fprintf(stderr, " %c%c%c: %c%c%c %s\n", buf[i+4], buf[i+5], buf[i+6], buf[i], buf[i+1], buf[i+2], + buf[i+3] == ' ' ? "" : buf[i+3] == 'h' ? "(Hearing Impaired)" : "(Unknown type)"); + if(buf[i] == mLanguage[0] && + buf[i+1] == mLanguage[1] && + buf[i+2] == mLanguage[2] && + ((buf[i+3] == ' ') || (buf[i+3] == 'h')) && + buf[i+4] >= '1' && buf[i+4] <= '8' && + buf[i+5] >= '0' && buf[i+5] <= '9' && + buf[i+6] >= '0' && buf[i+6] <= '9' && + buf[i+7] == ' ') { + unsigned int page = + ((buf[i+4] - '0') << 8) + + ((buf[i+5] - '0') << 4) + + (buf[i+6] - '0'); + if(page >= 0x100 && page <= 0x900) { + if(page >= 0x800) + page -= 0x800; + + if(((!mHearingImpaired) && (buf[3+i] == ' ')) || + (mHearingImpaired && (buf[3+i] == 'h'))) { + mDisp->SetPage(page); + mFoundLangPage = 1; + fprintf(stderr, "FOUND subtitle page: %03x\n", page); // XXX + return; + } + if(mHearingImpaired && (buf[3+i] == ' ')) { + foundNonHIPage = page; + foundNonHI = 1; + fprintf(stderr, "FOUND non hi subtitle page, remembering: %03x\n", page); // XXX + } + } + } + } + break; + } + } + + if(foundNonHI) { + mDisp->SetPage(foundNonHIPage); + mFoundLangPage = 1; + fprintf(stderr, "Didn't find HI page, but found right language: %03x\n", foundNonHIPage); // XXX + return; + } +} diff --git a/ttxtsubsdisplayer.h b/ttxtsubsdisplayer.h new file mode 100644 index 0000000..00127ff --- /dev/null +++ b/ttxtsubsdisplayer.h @@ -0,0 +1,47 @@ + +#include <vdr/receiver.h> +#include <vdr/ringbuffer.h> + +class cTtxtSubsDisplay; + +class cTtxtSubsDisplayer : public cThread { + public: + cTtxtSubsDisplayer(int textpage); + ~cTtxtSubsDisplayer(void); + void ShowDisplay(void); + void HideDisplay(void); + + protected: + virtual void Action(void); + + protected: + cTtxtSubsDisplay *mDisp; + cMutex mGetMutex; + cCondVar mGetCond; + cRingBufferFrame mRingBuf; + int mRun; +}; + +class cTtxtSubsLiveReceiver : public cReceiver, public cTtxtSubsDisplayer { + public: + cTtxtSubsLiveReceiver(int Pid, int textpage); + + protected: + virtual void Activate(bool On); + virtual void Receive(uchar *Data, int Length); +}; + +class cTtxtSubsPlayer : public cTtxtSubsDisplayer { + public: + cTtxtSubsPlayer(char *lang, int HI, int backup_textpage); + virtual void PES_data(uchar *Data, int Length); + + private: + void SearchLanguagePage(uint8_t *p, int len); + + char mLanguage[4]; + int mHearingImpaired; + int mHasFilteredStream; + int mFoundLangPage; + int mLangInfoState; +}; diff --git a/ttxtsubsfilter.c b/ttxtsubsfilter.c new file mode 100644 index 0000000..a50a3fb --- /dev/null +++ b/ttxtsubsfilter.c @@ -0,0 +1,158 @@ + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <string.h> + +#include "ttxtsubsfilter.h" +#include "teletext.h" + +cTtxtSubsFilter::cTtxtSubsFilter(void) + : + mCount(0), + mPages(0) +{ + int i; + for(i = 0; i < 8; i++) + mMagPage[i] = -1; +} + + +cTtxtSubsFilter::~cTtxtSubsFilter(void) +{ + FreePages(); +} + + +void cTtxtSubsFilter::SetPages(int count, uint16_t *pages) +{ + FreePages(); + + mPages = (uint16_t *) malloc(count * sizeof(uint16_t)); + + memcpy((char *) mPages, (char *) pages, count * sizeof(uint16_t)); + + mCount = count; + + if(0) { + int i; + fprintf(stderr, "SetPages: %d, ", count); + for(i = 0; i < count; i++) + fprintf(stderr, "%03x ", pages[i]); + fprintf(stderr, "\n"); + } +} + +// XXX IF MERGING PAGES FROM SEVERAL PIDS (which we currently don't) +// Need to insert a line Y0 if +// - there has been data from other streams inserted and the next line is not a Y0 +// there has been data from other magazines and the next line for this mag is not a Y0 +// indata and outdata are pointers to a ttxt data row buffers, +// if return value <> 0 outdata countains data to pass on +int cTtxtSubsFilter::Filter(char *indata, char *outdata) +{ + int mp; + int mag; // X in ETSI EN 300 706 + int packet; // Y + int result = 0; + + struct ttxt_data_field *d = (struct ttxt_data_field *) indata; + + mp = UNHAM_INV(d->mag_addr_ham[0], d->mag_addr_ham[1]); + mag = mp & 0x7; + packet = (mp >> 3) & 0x1f; + + if (0) { // XXX + uint8_t buf[2]; + ham8_4byte(mp, buf); + fprintf(stderr, "unham %02x %02x -> %02x, ham: %02x %02x\n", + invtab[d->mag_addr_ham[0]], invtab[d->mag_addr_ham[1]], + mp, + buf[0], buf[1]); + } + + // packet 0, change page no in this magazine + if(packet == 0) { + uint8_t no; + int newindex; + + no = UNHAM_INV(d->data[0], d->data[1]); + + mMagPage[mag] = newindex = Find(mCount, mPages, (mag << 8) | no); + + if(newindex != -1) { + //fprintf(stderr, "Filter: %x%02x (%d)\n", mag, no, newindex); // XXX + + MakeY0(outdata, indata, mPages[newindex]); + result = 1; + } + } else if(packet >= 1 && packet <= 25) { + if(mMagPage[mag] != -1) { + memcpy(outdata, indata, sizeof(ttxt_data_field)); + result = 1; + } + } + + return result; +} + + +void cTtxtSubsFilter::FreePages(void) +{ + mCount = 0; + + if(mPages) { + free(mPages); + mPages = NULL; + } +} + + +int cTtxtSubsFilter::Find(int count, uint16_t *pages, uint16_t pageno){ + + int i; + for(i = 0; i < count; i++) { + if(pages[i] == pageno) + return i; + } + + return -1; +} + + +// make a new Y0 line with the flags in +void cTtxtSubsFilter::MakeY0(char *outdata, char *indata, uint16_t newpageno) +{ + struct ttxt_data_field *i = (struct ttxt_data_field *) indata; + struct ttxt_data_field *o = (struct ttxt_data_field *) outdata; + uint8_t hambuf[2]; + + // new magazine number (Y = 0) + ham8_4byte((newpageno >> 8) & 0x7, hambuf); + o->mag_addr_ham[0] = invtab[hambuf[0]]; + o->mag_addr_ham[1] = invtab[hambuf[1]]; + + // new page number + ham8_4byte(newpageno & 0xff, hambuf); + o->data[0] = invtab[hambuf[0]]; + o->data[1] = invtab[hambuf[1]]; + + // copy flags + memcpy(o->data + 2, i->data + 2, 6); + + // new text + char txtbuf[32]; + size_t txtlen; + size_t n; + + txtlen = snprintf(txtbuf, 32, "%03x", newpageno < 0x100 ? newpageno + 0x800 : newpageno); + + for(n = 0; n < txtlen; n++) { + if(parity(txtbuf[n])) + o->data[n + 8] = invtab[txtbuf[n] | 0x80]; // XXX wrong parity? + else + o->data[n + 8] = invtab[txtbuf[n]]; // XXX wrong parity? + } + for(; n < 32; n++) + o->data[n + 8] = invtab[' ']; // space already has right parity +} diff --git a/ttxtsubsfilter.h b/ttxtsubsfilter.h new file mode 100644 index 0000000..3e09b20 --- /dev/null +++ b/ttxtsubsfilter.h @@ -0,0 +1,27 @@ + +// ----- cTtxtSubsFilter ----- + +// Filters out the pages on one teletext stream +class cTtxtSubsFilter +{ + public: + cTtxtSubsFilter(void); + virtual ~cTtxtSubsFilter(void); + + void SetPages(int count, uint16_t *pages); + + // indata and outdata are pointers to a ttxt data row buffers, + // if return value <> 0 outdata countains data to pass on + int Filter(char *indata, char *outdata); + + private: + void FreePages(void); + int Find(int count, uint16_t *pages, uint16_t pageno); + void MakeY0(char *outdata, char *indata, uint16_t newpageno); + + int mCount; // number of pages to filter out + uint16_t *mPages; // array of page numbers + int mMagPage[8]; /* index into pages for each mag number, or -1 if + not currently collecting pages for that mag */ +}; + diff --git a/ttxtsubsreceiver.c b/ttxtsubsreceiver.c new file mode 100644 index 0000000..a497fd6 --- /dev/null +++ b/ttxtsubsreceiver.c @@ -0,0 +1,253 @@ + +#include <stdlib.h> +#include <vdr/receiver.h> +#include <vdr/ringbuffer.h> + +#include "siinfo.h" +#include "ttxtsubsfilter.h" +#include "ttxtsubsreceiver.h" +#include "teletext.h" + +#define MAXINDEXPAGELINES 24 + +// ----- cTtxtSubsReceiver ----- + +cTtxtSubsReceiver::cTtxtSubsReceiver(int Ca, struct ttxtpidinfo *PI) + : + cReceiver(Ca, -1, 1, PI->pid), + mGetMutex(), + mGetCond(), + mRingBuf(46 * 500, true), + mPI(*PI), + mIndexPageLines(0), + mIndexPageCol(0), + mIndexPageNo(0) +{ + int count = 0; + uint16_t *pages = (uint16_t *) malloc(sizeof(uint16_t) * mPI.pagecount); + + // find a free page to put the index page on + mIndexPageNo = 0x100; + int again; + do { + again = 0; + for(int i = 0; i < count; i++) { + if(pages[i] == mIndexPageNo) { + mIndexPageNo++; + if((mIndexPageNo & 0x000f) == 0x000a) + mIndexPageNo += 6; + again = 1; + } + } + } while(again); + + // get pages to record + for(int i = 0; i < mPI.pagecount; i++) { + // only subtitle pages + if(mPI.i[i].type != TTXT_SUBTITLE_PAGE && + mPI.i[i].type != TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE) + continue; + + pages[count] = (mPI.i[i].mag << 8) + mPI.i[i].page; + + AddIndexInfo(mPI.i[i].lang, mPI.i[i].type, pages[count]); + + count++; + } + + mFilter.SetPages(count, pages); + EndIndexInfo(); + + free(pages); +} + + +cTtxtSubsReceiver::~cTtxtSubsReceiver() +{ + // get any waiting threads going + mGetCond.Broadcast(); +} + + +// returns pointer buf if there is new data +uint8_t *cTtxtSubsReceiver::Get(uint8_t *buf) +{ + const cFrame *f; + + f = mRingBuf.Get(); + if(f) { + memcpy(buf, f->Data(), 46); + mRingBuf.Drop(f); + // fprintf(stderr, "cTtxtSubsReceiver::Get: returned data!\n"); + return buf; + } else { + return NULL; + } +} + + +void cTtxtSubsReceiver::IndexPage(char ***lines, int *linecount) +{ + static char *lineptrs[MAXINDEXPAGELINES]; + + for(int i = 0; i < mIndexPageLines; i++) + lineptrs[i] = (char *) mIndexPage[i]; + + *linecount = mIndexPageLines; + *lines = lineptrs; +} + + +// wait for more data +void cTtxtSubsReceiver::Wait(void) +{ + mGetMutex.Lock(); + mGetCond.Wait(mGetMutex); + mGetMutex.Unlock(); +} + + +void cTtxtSubsReceiver::Activate(bool On) +{ +} + + +// Take TS packets and break out the teletext data +// Buffer the data for processing in a separate thread +// XXX We should do some filtering here to avoid unneccessary load! +void cTtxtSubsReceiver::Receive(uchar *Data, int Length) +{ + int i; + + if(Length != 188) // should never happen + return; + + if(Data[1] & 0x80) // transport_error_indicator + return; + + // payload_unit_start_indicator + for(i = (Data[1] & 0x40) ? 1 : 0; i < 4; i++) { + char buf[46]; + + if(0xff == Data[4 + i*46]) // stuffing data + continue; + + if(mFilter.Filter((char *) Data + 4 + i*46, buf)) { + // fprintf(stderr, "Forward Packet:\n"); + // print_line((char *) Data + 4 + i*46); + // print_line(buf); + + cFrame *f = new cFrame((uchar *) buf, 46); + mRingBuf.Put(f); + mGetCond.Broadcast(); + } + } +} + +static void init_line(uint8_t *buf, uint8_t Y, uint8_t mag) +{ + struct ttxt_data_field *d = (struct ttxt_data_field *) buf; + uint8_t magbuf[2]; + + d->data_unit_id = 0x02; // EBU Teletxt non-subtitle data + d->data_unit_length = 0x2c; + d->par_loff = 0xc0; // reserved bits + unknown line + d->framing_code = 0xe4; + ham8_4byte((Y << 3) | (mag & 0x7), magbuf); + d->mag_addr_ham[0] = invtab[magbuf[0]]; + d->mag_addr_ham[1] = invtab[magbuf[1]]; +} + +static void copy_inv_par(uint8_t *dest, uint8_t *src, int count) +{ + for(int i = 0; i < count; i++) + if(parity(src[i])) + dest[i] = invtab[src[i] | 0x80]; + else + dest[i] = invtab[src[i]]; +} + +void cTtxtSubsReceiver::AddIndexInfo(char *lang, int type, uint16_t page) +{ + //fprintf(stderr, "AddIndexInfo: %c%c%c/%d/%03x\n", lang[0], lang[1], lang[2], type, page); // XXX + + if(mIndexPageLines == MAXINDEXPAGELINES) { + fprintf(stderr, "cTtxtSubsReceiver::AddIndexInfo: Index page full!\n"); + return; + } + + if(mIndexPageLines == 0) { // make Y0 and info text + struct ttxt_data_field *d = (struct ttxt_data_field *) mIndexPage[0]; + uint8_t buf[40]; + size_t txtlen; + + init_line(mIndexPage[mIndexPageLines], mIndexPageLines, mIndexPageNo >> 8); + + d->data[0] = invtab[ham8_4nibble(mIndexPageNo & 0x000f)]; + d->data[1] = invtab[ham8_4nibble((mIndexPageNo >> 4) & 0x000f)]; + uint8_t ham_inv_zero = invtab[ham8_4nibble(0)]; + d->data[2] = ham_inv_zero; + d->data[3] = ham_inv_zero; + d->data[4] = ham_inv_zero; + d->data[5] = ham_inv_zero; + d->data[6] = ham_inv_zero; + d->data[7] = ham_inv_zero; + + txtlen = snprintf((char *) buf, 32, "%03x", mIndexPageNo); + copy_inv_par(d->data + 8, buf, txtlen); + for(int i = txtlen; i < 32; i++) + d->data[i+8] = invtab[' ']; // space already has correct parity + + //print_line((char *) mIndexPage[mIndexPageLines]); // XXX + + mIndexPageLines++; + + init_line(mIndexPage[mIndexPageLines], mIndexPageLines, mIndexPageNo >> 8); + d = (struct ttxt_data_field *) mIndexPage[mIndexPageLines]; + char *header = "Subtitles Index Page"; + txtlen = strlen(header); + copy_inv_par(d->data, (uint8_t *) header, txtlen); + for(int i = txtlen; i < 40; i++) + d->data[i] = invtab[' ']; // space already has correct parity + + //print_line((char *) mIndexPage[mIndexPageLines]); // XXX + + mIndexPageLines++; + } + + struct ttxt_data_field *d = (struct ttxt_data_field *) mIndexPage[mIndexPageLines]; + uint8_t buf[10]; + + if(mIndexPageCol == 0) { + init_line(mIndexPage[mIndexPageLines], mIndexPageLines, mIndexPageNo >> 8); + for(int i = 0; i < 40; i++) + d->data[i+8] = invtab[' ']; // space already has correct parity + } + + if(page < 0x100) + page += 0x800; + + snprintf((char *) buf, 10, "%c%c%c%c%03x ", lang[0], lang[1], lang[2], + type == TTXT_SUBTITLE_PAGE ? ' ' : + type == TTXT_SUBTITLE_HEARING_IMPAIRED_PAGE ? 'h' : 'x', + page); + + copy_inv_par(d->data+ mIndexPageCol*8, buf, 8); + + //print_line((char *) mIndexPage[mIndexPageLines]); // XXX + + mIndexPageCol++; + + if(mIndexPageCol == 5) { + mIndexPageCol = 0; + mIndexPageLines++; + } +} + +void cTtxtSubsReceiver::EndIndexInfo(void) +{ + if(mIndexPageCol != 0) { + mIndexPageCol = 0; + mIndexPageLines++; + } +} diff --git a/ttxtsubsreceiver.h b/ttxtsubsreceiver.h new file mode 100644 index 0000000..a88aa65 --- /dev/null +++ b/ttxtsubsreceiver.h @@ -0,0 +1,34 @@ + +class cRingBufferFrame; + +class cTtxtSubsReceiver : public cReceiver +{ + public: + cTtxtSubsReceiver(int Ca, struct ttxtpidinfo *PI); + virtual ~cTtxtSubsReceiver(); + + // returns pointer buf if there is new data + uint8_t *Get(uint8_t *buf); + // wait for more data + void Wait(void); + + void IndexPage(char ***lines, int *linecount); + + protected: + virtual void Activate(bool On); + virtual void Receive(uchar *Data, int Length); + + virtual void AddIndexInfo(char *lang, int type, uint16_t page); + virtual void EndIndexInfo(void); + + private: + cMutex mGetMutex; + cCondVar mGetCond; + cRingBufferFrame mRingBuf; + struct ttxtpidinfo mPI; + cTtxtSubsFilter mFilter; + uint8_t mIndexPage[24][46]; + uint8_t mIndexPageLines; + uint8_t mIndexPageCol; + uint16_t mIndexPageNo; +}; diff --git a/ttxtsubsrecorder.c b/ttxtsubsrecorder.c new file mode 100644 index 0000000..b6719af --- /dev/null +++ b/ttxtsubsrecorder.c @@ -0,0 +1,167 @@ + +#include <stdint.h> +#include <stdlib.h> + +#include <vdr/vdrttxtsubshooks.h> +#include <vdr/device.h> +#include <vdr/channels.h> +#include <vdr/receiver.h> +#include <vdr/ringbuffer.h> + +#include "siinfo.h" +#include "ttxtsubsfilter.h" +#include "ttxtsubsrecorder.h" +#include "ttxtsubsreceiver.h" +#include "ttxtsubs.h" +#include "teletext.h" + +#define MAXPACKETSIZE 20000 + +// ----- cTtxtSubsRecorder ----- + +cTtxtSubsRecorder::cTtxtSubsRecorder(cDevice *dev, const cChannel *ch, char *lang, int HI) + : + mDev(dev), + mSid(ch->Sid()), + mVid(ch->Vpid()), + mTtxtinfo(NULL), + mReceiver(NULL), + mPacketBuffer(NULL) +{ + mTtxtinfo = (struct ttxtinfo *) malloc(sizeof(*mTtxtinfo)); + struct ttxtpidinfo *pi = NULL; + int pid, page; + + if(GetTtxtInfo(dev->DeviceNumber(), ch->Sid(), ch->Vpid(), mTtxtinfo)) { + fprintf(stderr, "cTtxtSubsRecorder::cTtxtSubsRecorder: GetTtxtSubtitleInfo error!\n"); + } else { + pi = FindSubs(mTtxtinfo, lang, HI, &pid, &page); + + if(!pi && mTtxtinfo->pidcount > 0) { + pi = &(mTtxtinfo->p[0]); + fprintf(stderr, "Selected language not found, just recording first teletext pid found.\n"); + } + + if(pi) { + mReceiver = new cTtxtSubsReceiver(ch->Ca(), pi); + mPacketBuffer = (uint8_t *) malloc(MAXPACKETSIZE); + } else { + fprintf(stderr, "No teletext pid found, not recording any (obviously).\n"); + } + } +} + +cTtxtSubsRecorder::~cTtxtSubsRecorder(void) +{ + if(mReceiver) { + delete mReceiver; + mReceiver = NULL; + } + + if(mTtxtinfo) { + FreeTtxtInfoData(mTtxtinfo); + free(mTtxtinfo); + } + + if(mPacketBuffer) + free(mPacketBuffer); +} + +uint8_t *cTtxtSubsRecorder::GetPacket(uint8_t **outbuf, size_t *lenp) +{ + int done = 0; + size_t len; + uint8_t *b = (uint8_t *) mPacketBuffer; + uint8_t line[46]; + + *outbuf = NULL; + + if(!mReceiver) + return *outbuf; + + len = 46; // skip PES header area + + if(mReceiver->Get(line)) { + // if first line is a Y0, insert an index page + // XXX This isn't really correct, we don't know if there is parallel magazine + // transmission and a page in the index pages' mag is in progress... + + int mp; + //int mag; // X in ETSI EN 300 706 + int packet; // Y + + struct ttxt_data_field *d = (struct ttxt_data_field *) line; + + mp = UNHAM_INV(d->mag_addr_ham[0], d->mag_addr_ham[1]); + //mag = mp & 0x7; + packet = (mp >> 3) & 0x1f; + + if(packet == 0) { + char **iplines; + int iplcount; + mReceiver->IndexPage(&iplines, &iplcount); + for(int i = 0; i < iplcount; i++) { + memcpy(b + len, (char *) iplines[i], 46); + len += 46; + } + } + + // insert the line + memcpy(b + len, line, 46); + len += 46; + } + + // get the rest of the lines + while((len < (MAXPACKETSIZE - 184)) && !done) { + if(mReceiver->Get(b + len)) { + len += 46; + } else + done = 1; + } + + if(len > 46) { // we have data, make a PES packet according to ETSI EN 300 472 + // fill out packet with stuffing data to 184 byte boundary + size_t stufflen = 184 - (len % 184); + if(stufflen == 184) + stufflen = 0; + memset((char *) b + len, 0xff, stufflen); + + len += stufflen; + + // fill in header + uint8_t header[] = {0x00, 0x00, 0x01, 0xbd, // private data stream 1 + 0x00, 0x00, // len + 0x80, // 10 and flags + 0x00, // PTS_DTS_flags and other flags + 0x24}; // PES_header_data_length + + memcpy((char *) b, (char *) header, sizeof(header)); + b[4] = (len-6) >> 8; + b[5] = (len-6) & 0xff; + memset((char *) b + sizeof(header), 0xff, 45 - sizeof(header)); // add stuffing bytes + b[45] = 0x1f; // EBU data, our payload type to indicate we have filtered data + + *outbuf = b; + *lenp = len; + } + +#if 0 + if(*outbuf) { // XXX + fprintf(stderr, "cTtxtSubsRecorder::GetPacket: len: %d\n", len); + for(size_t i = 46; i < len; i +=46) { + struct ttxt_data_field *d = (struct ttxt_data_field *) b + i; + if(d->data_unit_id != 0xff) + print_line((char *) b + i); + } + } +#endif + + return *outbuf; +} + +void cTtxtSubsRecorder::DeviceAttach(void) +{ + if(mReceiver) + mDev->AttachReceiver(mReceiver); +} + diff --git a/ttxtsubsrecorder.h b/ttxtsubsrecorder.h new file mode 100644 index 0000000..bb9182a --- /dev/null +++ b/ttxtsubsrecorder.h @@ -0,0 +1,23 @@ + +class cDevice; +class cChannel; +class cTtxtSubsReceiver; +struct ttxtinfo; + +class cTtxtSubsRecorder : public cTtxtSubsRecorderBase { + public: + cTtxtSubsRecorder(cDevice *dev, const cChannel *ch, char *lang, int HI); + virtual ~cTtxtSubsRecorder(); + + // returns a PES packet if there is data to add to the recording + virtual uint8_t *GetPacket(uint8_t **buf, size_t *len); + virtual void DeviceAttach(void); + + private: + cDevice *mDev; + int mSid; + int mVid; + ttxtinfo *mTtxtinfo; + cTtxtSubsReceiver *mReceiver; + uint8_t *mPacketBuffer; +}; @@ -0,0 +1,2 @@ + +#define dprint(format, args...) fprintf(stderr, format, ## args) diff --git a/vdrttxtsubshooks.c b/vdrttxtsubshooks.c new file mode 100644 index 0000000..4c1836b --- /dev/null +++ b/vdrttxtsubshooks.c @@ -0,0 +1,44 @@ + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> + +#include "vdrttxtsubshooks.h" + +// XXX Really should be a list... +static cVDRTtxtsubsHookListener *gListener; + +// ------ class cVDRTtxtsubsHookProxy ------ + +class cVDRTtxtsubsHookProxy : public cVDRTtxtsubsHookListener +{ + public: + virtual void HideOSD(void) { if(gListener) gListener->HideOSD(); }; + virtual void ShowOSD(void) { if(gListener) gListener->ShowOSD(); }; + virtual void PlayerTeletextData(uint8_t *p, int length) + { if(gListener) gListener->PlayerTeletextData(p, length); }; + virtual cTtxtSubsRecorderBase *NewTtxtSubsRecorder(cDevice *dev, const cChannel *ch) + { if(gListener) return gListener->NewTtxtSubsRecorder(dev, ch); else return NULL; }; +}; + + +// ------ class cVDRTtxtsubsHookListener ------ + +cVDRTtxtsubsHookListener::~cVDRTtxtsubsHookListener() +{ + gListener = 0; +} + +void cVDRTtxtsubsHookListener::HookAttach(void) +{ + gListener = this; + //printf("cVDRTtxtsubsHookListener::HookAttach\n"); +} + +static cVDRTtxtsubsHookProxy gProxy; + +cVDRTtxtsubsHookListener *cVDRTtxtsubsHookListener::Hook(void) +{ + return &gProxy; +} + diff --git a/vdrttxtsubshooks.h b/vdrttxtsubshooks.h new file mode 100644 index 0000000..b2a1dcd --- /dev/null +++ b/vdrttxtsubshooks.h @@ -0,0 +1,36 @@ + +#ifndef __VDRTTXTSUBSHOOKS_H +#define __VDRTTXTSUBSHOOKS_H + +class cDevice; +class cChannel; + +#define VDRTTXTSUBSHOOKS + +class cTtxtSubsRecorderBase { + public: + virtual ~cTtxtSubsRecorderBase() {}; + + // returns a PES packet if there is data to add to the recording + virtual uint8_t *GetPacket(uint8_t **buf, size_t *len) { return NULL; }; + virtual void DeviceAttach(void) {}; +}; + +class cVDRTtxtsubsHookListener { + public: + cVDRTtxtsubsHookListener(void) {}; + virtual ~cVDRTtxtsubsHookListener(); + + void HookAttach(void); + + virtual void HideOSD(void) {}; + virtual void ShowOSD(void) {}; + virtual void PlayerTeletextData(uint8_t *p, int length) {}; + virtual cTtxtSubsRecorderBase *NewTtxtSubsRecorder(cDevice *dev, const cChannel *ch) + { return NULL; }; + + // used by VDR to call hook listeners + static cVDRTtxtsubsHookListener *Hook(void); +}; + +#endif __VDRTTXTSUBSHOOKS_H |