pax_global_header00006660000000000000000000000064140053376030014513gustar00rootroot0000000000000052 comment=b2362c3b60991f0ed1e6493fce7de3ee2e74c8cb gmrender-resurrect-0.0.9/000077500000000000000000000000001400533760300153405ustar00rootroot00000000000000gmrender-resurrect-0.0.9/.gitignore000066400000000000000000000002631400533760300173310ustar00rootroot00000000000000*.o *~ aclocal.m4 autom4te.cache config config.h config.h.in config.log config.status configure INSTALL Makefile Makefile.in src/.deps src/git-version.h src/gmediarender stamp-h1 gmrender-resurrect-0.0.9/AUTHORS000066400000000000000000000003341400533760300164100ustar00rootroot00000000000000Author ====== Henner Zeller - Current maintainer (2012-..) Ivo Clarysse - original author (2005-2007) Contributors: ------------- David Siorpaes Contributed a patch to compile gmediarender against gstreamer-0.10 gmrender-resurrect-0.0.9/COPYING000066400000000000000000000431051400533760300163760ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 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. Copyright (C) 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 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. , 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. gmrender-resurrect-0.0.9/ChangeLog000066400000000000000000000000001400533760300171000ustar00rootroot00000000000000gmrender-resurrect-0.0.9/INSTALL.md000066400000000000000000000151331400533760300167730ustar00rootroot00000000000000# Installing On a typical Ubuntu or Debian system, you need tools to be able to bootstrap the compilation configuration: sudo apt-get install build-essential autoconf automake libtool pkg-config .. and the libraries needed for gmrender, most notably gstreamer. ``` sudo apt-get update sudo aptitude install libupnp-dev libgstreamer1.0-dev \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ gstreamer1.0-libav ``` (The code will also compile with the older 0.10 of gstreamer) Then pulseaudio or alsa depending on what output you prefer (personally, I use alsa) sudo aptitude install gstreamer1.0-alsa sudo aptitude install gstreamer1.0-pulseaudio Get the source. If this is your first time using git, you first need to install it: sudo apt-get install git .. Then check out the source: git clone https://github.com/hzeller/gmrender-resurrect.git Then configure and build cd gmrender-resurrect ./autogen.sh ./configure make You then can run gmrender directly from here if you want. The `-f` option provides the name under which the UPnP renderer advertises: ./src/gmediarender -f "My Renderer" .. to install, run sudo make install The final binary is in `/usr/local/bin/gmediarender` (unless you changed the PREFIX in the configure step). # Running ## Init Script There is a sample init script in `scripts/init.d/gmediarenderer` that could be a good start if you install things on your system. A systemd unit file can be found in `dist-scripts/debian/gmrender-resurrect.service`. (To Linux distribution packagers: please let me know if you have some common changes that might be useful to have in upstream; other than that, just do what makes most sense in your distribution) ## Commandline Options If you write your own init script for your gmediarender, then the following options are particularly useful. ### -f, --friendly-name Friendly name to advertise. Usually, you want your renderer show up in your controller under a nice name. This is the option to set that name. ### -u, --uuid UUID to advertise. Usually, gmediarender comes with a built-in static id, that is advertised and used by controllers to distinguish different renderers. If you have multiple renderers running in your network, they will all share the same static ID. With this option, you can give each renderer its own id. Best way is to create a UUID once by running the `uuidgen` tool: $ sudo apt-get install uuid-runtime $ uuidgen a07e8dfe-26a4-11e2-9dd1-5404a632c90e You take different generated numbers and hard-code it in each script starting an instance of gmediarender (In my init script, I just generate some stable value based on the ethernet MAC address; see "Init Script" below). Also, you can do this already at compile time, when running configure ./configure CPPFLAGS="-DGMRENDER_UUID='\"`uuidgen`\"'" ### --gstout-audiosink and --gstout-audiodevice You can set the audio sink and audio device with these commandline options. Say, you want to use an ALSA device. You can see the available devices with `aplay -L`. The main ALSA device is typically called `sysdefault`, so this is how you select it on the command line: gmediarender --gstout-audiosink=alsasink --gstout-audiodevice=sysdefault The options are described with gmediarender --help-gstout There are other ways to configure the default gstreamer output devices via some global system settings, but in particular if you are on some embedded device, setting these directly via a commandline option is the very best. ### --gstout-initial-volume-db This sets the initial volume on startup in decibel. The level 0.0 decibel is 'full volume', -20db would show on the UPnP controller as '50%'. In the following table you see the non-linear relationship: [decibel] [level shown in player] 0db 100 # this is the default if option not set -6db 85 -10db 75 -20db 50 -40db 25 -60db 0 So with --gstout-initial-volume-db=-10 the player would show up as being set to #75. Note, as always with the volume level in this renderer, this does not influence the hardware level (e.g. Alsa), but only the internal attenuation. So it is advised to always set the hardware output to 100% by system means. ### Running as daemon If you want to run gmediarender as daemon, the follwing two options are for you: -d, --daemon Run as daemon. -P, --pid-file File the process ID should be written to. ### Misc options --logfile Write a logfile. If you want this on the terminal use --logfile /dev/stdout This can be big over time, so only do it for debugging. In particular when you file a bug, please always attach the output of such a logfile; start gmrender-resurrect in foreground mode (without `-d`) on the commandline and give it a file to log into. Attach that to your bug-report. The following command makes sure to capture all logs from gmediarender and other log entries that might come from gstreamer plugins not using the gmediarender logging, all in one file: src/gmediarender -f "MyRender" --logfile=/tmp/gmrender.log >> /tmp/gmrender.log 2>&1 # Other installation resources ## Raspberry Pi If you're installing gmrender-resurrect on the Raspberry Pi, there have been reports of bad sound quality. For one, the 3.5mm output is very low quality, so don't expect wonders (though it seems that driver changes improved this quality a lot). You can use gmrender-resurrect with Pulseaudio or ALSA (or whatever other output ways gstreamer supports). Personally, I use ALSA as it is the most simple and robust way (Pulseaudio would be a layer on top of ALSA anyway). See flag --gstout-audiosink above how to tell gmediarender to use alsasink. By default, ALSA seems to attempt some re-sampling apparently; A user pointed out that this can be fixed by putting this in your `/etc/asound.conf` ctl.!default { type hw card 0 } Stephen Phillips wrote a comprehensive blog-post about installing gmrender-resurrect on the Raspberry Pi (July 2013): http://blog.scphillips.com/2013/07/playing-music-on-a-raspberry-pi-using-upnp-and-dlna-revisited/ ## Arch Linux There is an Arch package available here https://aur.archlinux.org/packages/gmrender-resurrect-git/ ## Debian Since Debian 8, the official `gmediarender` package uses the gmrender-resurrect code: https://packages.debian.org/stretch/gmediarender ## Nix There is a Nix package available: https://nixos.org/nixos/packages.html?attr=gmrender-resurrect gmrender-resurrect-0.0.9/Makefile.am000066400000000000000000000000541400533760300173730ustar00rootroot00000000000000SUBDIRS = src data EXTRA_DIST = autogen.sh gmrender-resurrect-0.0.9/NEWS000066400000000000000000000051521400533760300160420ustar00rootroot000000000000002021-01-21 Bump version number: GMediaRender 0.0.9 - More fine-grained mime filters - Reduced memory in scan mime list - A bunch of little cleanups. - Use UpnpInit2() as the old has been dropped from libupnp (CVE-2020-12695) This means, there is no --ip-address anymore to bind, but --interface-name instead. 2019-10-20 Bump version number: GMediaRender 0.0.8 - Various small changes over the last years. - Now compiles with old and new libupnp (1.6.x and 1.8.x) 2013-... Ongoing changes in git. 2013-05-18: (there are ongoing changes in git, but these are notable features) - Various state variable handling that should improve robustness and compatibility with various clients: - Improved eventing of state variable changes. - Active eventing of current track position (relative time) - Subscription of a new client syncs all available variables. - Provide a way to write a logfile with --logfile - provide callbacks for user-hooks (scrobbling ? LCD display ?) 2012-10-12: GMediaRender 0.0.7dev (Henner Zeller) - Support get duration and position of current stream. - Support basic commands: o Pause o Seek - When we're finished with the current track, we go back to STOPPED, so that controllers will send us the next song. - Initial code to support SetNextAVTransportURI, but still have to find a controller that supports it :) 2007-11-07: GMediaRender 0.0.6 - Escape evented variables in subscription response - Various code cleanups - New command-line options: --ip-address --uuid --friendly-name --dump-devicedesc --dump-connmgr-scpd --dump-control-scpd --dump-transport-scpd 2007-11-05: GMediaRender 0.0.5 - BUG #16640: Rename inline 'min' function to avoid issues with GCC 4.0.3 - BUG #17887: Update to gstreamer-0.10 Based on a patch contributed by David Siorpaes - Fix compilation with libupnp-1.60, with largefile support (the libupnp branch from http://pupnp.sourceforge.net) - Proper encoding of LastChange event in AVTransport service - Add '--version', '--gstout-audiosink' and '--gstout-videosink' command-line options - Look in datadir for icons - Emit xmlns for XML root elements in device and service descriptors 2006-03-27: GMediaRender 0.0.4 - Fix compilation with libupnp >= 0.3.1 2005-09-02: GMediaRender 0.0.3 - Use autoconf and automake - Generate device and service XML descriptions on the fly 2005-09-01: GMediaRender 0.0.2 - Added binary files copyright and license notices to README - Added copyright and license notice to static XML files 2005-08-31: GMediaRender 0.0.1 - First preliminary release gmrender-resurrect-0.0.9/README000066400000000000000000000061431400533760300162240ustar00rootroot00000000000000GMediaRender ============ Introduction ------------ GMediaRender is a UPnP(tm) media renderer for POSIX(r)-compliant systems, such as Linux(r) or UNIX(r). It implements the server component that provides UPnP controllers a means to render media content (audio, video and images) from a UPnP media server. Copyright --------- GMediaRender is copyright (C) 2005-2007 Ivo Clarysse. Continuation of this project (C) 2012-2013 Henner Zeller. License ------- GMediaRender 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Note that GMediaRender links with libupnp, which is licensed under the terms of a modified BSD license (i.e. the original BSD license without the advertising clause). This license is compatible with the GNU GPL. Note that GMediaRender does not ship with any codecs, and is licensed without exception to the GNU GPL. For more info, contact your lawyer, check out the FSF website, or have a look at: http://gstreamer.freedesktop.org/documentation/licensing.html The source code of this project may contain files from other projects, and files generated by other projects, including: * GMediaServer (http://www.nongnu.org/gmediaserver/) Binary files license -------------------- The files 'grender-128x128.png' and 'grender-64x64.png', included in this package, are Copyright (C) 2005 Ivo Clarysse. Copying and distribution of these files, with or without modification, are permitted in any medium without royalty provided this notice is preserved. Requirements ------------ The following programs are required to build GMediaRender: * GNU C Compiler (gcc), 2.95 or later. The following libraries are required to build and run GMediaRender: * Portable SDK for UPnP Devices (libupnp), 1.6.0 or later, including the latest 1.8.x versions (BSD license). http://pupnp.sourceforge.net/ * GStreamer 1.x (LGPL). https://gstreamer.freedesktop.org/ Note that GMediaRender completely relies on GStreamer for rendering content, so if your GStreamer environment does not support a given media format, GMediaRender won't either. You can test this from the command line: gst-launch-0.10 playbin uri=file:///path/to/file Trademarks ---------- UPnP(tm) is a trademark of the UPnP Implementers Corporation. "POSIX" is a registered trademark of the IEEE in the United States. Linux(r) is the registered trademark of Linus Torvalds in the U.S. and other countries. UNIX(r) is a registered trademark of The Open Group. All other trademarks are the property of their respective owners. gmrender-resurrect-0.0.9/README.md000066400000000000000000000037751400533760300166330ustar00rootroot00000000000000Headless UPnP Renderer ---------------------- I needed a small headless UPnP media renderer for Linux (for small footprint-use in a Raspberry Pi or CuBox), but there was not much available. Found this old project [GMediaRender][orig-project] - but it was incomplete, several basic features missing and the project has been abandoned several years ago. So this is a fork of those sources to resurrect this renderer and add the missing features to be useful (Original sources in [savannah cvs][orig-cvs]). To distinguish this project from the original one, this is called **[gmrender-resurrect](http://github.com/hzeller/gmrender-resurrect)**. After many features added, this is now a very usable, headless UPnP renderer that I would consider **stable**. You don't see many changes in this git - don't worry, this project is not abandoned, but it just works as intended (I use it every day). If you run into problems, please file bugs, write me directly or send a pull request; I am busy but will try to respond. If you have tested gmrender resurrect with your control point, **please add it to the [Compatibility Wiki][compat-wiki] page** (even if there is a problem). At this point, it should work with all media controllers, if not, please file a bug so that we can figure out the issue and I can make it work for you. See [INSTALL.md](./INSTALL.md) how to create a logfile that helps to narrow down things. If you're running this on a Raspberry Pi, you might be interested to connect a little LCD display. Check out the **[upnp-display][]** github project. Installation ------------ For installation instructions, see [INSTALL.md](./INSTALL.md) You can reach me via . [orig-project]: http://gmrender.nongnu.org/ [orig-cvs]:http://cvs.savannah.gnu.org/viewvc/gmrender/?root=gmrender [compat-wiki]: https://github.com/hzeller/gmrender-resurrect/wiki/Comptibility [upnp-display]: https://github.com/hzeller/upnp-display [open-max-support]: https://github.com/hzeller/gmrender-resurrect/issues/33#issuecomment-23859699 gmrender-resurrect-0.0.9/autogen.sh000077500000000000000000000000331400533760300173350ustar00rootroot00000000000000#!/bin/sh autoreconf -vfi gmrender-resurrect-0.0.9/configure.ac000066400000000000000000000056511400533760300176350ustar00rootroot00000000000000AC_PREREQ(2.59) AC_INIT(gmediarender, 0.0.9, https://github.com/hzeller/gmrender-resurrect) AC_CONFIG_AUX_DIR(config) AC_CONFIG_SRCDIR(src/main.c) AC_CONFIG_HEADERS(config.h) AM_INIT_AUTOMAKE([1.8 dist-bzip2 no-dist-gzip check-news]) AC_SYS_LARGEFILE # Checks for programs AC_PROG_CC AC_PROG_CC_STDC AC_PROG_INSTALL AC_PROG_MAKE_SET EXTRA_GCC_DEBUG_CFLAGS="" EXTRA_GCC_DEBUG_CXXFLAGS="" if test -n "$GCC"; then EXTRA_GCC_DEBUG_CFLAGS="$CFLAGS" EXTRA_GCC_DEBUG_CXXFLAGS="$CXXFLAGS" CFLAGS+=" -Wall -Wpointer-arith -Wmissing-prototypes -Wmissing-declarations -Wwrite-strings" CXXFLAGS+=" -Wall -Wpointer-arith" fi AC_CHECK_FUNCS([asprintf]) AC_CHECK_LIB([m],[exp]) # Debugging AC_ARG_ENABLE(debug, [ --enable-debug enable debugging],, enable_debug=no) if test "x$enable_debug" = "xyes"; then CFLAGS="$CFLAGS -g -O0 -Wall -Werror" fi PKG_PROG_PKG_CONFIG PKG_CHECK_MODULES(GLIB, glib-2.0 gthread-2.0, HAVE_GLIB=yes, HAVE_GLIB=no) # This is a bit crude, someone with more configure-fu please fix :) # We want either the new, or if that fails, the old version of gstreamer. # There must be a better way than my attempt of having a nested test. GST_REQS=0.10.1 GSTPLUG_REQS=0.10.1 GST_OLD_MAJORMINOR=0.10 GST_NEW_MAJORMINOR=1.0 AC_ARG_WITH( gstreamer, AC_HELP_STRING([--without-gstreamer],[compile without GStreamer support]), try_gstreamer=$withval, try_gstreamer=yes ) HAVE_GST=no if test x$try_gstreamer = xyes; then dnl check for GStreamer PKG_CHECK_MODULES(GST, gstreamer-$GST_NEW_MAJORMINOR >= $GST_REQS, [ HAVE_GST=yes AC_SUBST(GST_CFLAGS) AC_SUBST(GST_LIBS) ], [ HAVE_GST=no ]) if test x$HAVE_GST = xno; then # check for an old version. PKG_CHECK_MODULES(GST, gstreamer-$GST_OLD_MAJORMINOR >= $GST_REQS, [ HAVE_GST=yes AC_SUBST(GST_CFLAGS) AC_SUBST(GST_LIBS) ], [ HAVE_GST=no ]) fi fi if test x$HAVE_GST = xyes; then AC_DEFINE(HAVE_GST, , [Use GStreamer]) fi AC_SUBST(HAVE_GST) AM_CONDITIONAL(HAVE_GST, test x$HAVE_GST = xyes) LIBUPNP_REQUIRED=1.6.0 AC_ARG_WITH( libupnp, AC_HELP_STRING([--without-libupnp],[compile without libupnp support]), try_libupnp=$withval, try_libupnp=yes ) HAVE_LIBUPNP=no if test x$try_libupnp = xyes; then dnl check for libupnp PKG_CHECK_MODULES(LIBUPNP, libupnp >= $LIBUPNP_REQUIRED, [ HAVE_LIBUPNP=yes AC_SUBST(LIBUPNP_CFLAGS) AC_SUBST(LIBUPNP_LIBS) ], [ HAVE_LIBUPNP=no ]) fi if test x$HAVE_LIBUPNP = xyes; then AC_DEFINE(HAVE_LIBUPNP, , [Use libupnp]) fi AC_SUBST(HAVE_LIBUPNP) # Checks for header files. AC_HEADER_STDC dnl Give error and exit if we don't have any UPnP SDK if test "x$HAVE_LIBUPNP" = "xno"; then if test "x$HAVE_LIBGUPNP" = "xno"; then AC_MSG_ERROR(you need either libupnp or libgupnp) fi fi AC_CONFIG_FILES([Makefile src/Makefile data/Makefile]) AC_OUTPUT gmrender-resurrect-0.0.9/data/000077500000000000000000000000001400533760300162515ustar00rootroot00000000000000gmrender-resurrect-0.0.9/data/Makefile.am000066400000000000000000000001701400533760300203030ustar00rootroot00000000000000stuffdir = $(datadir)/gmediarender stuff_DATA = \ grender-128x128.png \ grender-64x64.png EXTRA_DIST = $(stuff_DATA) gmrender-resurrect-0.0.9/data/grender-128x128.png000066400000000000000000000607111400533760300213450ustar00rootroot00000000000000PNG  IHDR>abKGD pHYs  tIME /4ATtEXtCommentCreated with The GIMPd%n IDATxgdYv>>+|WUW$*(d,II %PH )R.HI@  H;X3;g.ӛ޽L̪] [/2T;9c{gaa @ρK_~d_?HU(Sr;cV)JG 0XJ`;Hg0} :@cF>;u2bR`IIֶ.xAp*B [6D%Ёvf0S3`i=?~({N—!~t&J^%pUXZvw{<6O|LբX#/P]DHhA lY ittހoxAJ5t`v@Wo)Wz߳ >e?~J<={% XԔ~R~q?佷ݻM|Ks؎R6B)!()L pњ5''mCM7F{@kz @Gnux~O*‚e`Juee[JPDJm T`}3h}|x=x{O<cWY:0o}?E\eR #%F:^ #\RW * *|`{gG`q#G@{?YIνrP$P/l7$i>Ons:&v]SLU:=}|O7)ZeӚpq}Z!6Z䰍1 c[l MVJ9BkCO?o3 s _rXx 0(Lh d{Gx|=?sz1f\TY*qlm 'GG|lt|64ro QJl>d2y~} y݇赚gжm 2 LOW)ǂ8(@@uY^^~Wr ihG>~ GSP F$<tOu`3Rm_Df/khttlslݽK9J) 2Kj5$g/A!4M.]ŋA}6~!n e*Jb9 }~KUr"h@0AM`\ }n<\(Nϡ/)/hH)ֺ ?9 [_:RJ22UF @H"TO+y찷&a1#`jjbDǏYXZ @h?sr G(|c aN'-Uoaāa(c7(H ]FÍlt?y}?~w&O/WR)riX/`8`oo-vvv8.]J^.{[_ zm<: t?⺂)EHa0E=~!Z~1,?kK?93C>òQx(x~3d277G^gyyB <1Oo{mwpY>F)2h|3)G^v? zzH OL/}KibP) ݍ_H7#{5R1&ˉ ++d3l؟ z>ptt`0@O>l6,Lv6'''\pef]>'֯.]>²^{ׯTT6 PƠTXho/?!*1oUgAO N ћYR2-pyY.;+++n62LJ `h5[C:#t!LZ2zqhZ<F`1F sU>|C=}E!V>5#5;~>z}fr"O5 v7'o-ioo p3*}|vYS= ~ YF%„k[V9uM6̓@ȳ1f1;;;43. RLmLƥlS6׍Pbnnة s9n1wv8 8:2hOz6[vZM\?ϼ.]٠!h[iJ{GV<~OD"K_9G联rP.l]egjjʉ-YH["[B@%$y"2ٶw'<(CGBN69j_j@ WcOQGe{J)/R;C'>nCh6gw)%\Z"Jfr||)zXdqqjl뗯ɇ]-m[8y+Ce\ {T􏇸%ZA4Cv\!)yL"+3_zn{yy9_,E\JCzŷv988FAgfgffrTvͧw?fy&,|>Y~'P(x7'`v)1Z8~WdAZ eܹj1`C=(pt1Eu:K^ Aj*꺤Ӏ_Mׁt}Z9=aȺ4@)(kkkb`tH$Mo{h4O)E.cnnY* st3q@,:3$h>D)\t)]W>FF2O` %"!:ݑ%}O5dVA!m|Lt8e`xMP4+ѳQNz-/9;!ov… r, 2#h)VKssLU*GGܾ}}zDGT*j5)Ĝa>޽{hf{{˗/Ջܽs'汇3A$VAV6LDZ˲ $N#HW#Ȯ\?@H&(vRYosrTVс^6m姧l6;3^aco䄃vq>??Jcc9{{{cm!zLT;|'hm^z3Rܽ{ *u Uv88m#+`fidȼn!QRbz܍#8{"gb_ s^ZzqOpAp|ra$`,zTbvn bwwyGGGm[i>= C3A&Gse!L^GcooŅ$Dq7i7NZn2EP+L m7O݃md,9TA ~P`|~)t ?{VW*gcKoE\.sss5۲R*[rtx RYfrtvwwZֹBw9fg,,,d=j# ~!ϥPh6d2}}^)C}7^Ų‘i3EP,^ 5#?DDz;X{1r@׀/F(\RtZڏ.abqKJYXxOq''xwǶT*033mz]ݥn[ qREXoE `5P(q=<}_=q.pOсp##+,Eq U!ep`)& M\ oGM1 .OoQuiDZerzS'n999hyޙqefuaUfgg=i)JT*wR>SK!!t:&^ySSSs||85ʕ)N鞞09 St|9n5G!i@fd JU^`6Ow[~ߍ |0BtёJC }9V\KwRD^R`)EasB@VcaaR뺡OO?Pgrtt&F^wF93L6NJB<VWV6 Kb_j:Ce._%Hmbk^nOs~!$ڲJ>Dk p+""ocA^wc+8<իl|tJsM yCzoym%K/DʬoF5| 0I󖢈:zض,m`eYr9.B?~|n#hRaaa/RVq)SG󪘂scq)7G C9>86Wlmyܻw& \]" ob<{ \\XX`_LU~2UYjyW=RJA&$n`6CPHطyR m\4Q.  m{^(] A&  NR%!i5)\P:/|s~rr8=[X*SVe832<{[ f@`Ovvi?/,\dz?E.9cb:Hm۶ rmj*aY^/BUF+ Rʤ3 =>.]$;]fSG'KJL~7v` Γg,^fM߉NkiZ;/r"ŵz4^{RJ_k]nǞɘ89>?qPX:?t:TU<muqѨ-RpayJ8܁QdJ W8Th")gYY]уϳ'dV)drXswTK%0 `A;./Eҋ2/EyFM~ $V rwg7(J*C+ IDAT˲XXXc Nm'ŖvCmzl&Cӡh+tufvvrq)ˬ0/P}©NCO@{e2.|RDV w T*dYT#:[:?׾5?SdBlĔB5rp)ctq7< lC9qGSB-j;WDʷ%Rʎ} ݶmC^G>Ou\fffLFE '''O\}aarZϦb xOy;%hn=;&/rcW3`e, 1 4c k ‚= /DAeYwc1&^ZZJ@WҲ,m,ef2׹uׯߠ^f:j!Y<v۷oo|Ǐ'͏֚n'T,8NrR+ #Clbw͍I6PZ)2% @ aGiɱPf 9yHO)uV3F?! ۠*Ztʄ1yvvz=v +K|IXLC)gQ`0G/oWJ%ķRvͣGw"}PޅSl607Whð#? z@A - |?舋 uz-3#. 5gc *J_RIEi_ 31m(Ǐ1$zi^\.fδ|Ii3yw-׺o&+++ŰYv}:CoH`8 =SeT\zzi3qn377 Ĉd-6md+KbQ,clu % U.,{?|}T?5䁷Z/RjD&: e!p8dffr\2Q^h!`zz[nqѭ&@wg8Q999lQJ%eYHl0nGLMMqF֚>}1SH9JKh5;[+_Jt!#hA4]CQlz]Y&,!ֽS `jy y"(EZ/)@G)hYaX,RBV턫y&7od$V|ٳggRJµkרvH)e[Xm[(Re%S~|tBJ*ssI*F癛;mT"Pν啅(=kѹO)i?DG6E/ ~.)QnX,rip':" XH)})@rb"r!tkP@](ӹy.* #N jyyXbeeWoJe0bEDe[GOR()R()Q*t=$օ fcYF)l!7+,KlwxQ Ef3%CFpFW~@!g?~j `= r8})' 2N }_j5l@pbDY=)] ;lll qn ssڡk[ؑmJaB²2FEDBkM#rq}VTwqq;Z6JW50cMjs 4f/^j o!!Fh0H%nZ'iHGףJp8ߙG|"W\Ap/,Ė1|Gܽ{wr+d2.~?#a?Ron@܁QP*1 <-`ee%\&ûަiT ktb?0EKș%XY'dxV Ɔϻ _:~GVYZd?Dـō>wqP(=Qk\L?oksw}w,-d2+TUz^q%} D@'^?"zrIe T=ܿPfeea,;tARbIT2[@L/u2AG=8d`0Ͻ<~  yrkn,T%~{hTP}9xx _Baf - ;C7opp}ߜ?1ƸykmۖB}'Ox7'Ʃ6n펥}lECo[)HXT,,;֍HiIHݘF@W  _v$0c,^{mlկگr.R`n{F02Z,e4ώ3Qdua7"‹&3l^\, }y^I,VD"R<C?ñI:H ?m{<0L@{_F)lU&D(f;k6RFm7M| ?Rq&UGNESBUAkb;|/Z(pBx(#vیw؎`3<7?,L&#D8l)t<:Fzn$Ș˷CLdoRi2hcQL1 W̆G4 ³g8>>&z-^Nݭ-N7wp/B#-2- !-@ CXD9 tzboNCࣔކ|yV#sR69v;RfGwA RKcj6c]| <證rb2g &n5!xˉo"LEdt:ˉ=zϟsiptG K%V[IK '9$ȕ+XL4VjL^o"'P~ =s4ҿE#*jF0jcDttBCr(&l+!aR:+A$]7|N|;~^ti! c!fS֚ BGۀd$<f }[cIlKXǖI6r\1y @? O59"'ĝQgQB)E~zăck`D1͇L3xq_Iw7:gB1 砬}xxד{X[]% w8~ Q,)"[iP9|/#_XX3 "WAl!l"Y{"帅!@| ,bjj*sveSӁ7[ dv{aTGx " -Cw0vt8Ҳ0l$U\F%eJ%/Rb퇇lp.\ wuS磢;?~tڊ\>OX&j}7g)@ t"%x ؾ @˓q{y@3EŸ%@𶟤a!Ǡkr'x(*tJXcۑE#EHNscVo;M!L}D.-@p\?ٝW[!仑݁um%EEYEw2hTL)'jJ APvjsM13'x&Ysln|Pu&P`DykBL,?dwƍ{!N˗qmC?~_T1&d(%䱎!1B'2+C%\2j1QJp|Eݧׅ{/:X&0(@bNQ P#W!ɐ#\׍݀I/W:;~ƙB %HF`*Rͨc)%<䄿K|0%H Gqt8> œq\IGO:]v y64u}}O R(I C&Iq >M!ssq-lL b v15+`,(H`[oY^z%nݺŭ[X^^؜M :_Ǟ[O`*SĵIrHR;Ijx)A;ovPg<7.!}2kl4ϟ?Pr1ōF" /+0gF[R2avυW\a8RrttߧnS,_7 ]g5U}MFƌ`?tn7i*][YMJĽnGOտ:mr<^GYhӣmDO_T `pBB8 d ęVDPJܜ8ΨBXѨlƭ:3ڻ&N_ܷq}tt}7FEHI&uTqdj+Wo+<*T6V^RG^۩ؒMi%H2($Dą̭/<bɒ V  И??"+/S4@*B2>nX, [!{|"\E`.n`pNrs,ϡa;Xo!ZM}b3^W!8= z]B 4Bo 6K۷8,cT|{0.ݫ {ADRNn޸۷occcR J]q\.xg ./)|x֦(ZANBߪmS6އDMf܆p|rcJ" AG> ث4\04MnYQutQY '}}}dsssiPZݥࠂI ]E..mlnnBUU#B7a#66qQ0M O)<ܠ>˹|B³HM% ;{ű Db:F6[Q]t#%(&c% &%?ҨaY S@%07nVHpKw* .0 VQʕ+࠶3plchhBd7OJ IDATs[eb O?1;;eX,D"Z9\1&'`r ]%8q>(r!>1c6- g\>i\WT~JJ|+iYVUQ0~lw3n,T`ql} n|N@ To۶ 58k*LBUUbT*DM,)r)_/ &ʹÚ@NGU-|;{*P*ߏb~Ḙ8+2OVUZ! _FX,Lz naQ\@ñQHs(:7[ކPᮖ(:$ERɡAݻ)8$Aɤ4JyN,,.ola?Ix9 +_y_>D"I?ۏ f:0k&VJw'a{f3AeTsm 0#ZTT*E' { nO L&i: 2;;ƔYrd2xsȖuDPu]s2@[vNTRK N<>rE ],Gz!\z" sAD5 8jaiUZs'-8`08#_CTZN} <BNH$7\9իW|8&v7pme\t2aʫat^ ߆q-U2pW0 d*'ƛ aöl0ˆS!hZϣR#kS2I[ v/ߺ6вa#Nt:- `ZelֿEދ·G ] iJA.[*e܁m׽@^\~1 g |9i&e[5;|i`E j#XDKh  c@h$@Uz򾓡|ow-`vHeZ̀Ce¶M-?h~[/)|'Ipxw^x[%K}y_G  TQ*Dp:~LW {{zN4ypT* 5mY-7H8o>Z-؞ tmKJYxnsa+4UqoDsPV~ w `Ufn4H깁v${vmb𰢪#KʪxGlߞ5[lSF27-_A?o?e!/P| ?Wj8s,V|2e6po~)d['x%oAdج%4c2\>p133w?zꩶ}{/d2CGzP:!9r8>R =v%b1')b{LEe{NЮnZX,bp` =@hvXK3)D3U}/O8._ׇ{? ;VSB{$\!5p,wM"*ϾH $H2yYR[C±\*5c?`xee׮] !Oe-&}=-+FVZp<*w?~=p`M=. &") ^FEmD23Զm UUI&A.75Fd^2<m+ŢcVW!fJ!!JrtL19DE٬$A:P(ŅfYb11>>N`d~WW/r8vt]GZ BN9N섅tP?0Ν;n}{8y,ܢBa6j^-d6Tm!$Jr"-@p!A>߯(T Svz@p@S5h4X>Wd9szLc(^L055!j5U,DG/6@ Yu^8~{/쯍ʿ/o# SlO_^~e[ 9TEu_hj6~A gJF)^_Hvd~:@9v 'gHRxf1ꫯb||gϞeYH#~ "=cJtL]]]x饗|pO瞃v[;T/`r >2,l(Ivo_l|Y@'Qo!-@/9g===thpw :=BYZ(Z GOcdd/Bh4q|Gx0ql;;;o?QG@?>[n?8_xxK* *&8JGQDHle {g !49}wsD"8~.dY7bzz:d J_8+<";wڈ5=<oK//lmmm'Ë/3g 7u`mm߾ c31EO!b+si !3!w3_tj$Z>$NE |j"9qzUx|m\RSSSx7seܐ`x=F"D:F<CnPJaYJ"r+ӻ?(8}pqW_Y]2~?f>4ODtRit ݇T>SY$h ѡF T[`(r/4靝L}+`]LM6ɑÇ]to҈-c#-ׯ_B277~cJx==K/aʤpKEKEͅ8ο9lmnfO'߇!$ D DFXxz5>3b@\𗄐_RL9PSV@{{v|rrR \.L&CJRB@U!  Gŝ;wpu,..X,h n4FGGquwTU,//w1;; )h,>t"-C$K@C6ۇaD֎cq"zBBʄKTVPiIMyQx${QɷV^*l&[[[$l_4@Z@==x(X_2r9lo[{^@UUid2`ž}0&Jeܸ ڄN(bhx?FFw`4殀b1H,=Eia ?x$U@Q05y=]c ߥLlhM 2 \n`fg2y =lFbh=1>N@҆9{Hd$N (4twww8 a~[:4Mx!Jw߽οӰm !݃18t#H'GӠDtи:"V7{A4bC*+o#j%Ф_ZgJ<<ů*G5;P!63mBkC oonJ< X$FR*{0MM\xIZ9EUd34#݈͠' B@#8QDU@ju$ћICEh:8s.۲7S,(@puQ,EOw vBA!bSyF^B@O:چK2yRTF8v`y+\333~Jt!hN \.oܸ^kqC?d6X& =ˆGH'R&@Eq&k*GTJ1eܘB&^/@-A !58TA~-p80Xm$iJB*H$'HS NEO)v% C9r6/wo4gΝBAm4D_&^tuCǰd LlLLtZ" =j@jD] zC7#ecC0?Fas4j 'QXC\WaH"WQG\Z%Wi:MWs'(6V hG1M088ȶxpYL&ڂ?0w}x"[XXPd^BL}dL,cH0Rq(XA08&&.vw (wrijɠFzQ/[18B"`_Cu#Q.puRC@Eic$ 2(no]G젒B1 ;!?hURK`.ui6W~rs \olBn4iUQYtUUUaBvL?'9'L9خ-CK(՗`Jh0"u`O&`1p8%>4]pM,ձ؊ 4*5CDÁQQGU @ĶR\2~TPA&.h궏.&" txtEcAJJB]4iEQGFy{WMYai}؍MU6( VKrp6q+.p˹.GKXM'Ѡ5څ@ r" Ov7 Dz8"h ?HHlVup<;( BDAE%=niuா)kG=/{2 =/Y% ,@(F n  " IDATT3=E K:!8Ÿ+dP:`k&a)c Zefnq!#p; ,K#í@l@ !b9T)gI6*]yR~B7\c(1zcT7YڵOg4${t1txR6tB%GKW.@<܁eBQnxQww: v)5/``ɴIIENDB`gmrender-resurrect-0.0.9/data/grender-64x64.png000066400000000000000000000200211400533760300211710ustar00rootroot00000000000000PNG  IHDR@@iqIDATx[ Tg- *" *("*"FEEqC\ԸdNY:I{zҧ{LtH՝wd4LSPUTw[{6lh?]0. py޻Og)՜ƜF>Z}Taޖ&ԝ3!\w47ZuLYfi|1wv@1 K@`Y=|aG#<_@QS \iCՙH-X Whf^} t'6\-ȹԆwDoȔ]Bp9̨:qQد0  د~7Uwǚp"O3H^pt@ za% =׊ͭ=]~[R+i7`}DlU6Sbo:thػC;[sPx uG*]Чhpf 2ۛ8'Xw.2IJVSW2\BāYUob$Eknz 2kvi79j& tj8h(6}z_hx>?Uo7w/ĊmHolUT )Z.-{.WlK/4~}n6t){Vpuuܹs (5kƎg^px*?GC|l|&Ϡ\逻b_&[lXd/]Oo ᴳ +!Q9r$zGN9dѢda.7z֔~93䝭*gx#b6  :+x ،m]z@*V(v[K1J#U͛'` l.]зo_5 ӦMS!OII3c⣑c2loC6̀*{L)6X3++fz -b3v/o&O>sLVLij8֦^ ERSSݻa4,jZԛEHi-H: _,7x7w̡j>s["MнH[2ҝAK&'~+22g+#B0)ΰ06@J Hx{{ X+5m8E!\B⫓&kc4{ pKb~l_@/ Z.:nܸoB6@G !F w=i$ꫤ_SbG4#~L2' {91 VNԊxC 0}7;bc@~k["]~eZm$_OO//ۏ2 q_ !!!Xp*wHt3v3CePƲ $/JQ 6aaq=.06tpZC/q7Dk777#ԔAMs1cƐ|u:8HR4*J[y>7u+HP'"˗cʕag^xlKe&Xl*AI pg{{WER ?vww'h*7/4T5ݟ3 L (<)No"%B1,BAĕTܽup ~`z8=Qax: Gb?Dʶ9Fe͎wkz3y6Ct5$''%`f\rI܆2 $KH1pLѲ yfb~#Durm#F%N3z1YƏiu Adg*2LbJVZ<w= %p Ej@V kgt;Ķ=OXV.X~{, r˝x%U`Μ9JlllPU5IԁvM4KP/bbc.e/իwyS}=|ukbn\3;7˃t, G#&qVQ_g.h)Ƌoh}E"F6SHEi4_g泿_xb%S\O3/((Z_TTܼ\]3 6"E~}&g.Q +i%匶{s1.zctK|#:RD9jFll m1t{U*Y^]\VS>,tS+Zף'g^lHGҥ\GtB`fDxZ!G6J6;}5޷r<FD΅SHu]>1)|)vXgEH'sd+ذaJ`XsxuČ8H$nBʺhėaǿ |.FKq˙|O C6:>wMOPp^`d Ö?)qF ѵ{WZ0 aP)AX<iAmzO }w=xGAmHô.y}K'Pt+I0'%HM#炄N09KlAq$-[ F;bjh4lgz`t3cr?\ϖ-4]-Cw&oh 3RuQ&x7 |іstq"h6T'X(u )~8jʬɼ&Iℭ[b8d',Ǹ(Epy`T$Ϙ kZK<N+gО{iKcBS= CƎ;zpA8;;RGgVIt6ar(dH>n;cja[8YOa#(6bb_hCOG}ؼP0 dwPRSO[ Sa$~?4ͥj01UnC,2F6E0Lʃt/Xl\,P^{/WBS f6|\29$ ]>rY26=홌g'%KNa]f?kFy^P!}a3y ʮ\P|[dMp.jȀ8f/Ŋ:/>J{:`/eBheiexZףOuZDZ+M;=*:keeeTw>w<6@t!mt'hI1t^}!j;^@z>N#6ބS|>)qm#}n;}/A(#A|Hڔ;} * U)|%w*xxySZ#0+<mCļp8-OD&$~x?k5 N]D}ȐO^OgKJsqtz /2+x18$Lr ;Q0,GJΉN9r4`NU}H5`%)m1̵M~fϬO LlدE ~W'ԨČGi@`t lpXjR}3E3"UO@8v ϟѣG[St 8`?_9."<ՈUGr-:Y^ZmpβWkνVgm3Ě˨^G޳')Sr{u1B 0jg,2~ojj(q} QVt@ Y#'Fz`DX:[(חL]p򭀿L|b)1t=s}J?*trD!DgV'!/bČe}QXғK*0RuvxlrXm>1<Ӟ"x>7JT,?1(&.6D( q/b'`ƿP y։: ^  Ip ld ]Vc Nщm%<;tK4U/*BfE;X$Tbs&D_k@{zEI=gvhZfyy^42tDYi#K΃PC,QG>\@Nf V͕ɉZ.{6c8 Ѡv!u=jw(:@q]f,Ť$fLqA'#h2Vlf 伾#x^ 褧gݼ'۲"؟ mrbb ^g_.@=~.>>*JBu@:K?!8/=ti񄘏)2U"RǬ^S/Z>C|?!{{Bzea-dhtP?NyQ=e3Z ~uj:Hず>`Mo3ԮR@~˲QsDDOGGx[o.N>hѢd$O뛷 )MLĶ<'člmm;{s;3^:DviN~,?Hʚ{?>Z_tbrFOmbip1W1:V3KNkeNFծcܑ)u9𭜃f`p1[nuW4LYS΂&_m 5ްQ=ә^+eIENDB`gmrender-resurrect-0.0.9/dist-scripts/000077500000000000000000000000001400533760300177705ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/arch/000077500000000000000000000000001400533760300207055ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/arch/README000066400000000000000000000001301400533760300215570ustar00rootroot00000000000000Arch package is maintained at https://aur.archlinux.org/packages/gmrender-resurrect-git gmrender-resurrect-0.0.9/dist-scripts/centos7/000077500000000000000000000000001400533760300213525ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/centos7/README.md000066400000000000000000000051251400533760300226340ustar00rootroot00000000000000How to compile, install and configure GMediaRender -------------------------------------------------- This guide is based on Fedora procedure and .spec file is just modified Fedora version adjusted for CentOS 7.x (based on Fedora 19 anyway). Install and enable Nux's repository since we need 'ugly' plugins for gstreamer :: # yum -y install http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-1.el7.nux.noarch.rpm # yum -y update Install pre-requisite packages :: # yum -y install gstreamer1 gstreamer1-devel gstreamer1-plugins-ugly gstreamer1-plugins-bad-free gstreamer1-plugins-base gstreamer1-plugins-good libupnp-devel Install build tools :: # yum -y install autoconf automake gcc rpmdevtools git (Optionally, create less priviledged user for building the rpms' as you should not use root) :: # useradd makerpm # passwd makerpm # su - makerpm Build the packages :: $ rpmdev-setuptree $ cd rpmbuild/SOURCES $ git clone https://github.com/hzeller/gmrender-resurrect.git $ mv gmrender-resurrect gmediarender-0.0.7 $ tar cjvf gmediarender-0.0.7.tar.bz2 gmediarender-0.0.7 $ rpmbuild -ba gmediarender-0.0.7/dist-scripts/centos7/gmediarender.spec After the packages are built, they're be ready at ~/rpmbuild/RPMS/x86_64/ for installation. Source rpm will be ready at ~/rpmbuild/SRPMS/. Installation can be done easily by :: # yum -y install gmediarender-0.0.7-1.el7.centos.x86_64.rpm Note: I would avoid installing them by rpm -i or yum localinstall, as first alter db outside of yum, and latter is deprecated. Additional configuration is also recommended, sice there's no configuration file for details, this can be achieved by modifing how systemd will start the service. If you want to modify the daemon to listen only on specific IP address and present itself with custom string, follow the steps :: # mkdir /etc/systemd/system/gmediarender.service.d # vi /etc/systemd/system/gmediarender.service.d/customize.conf # or nano, or emacs, or whatever editor you like [Service] ExecStart= ExecStart=/usr/bin/gmediarender --port=49494 --interface-name= -f "DLNA Renderer GMediaRender" # systemctl daemon-reload # systemctl start gmediarender.service If you are using FirewallD, you will also need to open firewall ports for GMediaRender. Custom service files are shipped with the package, just refresh FirewallD and add services to running configuration (and permanent as well, if desired) :: # firewall-cmd --reload # firewall-cmd --add-service=ssdp # firewall-cmd --add-service=gmediarender gmrender-resurrect-0.0.9/dist-scripts/centos7/gmediarender.service000066400000000000000000000003441400533760300253630ustar00rootroot00000000000000[Unit] Description=GMediaRender UPnP/DLNA renderer After=network.target sound.target [Service] Type=simple User=gmediarender Group=gmediarender ExecStart=/usr/bin/gmediarender --port=49494 [Install] WantedBy=multi-user.target gmrender-resurrect-0.0.9/dist-scripts/centos7/gmediarender.spec000066400000000000000000000053521400533760300246610ustar00rootroot00000000000000Name: gmediarender Version: 0.0.7 Release: 1%{?dist} Summary: Resource efficient UPnP/DLNA renderer License: LGPLv2+ URL: https://github.com/hzeller/gmrender-resurrect Source0: %{name}-%{version}.tar.bz2 BuildRequires: gstreamer1 BuildRequires: gstreamer1-devel BuildRequires: gstreamer1-plugins-ugly BuildRequires: gstreamer1-plugins-bad-free BuildRequires: gstreamer1-plugins-base BuildRequires: gstreamer1-plugins-good BuildRequires: libupnp-devel BuildRequires: systemd Requires: gstreamer1 Requires: gstreamer1-plugins-ugly Requires: gstreamer1-plugins-bad-free Requires: gstreamer1-plugins-base Requires: gstreamer1-plugins-good Requires: libupnp Requires(pre): shadow-utils Requires(post): systemd Requires(preun): systemd Requires(postun): systemd %description GMediaRender is a resource efficient UPnP/DLNA renderer. %prep %setup -q -n %{name}-%{version} ./autogen.sh %build %configure make %pre getent group gmediarender >/dev/null || groupadd -r gmediarender getent passwd gmediarender >/dev/null || \ useradd -r -g gmediarender -G audio -M -d /usr/share/gmediarender -s /sbin/nologin \ -c "GMediaRender DLNA/UPnP Renderer" gmediarender exit 0 %install mkdir -p $RPM_BUILD_ROOT/%{_bindir} cp ./src/gmediarender $RPM_BUILD_ROOT/%{_bindir} mkdir -p $RPM_BUILD_ROOT/%{_unitdir} cp ./dist-scripts/centos7/%{name}.service $RPM_BUILD_ROOT/%{_unitdir} mkdir -p $RPM_BUILD_ROOT/usr/share/gmediarender cp ./data/grender-64x64.png $RPM_BUILD_ROOT/usr/share/gmediarender cp ./data/grender-128x128.png $RPM_BUILD_ROOT/usr/share/gmediarender mkdir -p $RPM_BUILD_ROOT/usr/lib/firewalld/services cp ./dist-scripts/centos7/%{name}.xml $RPM_BUILD_ROOT/usr/lib/firewalld/services cp ./dist-scripts/centos7/ssdp.xml $RPM_BUILD_ROOT/usr/lib/firewalld/services %post %systemd_post %{name}.service %preun %systemd_preun %{name}.service %postun getent passwd gmediarender >/dev/null && userdel gmediarender getent group gmediarender >/dev/null && groupdel gmediarender %systemd_postun_with_restart %{name}.service %files %attr(0755,root,root) %{_bindir}/%{name} %config(noreplace) %{_unitdir}/%{name}.service %attr(0755,root,root) /usr/lib/firewalld/services/%{name}.xml %attr(0755,root,root) /usr/lib/firewalld/services/ssdp.xml %attr(0755,gmediarender,gmediarender) /usr/share/%{name}/ %attr(0644,gmediarender,gmediarender) /usr/share/%{name}/grender-64x64.png %attr(0644,gmediarender,gmediarender) /usr/share/%{name}/grender-128x128.png %changelog * Sun Mar 29 2015 - Updated for systemd snippets * Mon Dec 01 2014 - Updated for CentOS7, added automatic system user/group add and removal upon installation * Mon Sep 16 2013 - Initial release gmrender-resurrect-0.0.9/dist-scripts/centos7/gmediarender.xml000066400000000000000000000003351400533760300245230ustar00rootroot00000000000000 GMediaRender GMediaRender is a small headless UPnP media renderer for Linux. gmrender-resurrect-0.0.9/dist-scripts/centos7/ssdp.xml000066400000000000000000000003051400533760300230430ustar00rootroot00000000000000 SSDP Microsoft SSDP Enables discovery of UPnP devices gmrender-resurrect-0.0.9/dist-scripts/debian/000077500000000000000000000000001400533760300212125ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/debian/README000066400000000000000000000003731400533760300220750ustar00rootroot00000000000000These files can be used with debconf to create a Debian package. A more up to date version of these files may be available at https://github.com/christiscarborough/gmrender-resurrect-debian Contributed by Christi Scarborough . gmrender-resurrect-0.0.9/dist-scripts/debian/README.Debian000066400000000000000000000001041400533760300232460ustar00rootroot00000000000000Configuration info can be found in /etc/default/gmrender-resurrect. gmrender-resurrect-0.0.9/dist-scripts/debian/changelog000066400000000000000000000004631400533760300230670ustar00rootroot00000000000000gmrender-resurrect (0.0.7.git-1.1) UNRELEASED; urgency=low * local package * initial release, pulled from git on 20140204 * initial debhelper generated package release - previous versions generated by checkinstall -- Christi Scarborough Mon, 03 Feb 2014 20:46:15 +0000 gmrender-resurrect-0.0.9/dist-scripts/debian/compat000066400000000000000000000000021400533760300224100ustar00rootroot000000000000009 gmrender-resurrect-0.0.9/dist-scripts/debian/control000066400000000000000000000020201400533760300226070ustar00rootroot00000000000000Source: gmrender-resurrect Maintainer: Henner Zeller Homepage: https://github.com/hzeller/gmrender-resurrect/ Section: sound Priority: optional Standards-Version: 3.9.4 Build-Depends: debhelper (>= 9), autoconf, automake, libtool, libupnp6-dev, libgstreamer0.10-dev, hardening-wrapper Package: gmrender-resurrect Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-ffmpeg, gstreamer0.10-pulseaudio | gstreamer0.10-alsa, libupnp6 Suggests: gstreamer0.10-plugins-bad, gstreamer0.10-plugins-ugly Description: Minimalist UPNP AV renderer gmrender-resurrect is a minimalist UPNP AV renderer that can be used to play music controlled by a UPNP AV control point. This package contains only a renderer and will therefore require these things to be installed either on this device or another device on the local network in order to be usable. gmrender-resurrect usese GStreamer to provide the infrastructure for playing music. gmrender-resurrect-0.0.9/dist-scripts/debian/copyright000066400000000000000000000022731400533760300231510ustar00rootroot00000000000000http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Name: gmrender-resurrect Maintainer: Christi Scarborough Source: https://github.com/hzeller/gmrender-resurrect Copyright: 2005-2007 Ivo Clarysse 2012-2014 Henner Zeller License: GPL-2 Files: debian/* Copyright: 2014 Christi Scarborough License: GPL-2 License: GPL-2 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. gmrender-resurrect-0.0.9/dist-scripts/debian/docs000066400000000000000000000000721400533760300220640ustar00rootroot00000000000000AUTHORS README.md NEWS ChangeLog README gmrender-resurrect-0.0.9/dist-scripts/debian/gmediarender.1000066400000000000000000000125051400533760300237250ustar00rootroot00000000000000.\" Hey, EMACS: -*- nroff -*- .\" (C) Copyright 2014 Christi Scarborough , .\" .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH GMEDIARENDER 1 "February 4, 2014" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME gmediarender \- UPNP-AV renderer daemon .SH SYNOPSIS .B gmediarender .RI [ options ] " files" ... .SH DESCRIPTION This manual page documents briefly the .B gmediarender command. .PP .\" TeX users may be more comfortable with the \fB\fP and .\" \fI\fP escape sequences to invode bold face and italics, .\" respectively. \fBgmediarender\fP is a daemon which acts as a UPNP-AV renderer on the local network. It can be controlled from a UPNP-AV control point to play media files from a UPNP-AV media server through an audio device on the local system. .SH OPTIONS This program follows the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .SS "Application options:" .TP .B \-d, \-\-daemon Runs the program as a daemon. .TP .B \-f, \-\-friendly\-name \fI\\fP Friendly name to advertise on the network. Usually, it is desirable for the renderer to show up on controllers under a recognisable and unique name. This is the option to set that name. .TP .B \-I, \-\-interface\-name \fI\\fP The local interface name the service is running and advertised on. .TP .B \-p, \-\-port \fI\\fP Port to listen to. [49152..65535]. \fBlibupnp\fP does not use SO_REUSEADDR, so it might be necessary to increment this number. .TP .B \-P, \-\-pid\-file \fI\\fP Name of the file which the process ID should be written to. .TP .B \-u, \-\-uuid \fI\\fP UUID to advertise on the network in the form \fIuser:group\fP. This sets the UPNP UUID that is advertised and used by controllers to distinguish different renderers. Usually, \fBgmediarender\fP uses a built-in static id. If there are multiple renderers running on the local network, they will all share the same static ID. With this option, each renderer can be given its own id. One way to generate a UUID is to create a UUID once by running the \fBuuidgen\fP tool. .TP .B \-\-mime-filter \fI\\fP Filters the advertised MIME types supported by the renderer. Normally, \fBgmediarender\fP advertises all MIME types (media formats) supported by the output. Use this option to limit the advertised media support or add/remove support for a specific type. Filters take 3 forms. Multiple filters are separated by a comma. \(bu Prefix filters will only advertise MIME types that match the supplied prefix. e.g. audio, video, image. \(bu Inclusion filters will add the supplied type to the supported list. e.g. +audio/x-flac \(bu Removal filters will remove the supplied type from the supported list. e.g. -audio/x-flac e.g. To allow only audio, without FLAC but include FLV. --mime-filter audio,-audio/x-flac,+video/x-flv .SS "Audio options:" .TP \fB\-\-gstout\-audiosink\fP \fI\\fP Set the audio sink. .TP \fB\-\-gstout\-audiodevice\fP \fI\\fP Set the audio device. The \fB\-\-gstout\-audio\.\.\.\fP options allow the user to configure where the sound output from \fBgmediarender\fP is sent. The correct values will depend on which method of audio output (e.g. ALSA) is being used. .TP .B \-\-gstout\-initial\-volume\-db \fI\\fP Sets the initial volume on startup in decibels. Values range from 0 (maximum volume) to \-60 (minimum volume. The decibel scale is non\-linear: \-20db represents roughly half volume. The default value is 0. Note that this does not influence the hardware (e.g. ALSA) volume level, but only internal attenuation. It is advisable to set the hardware level to 100\% where possible. .TP .B \-\-list\-outputs List available output modules and exit. .TP .B \-o, \-\-output \fI\\fR Output module to use. Use \fB\-\-output\fP to obtain a list of valid modules. .SS "Debug options:" .TP \fB\-\-dump\-devicedesc\fR Dump device descriptor XML and exit. .TP \fB\-\-dump\-connmgr\-scpd\fR Dump Connection Manager service description XML and exit. .TP \fB\-\-dump\-control\-scpd\fR Dump Rendering Control service description XML and exit. .TP \fB\-\-dump\-transport\-scpd\fR Dump A/V Transport service description XML and exit. .TP .B \-\-logfile \fI\\fP Name of a file to log output of the program to. To log to the terminal use use \-\-logfile \/dev\/stdout This file can get quite large over time, so it is only recommended to use this option for debugging purposes. .SS "Help options" .TP .B \-h, \-\-help Show summary of options. .TP \fB\-\-help\-all\fR Show all help options. .TP \fB\-\-help\-gstout\fR Show GStreamer Output Options. .TP \fB\-\-help\-gst\fR Show GStreamer Options. .TP .B \-v, \-\-version Show the program version. .SH SEE ALSO .BR uuidgen (1). gmrender-resurrect-0.0.9/dist-scripts/debian/gmrender-resurrect.default000077500000000000000000000030221400533760300263770ustar00rootroot00000000000000# Defaults for gmrender-resurrect initscript # sourced by /etc/init.d/gmrender-resurrect # installed at /etc/default/gmrender-resurrect by the maintainer scripts # # This is a POSIX shell fragment # # User and group the daemon will be running as. DAEMON_USER="nobody:audio" # Device name as it will be advertised to and shown in the UPnP # controller UI. Some string that helps you recognize the player, e.g. # "Livingroom Player" # Default is the short hostname of this device UPNP_DEVICE_NAME=`hostname -s` # Initial volume in decibel. 0.0 is 'full volume', -10 correspondents to # '75' on the exported volume scale (Note, this does not change the ALSA # volume, only internal to gmrender. So make sure to leave the ALSA # volume always to 100%). INITIAL_VOLUME_DB=-10 # If you explicitly choose a specific ALSA device here (find them with # 'aplay -L'), then gmediarenderer will use that ALSA device to play # audio. Otherwise, whatever default is configured for gstreamer for the # '$DAEMON_USER' is used. ALSA_DEVICE="sysdefault" #ALSA_DEVICE="iec958" if [ -n "$ALSA_DEVICE" ] ; then GS_SINK_PARAM="--gstout-audiosink=alsasink" GS_DEVICE_PARAM="--gstout-audiodevice=$ALSA_DEVICE" fi # A simple stable UUID, based on this systems' first ethernet # device's MAC address, only using tools readily available to generate. UPNP_UUID=`ip link show | awk '/ether/ {print "salt:)-" $2}' | head -1 | md5sum | awk '{print $1}'` # Alternatively use the MD5 summ of the short host name. # UPNP_UUID=`hostname -s | md5sum | | awk '{print $1}'` gmrender-resurrect-0.0.9/dist-scripts/debian/gmrender-resurrect.init000077500000000000000000000110501400533760300257160ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: gmrender-resurrect # Required-Start: $network $local_fs $remote_fs # Required-Stop: $network $local_fs $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: gmediarender-resurrect UPNP-AV renderer # Description: Allows media files to be played on a local media output device using GStreamer # when passed files from a UPNP-AV media library using a UNPN-AV control point. ### END INIT INFO # Author: Christi Scarborough # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="UPNP renderer" # Introduce a short description here NAME=gmediarender # Introduce the short server's name here PNAME=gmrender-resurrect # Package name DAEMON=/usr/bin/gmediarender # Introduce the server's location here PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$PNAME # Exit if the package is not installed [ -x $DAEMON ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$PNAME ] && . /etc/default/$PNAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions DAEMON_ARGS="-f $UPNP_DEVICE_NAME -d -u $UPNP_UUID $GS_SINK_PARAM $GS_DEVICE_PARAM --gstout-initial-volume-db=$INITIAL_VOLUME_DB" # Arguments to run the daemon with # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet -c "$DAEMON_USER" --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet -c "$DAEMON_USER" --pidfile $PIDFILE --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : gmrender-resurrect-0.0.9/dist-scripts/debian/gmrender-resurrect.service000066400000000000000000000011631400533760300264140ustar00rootroot00000000000000[Unit] Description=gmrender-resurrect service After=network.target sound.target [Service] Environment="UPNP_DEVICE_NAME=Raspberry" ExecStartPre=/bin/sh -c "/bin/systemctl set-environment UPNP_UUID=`ip link show | awk '/ether/ {print \"salt:)-\" $2}' | head -1 | md5sum | awk '{print $1}'`" ExecStart=/usr/local/bin/gmediarender -f "$UPNP_DEVICE_NAME" -u "$UPNP_UUID" \ --gstout-audiosink=alsasink --gstout-audiodevice=sysdefault \ --logfile=/tmp/gmediarenderer.log --gstout-initial-volume-db=-10 Restart=always [Install] WantedBy=multi-user.target gmrender-resurrect-0.0.9/dist-scripts/debian/manpages000066400000000000000000000000261400533760300227260ustar00rootroot00000000000000debian/gmediarender.1 gmrender-resurrect-0.0.9/dist-scripts/debian/rules000077500000000000000000000004021400533760300222660ustar00rootroot00000000000000#!/usr/bin/make -f export DEB_BUILD_HARDENING=1 %: dh $@ override_dh_auto_configure: ./autogen.sh dh_auto_configure -- override_dh_auto_clean: if [ -e ./Makefile ]; then make maintainer-clean; fi xargs rm -f < debian/autoconf.files dh_auto_clean -- gmrender-resurrect-0.0.9/dist-scripts/debian/source/000077500000000000000000000000001400533760300225125ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/debian/source/format000066400000000000000000000000141400533760300237200ustar00rootroot000000000000003.0 (quilt) gmrender-resurrect-0.0.9/dist-scripts/debian/watch000066400000000000000000000002441400533760300222430ustar00rootroot00000000000000version=3 opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/gmrender-resurrect-$1\.tar\.gz/ \ https://github.com/hzeller/gmrender-resurrect/tags .*/v?(\d\S*)\.tar\.gz gmrender-resurrect-0.0.9/dist-scripts/fedora/000077500000000000000000000000001400533760300212305ustar00rootroot00000000000000gmrender-resurrect-0.0.9/dist-scripts/fedora/README.md000066400000000000000000000047111400533760300225120ustar00rootroot00000000000000How to compile, install and configure GMediaRender -------------------------------------------------- Install and enable RPMFusion repository since we need 'ugly' plugins for gstreamer :: # yum -y install http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-21.noarch.rpm # Fedora 21 # yum -y update Install pre-requisite packages :: # yum -y install gstreamer1 gstreamer1-devel gstreamer1-plugins-ugly gstreamer1-plugins-bad-free gstreamer1-plugins-base gstreamer1-plugins-good libupnp-devel Install build tools :: # yum -y install autoconf automake gcc rpmdevtools git (Optionally, create less priviledged user for building the rpms' as you should not use root) :: # useradd makerpm # passwd makerpm # su - makerpm Build the packages :: $ rpmdev-setuptree $ cd rpmbuild/SOURCES $ git clone https://github.com/hzeller/gmrender-resurrect.git $ mv gmrender-resurrect gmediarender-0.0.7 $ tar cjvf gmediarender-0.0.7.tar.bz2 gmediarender-0.0.7 $ rpmbuild -ba gmediarender-0.0.7/dist-scripts/fedora/gmediarender.spec After the packages are built, they're be ready at ~/rpmbuild/RPMS/x86_64/ for installation. Source rpm will be ready at ~/rpmbuild/SRPMS/. Installation can be done easily by :: # yum -y install gmediarender-0.0.7-1.fc21.x86_64.rpm Note: I would avoid installing them by rpm -i or yum localinstall, as first alter db outside of yum, and latter is deprecated. Additional configuration is also recommended, sice there's no configuration file for details, this can be achieved by modifing how systemd will start the service. If you want to modify the daemon to listen only on specific IP address and present itself with custom string, follow the steps :: # mkdir /etc/systemd/system/gmediarender.service.d # vi /etc/systemd/system/gmediarender.service.d/customize.conf # or nano, or emacs, or whatever editor you like [Service] ExecStart= ExecStart=/usr/bin/gmediarender --port=49494 --interface-name= -f "DLNA Renderer GMediaRender" # systemctl daemon-reload # systemctl start gmediarender.service If you are using FirewallD, you will also need to open firewall ports for GMediaRender. Custom service files are shipped with the package, just refresh FirewallD and add services to running configuration (and permanent as well, if desired) :: # firewall-cmd --reload # firewall-cmd --add-service=ssdp # firewall-cmd --add-service=gmediarender gmrender-resurrect-0.0.9/dist-scripts/fedora/gmediarender.service000077500000000000000000000003441400533760300252440ustar00rootroot00000000000000[Unit] Description=GMediaRender UPnP/DLNA renderer After=network.target sound.target [Service] Type=simple User=gmediarender Group=gmediarender ExecStart=/usr/bin/gmediarender --port=49494 [Install] WantedBy=multi-user.target gmrender-resurrect-0.0.9/dist-scripts/fedora/gmediarender.spec000077500000000000000000000053601400533760300245410ustar00rootroot00000000000000Name: gmediarender Version: 0.0.7 Release: 1%{?dist} Summary: Resource efficient UPnP/DLNA renderer License: LGPLv2+ URL: http://github.com/hzeller/gmrender-resurrect Source0: http://github.com/hzeller/gmrender-resurrect/%{name}-%{version}.tar.bz2 BuildRequires: gstreamer1 BuildRequires: gstreamer1-devel BuildRequires: gstreamer1-plugins-ugly BuildRequires: gstreamer1-plugins-bad-free BuildRequires: gstreamer1-plugins-base BuildRequires: gstreamer1-plugins-good BuildRequires: libupnp-devel BuildRequires: systemd Requires: gstreamer1 Requires: gstreamer1-plugins-ugly Requires: gstreamer1-plugins-bad-free Requires: gstreamer1-plugins-base Requires: gstreamer1-plugins-good Requires: libupnp Requires(pre): shadow-utils Requires(post): systemd Requires(preun): systemd Requires(postun): systemd %description GMediaRender is a resource efficient UPnP/DLNA renderer. %prep %setup -q -n %{name}-%{version} ./autogen.sh %build %configure make %pre getent group gmediarender >/dev/null || groupadd -r gmediarender getent passwd gmediarender >/dev/null || \ useradd -r -g gmediarender -G audio -M -d /usr/share/gmediarender -s /sbin/nologin \ -c "GMediaRender DLNA/UPnP Renderer" gmediarender exit 0 %install mkdir -p $RPM_BUILD_ROOT/%{_bindir} cp ./src/gmediarender $RPM_BUILD_ROOT/%{_bindir} mkdir -p $RPM_BUILD_ROOT/%{_unitdir} cp ./dist-scripts/fedora/%{name}.service $RPM_BUILD_ROOT/%{_unitdir} mkdir -p $RPM_BUILD_ROOT/usr/share/gmediarender cp ./data/grender-64x64.png $RPM_BUILD_ROOT/usr/share/gmediarender cp ./data/grender-128x128.png $RPM_BUILD_ROOT/usr/share/gmediarender mkdir -p $RPM_BUILD_ROOT/usr/lib/firewalld/services cp ./dist-scripts/fedora/%{name}.xml $RPM_BUILD_ROOT/usr/lib/firewalld/services cp ./dist-scripts/fedora/ssdp.xml $RPM_BUILD_ROOT/usr/lib/firewalld/services %post %systemd_post %{name}.service %preun %systemd_preun %{name}.service %postun getent passwd gmediarender >/dev/null && userdel gmediarender getent group gmediarender >/dev/null && groupdel gmediarender %systemd_postun_with_restart %{name}.service %files %attr(0755,root,root) %{_bindir}/%{name} %config(noreplace) %{_unitdir}/%{name}.service %attr(0755,root,root) /usr/lib/firewalld/services/%{name}.xml %attr(0755,root,root) /usr/lib/firewalld/services/ssdp.xml %attr(0755,gmediarender,gmediarender) /usr/share/%{name}/ %attr(0644,gmediarender,gmediarender) /usr/share/%{name}/grender-64x64.png %attr(0644,gmediarender,gmediarender) /usr/share/%{name}/grender-128x128.png %changelog * Sun Mar 29 2015 - Updated for systemd snippets, added automatic system user/group add and removal upon installation, added FirewallD support * Mon Sep 16 2013 - Initial release gmrender-resurrect-0.0.9/dist-scripts/fedora/gmediarender.xml000066400000000000000000000003351400533760300244010ustar00rootroot00000000000000 GMediaRender GMediaRender is a small headless UPnP media renderer for Linux. gmrender-resurrect-0.0.9/dist-scripts/fedora/ssdp.xml000066400000000000000000000003051400533760300227210ustar00rootroot00000000000000 SSDP Microsoft SSDP Enables discovery of UPnP devices gmrender-resurrect-0.0.9/scripts/000077500000000000000000000000001400533760300170275ustar00rootroot00000000000000gmrender-resurrect-0.0.9/scripts/init.d/000077500000000000000000000000001400533760300202145ustar00rootroot00000000000000gmrender-resurrect-0.0.9/scripts/init.d/README000066400000000000000000000002301400533760300210670ustar00rootroot00000000000000Simple init script that can guide you as a template on common System-V init-based systems. Make sure to look at the variables to adapt to your system. gmrender-resurrect-0.0.9/scripts/init.d/gmediarenderer000077500000000000000000000037761400533760300231340ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: gmediarender # Required-Start: $remote_fs $syslog $all # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start GMediaRender at boot time # Description: Start GMediaRender at boot time. ### END INIT INFO # User and group the daemon will be running as. On the Raspberry Pi, let's use # the default user. DAEMON_USER="pi:audio" # Device name as it will be advertised to and shown in the UPnP controller UI. # Some string that helps you recognize the player, e.g. "Livingroom Player" UPNP_DEVICE_NAME="Raspberry" # Initial volume in decibel. 0.0 is 'full volume', -10 correspondents to '75' on # the exported volume scale (Note, this does not change the ALSA volume, only # internal to gmrender. So make sure to leave the ALSA volume always to 100%). INITIAL_VOLUME_DB=-10 # If you explicitly choose a specific ALSA device here (find them with 'aplay -L'), then # gmediarenderer will use that ALSA device to play audio. # Otherwise, whatever default is configured for gstreamer for the '$DAEMON_USER' is # used. ALSA_DEVICE="sysdefault" #ALSA_DEVICE="iec958" # Path to the gmediarender binary. BINARY_PATH=/usr/local/bin/gmediarender if [ -n "$ALSA_DEVICE" ] ; then GS_SINK_PARAM="--gstout-audiosink=alsasink" GS_DEVICE_PARAM="--gstout-audiodevice=$ALSA_DEVICE" fi # A simple stable UUID, based on this systems' first ethernet devices MAC address, # only using tools readily available to generate. UPNP_UUID=`ip link show | awk '/ether/ {print "salt:)-" $2}' | head -1 | md5sum | awk '{print $1}'` USER=root HOME=/root export USER HOME case "$1" in start) echo "Starting GMediaRender" start-stop-daemon -x $BINARY_PATH -c "$DAEMON_USER" -S -- -f "$UPNP_DEVICE_NAME" -d -u "$UPNP_UUID" $GS_SINK_PARAM $GS_DEVICE_PARAM --gstout-initial-volume-db=$INITIAL_VOLUME_DB ;; stop) echo "Stopping GMediaRender" start-stop-daemon -x $BINARY_PATH -K ;; *) echo "Usage: /etc/init.d/gmediarender {start|stop}" exit 1 ;; esac exit 0 gmrender-resurrect-0.0.9/src/000077500000000000000000000000001400533760300161275ustar00rootroot00000000000000gmrender-resurrect-0.0.9/src/Makefile.am000066400000000000000000000017271400533760300201720ustar00rootroot00000000000000bin_PROGRAMS = gmediarender gmediarender_SOURCES = main.c git-version.h \ upnp_service.c upnp_control.c upnp_connmgr.c upnp_transport.c \ upnp_service.h upnp_control.h upnp_connmgr.h upnp_transport.h \ song-meta-data.h song-meta-data.c \ variable-container.h variable-container.c \ upnp_device.c upnp_device.h \ upnp_renderer.h upnp_renderer.c \ webserver.c webserver.h \ output.c output.h \ logging.h logging.c \ xmldoc.c xmldoc.h \ xmlescape.c xmlescape.h if HAVE_GST gmediarender_SOURCES += \ output_gstreamer.c output_gstreamer.h endif main.c : git-version.h git-version.h: .FORCE $(AM_V_GEN)(echo "#define GM_COMPILE_VERSION \"$(shell git log -n1 --date=short --format='0.0.9_git%cd_%h' 2>/dev/null || echo -n '0.0.9')\"" > $@-new; \ cmp -s $@ $@-new || cp $@-new $@; \ rm $@-new) .FORCE: AM_CPPFLAGS = $(GLIB_CFLAGS) $(GST_CFLAGS) $(LIBUPNP_CFLAGS) -DPKG_DATADIR=\"$(datadir)/gmediarender\" gmediarender_LDADD = $(GLIB_LIBS) $(GST_LIBS) $(LIBUPNP_LIBS) gmrender-resurrect-0.0.9/src/logging.c000066400000000000000000000067141400533760300177310ustar00rootroot00000000000000/* logging.c - Logging facility * * Copyright (C) 2013 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include "config.h" #include "git-version.h" static int log_fd = -1; static int enable_color = 0; static const char *const kInfoHighlight = "\033[1mINFO "; static const char *const kErrorHighlight = "\033[1m\033[31mERROR "; static const char *const kTermReset = "\033[0m"; static const char *info_markup_start_ = "INFO "; static const char *error_markup_start_ = "ERROR "; static const char *markup_end_ = ""; void Log_init(const char *filename) { if (filename == NULL) return; if (!strcmp(filename, "stdout")) { log_fd = 1; } else if (!strcmp(filename, "stderr")) { log_fd = 2; } else { log_fd = open(filename, O_CREAT|O_APPEND|O_WRONLY, 0644); } if (log_fd < 0) { perror("Cannot open logfile"); return; } enable_color = isatty(log_fd); if (enable_color) { info_markup_start_ = kInfoHighlight; error_markup_start_ = kErrorHighlight; markup_end_ = kTermReset; } } int Log_color_allowed(void) { return enable_color; } int Log_info_enabled(void) { return log_fd >= 0; } int Log_error_enabled(void) { return 1; } static void Log_internal(int fd, const char *markup_start, const char *category, const char *format, va_list ap) { struct timeval now; gettimeofday(&now, NULL); struct tm time_breakdown; localtime_r(&now.tv_sec, &time_breakdown); char fmt_buf[128]; strftime(fmt_buf, sizeof(fmt_buf), "%F %T", &time_breakdown); struct iovec parts[3]; parts[0].iov_len = asprintf((char**) &parts[0].iov_base, "%s[%s.%06ld | %s]%s ", markup_start, fmt_buf, now.tv_usec, category, markup_end_); parts[1].iov_len = vasprintf((char**) &parts[1].iov_base, format, ap); parts[2].iov_base = (void*) "\n"; parts[2].iov_len = 1; int already_newline = (parts[1].iov_len > 0 && ((const char*)parts[1].iov_base)[parts[1].iov_len-1] == '\n'); if (writev(fd, parts, already_newline ? 2 : 3) < 0) { // Logging trouble. Ignore. } free(parts[0].iov_base); free(parts[1].iov_base); } void Log_info(const char *category, const char *format, ...) { if (log_fd < 0) return; va_list ap; va_start(ap, format); Log_internal(log_fd, info_markup_start_, category, format, ap); va_end(ap); } void Log_error(const char *category, const char *format, ...) { va_list ap; va_start(ap, format); Log_internal(log_fd < 0 ? STDERR_FILENO : log_fd, error_markup_start_, category, format, ap); va_end(ap); } gmrender-resurrect-0.0.9/src/logging.h000066400000000000000000000030201400533760300177210ustar00rootroot00000000000000/* logging.h - Logging facility * * Copyright (C) 2013 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _LOGGING_H #define _LOGGING_H // Define this with empty, if you're not using gcc. #define PRINTF_FMT_CHECK(fmt_pos, args_pos) \ __attribute__ ((format (printf, fmt_pos, args_pos))) // With filename given, logs info and error to that file. If filename is NULL, // nothing is logged (TODO: log error to syslog). void Log_init(const char *filename); int Log_color_allowed(void); // Returns if we're allowed to use terminal color. int Log_info_enabled(void); int Log_error_enabled(void); void Log_info(const char *category, const char *format, ...) PRINTF_FMT_CHECK(2, 3); void Log_error(const char *category, const char *format, ...) PRINTF_FMT_CHECK(2, 3); #endif /* _LOGGING_H */ gmrender-resurrect-0.0.9/src/main.c000066400000000000000000000237231400533760300172260ustar00rootroot00000000000000/* main.c - Main program routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #ifndef HAVE_LIBUPNP # error "To have gmrender any useful, you need to have libupnp installed." #endif #include #include // For version strings of upnp and gstreamer #include #ifdef HAVE_GST # include #endif #include "git-version.h" #include "logging.h" #include "output.h" #include "upnp_service.h" #include "upnp_control.h" #include "upnp_device.h" #include "upnp_renderer.h" #include "upnp_transport.h" #include "upnp_connmgr.h" static gboolean show_version = FALSE; static gboolean show_devicedesc = FALSE; static gboolean show_connmgr_scpd = FALSE; static gboolean show_control_scpd = FALSE; static gboolean show_transport_scpd = FALSE; static gboolean show_outputs = FALSE; static gboolean daemon_mode = FALSE; static const gchar *interface_name = NULL; static int listen_port = 49494; #ifdef GMRENDER_UUID // Compile-time uuid. static const gchar *uuid = GMRENDER_UUID; #else static const gchar *uuid = "GMediaRender-1_0-000-000-002"; #endif static const gchar *friendly_name = PACKAGE_NAME; static const gchar *output = NULL; static const gchar *pid_file = NULL; static const gchar *log_file = NULL; static const gchar *mime_filter = NULL; /* Generic GMediaRender options */ static GOptionEntry option_entries[] = { { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, "Output version information and exit", NULL }, { "interface-name", 'I', 0, G_OPTION_ARG_STRING, &interface_name, "The local interface name the service is running and advertised", NULL }, // The following is not very reliable, as libupnp does not set // SO_REUSEADDR by default, so it might increment (sending patch). { "port", 'p', 0, G_OPTION_ARG_INT, &listen_port, "Port to listen to; [49152..65535] (libupnp does not use " "SO_REUSEADDR, so might increment)", NULL }, { "uuid", 'u', 0, G_OPTION_ARG_STRING, &uuid, "UUID to advertise", NULL }, { "friendly-name", 'f', 0, G_OPTION_ARG_STRING, &friendly_name, "Friendly name to advertise.", NULL }, { "output", 'o', 0, G_OPTION_ARG_STRING, &output, "Output module to use.", NULL }, { "pid-file", 'P', 0, G_OPTION_ARG_STRING, &pid_file, "File the process ID should be written to.", NULL }, { "daemon", 'd', 0, G_OPTION_ARG_NONE, &daemon_mode, "Run as daemon.", NULL }, { "mime-filter", 0, 0, G_OPTION_ARG_STRING, &mime_filter, "Filter the supported media types. " "e.g. Audio only: '--mime-filter audio'. Disable FLAC: '--mime-filter -audio/x-flac'.", NULL }, { "logfile", 0, 0, G_OPTION_ARG_STRING, &log_file, "Debug log filename. Use 'stdout' or 'stderr' to log to console.", NULL }, { "list-outputs", 0, 0, G_OPTION_ARG_NONE, &show_outputs, "List available output modules and exit", NULL }, { "dump-devicedesc", 0, 0, G_OPTION_ARG_NONE, &show_devicedesc, "Dump device descriptor XML and exit.", NULL }, { "dump-connmgr-scpd", 0, 0, G_OPTION_ARG_NONE, &show_connmgr_scpd, "Dump Connection Manager service description XML and exit.", NULL }, { "dump-control-scpd", 0, 0, G_OPTION_ARG_NONE, &show_control_scpd, "Dump Rendering Control service description XML and exit.", NULL }, { "dump-transport-scpd", 0, 0, G_OPTION_ARG_NONE, &show_transport_scpd, "Dump A/V Transport service description XML and exit.", NULL }, { NULL } }; // Fill buffer with version information. Returns pointer to beginning of string. static const char *GetVersionInfo(char *buffer, size_t len) { #ifdef HAVE_GST snprintf(buffer, len, "gmediarender %s " "(libupnp-%s; glib-%d.%d.%d; gstreamer-%d.%d.%d)", GM_COMPILE_VERSION, UPNP_VERSION_STRING, GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION, GLIB_MICRO_VERSION, GST_VERSION_MAJOR, GST_VERSION_MINOR, GST_VERSION_MICRO); #else snprintf(buffer, len, "gmediarender %s " "(libupnp-%s; glib-%d.%d.%d; without gstreamer.)", GM_COMPILE_VERSION, UPNP_VERSION_STRING, GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION, GLIB_MICRO_VERSION); #endif return buffer; } static void do_show_version(void) { char version[1024]; GetVersionInfo(version, sizeof(version)); printf("%s; %s\n" "This is free software. " "You may redistribute copies of it under the terms of\n" "the GNU General Public License " ".\n" "There is NO WARRANTY, to the extent permitted by law.\n", PACKAGE_STRING, version); } static gboolean process_cmdline(int argc, char **argv) { GOptionContext *ctx; GError *err = NULL; int rc; ctx = g_option_context_new("- GMediaRender"); g_option_context_add_main_entries(ctx, option_entries, NULL); rc = output_add_options(ctx); if (rc != 0) { fprintf(stderr, "Failed to add output options\n"); g_option_context_free(ctx); return FALSE; } if (!g_option_context_parse (ctx, &argc, &argv, &err)) { fprintf(stderr, "Failed to initialize: %s\n", err->message); g_error_free (err); g_option_context_free(ctx); return FALSE; } g_option_context_free(ctx); return TRUE; } static void log_variable_change(void *userdata, int var_num, const char *variable_name, const char *old_value, const char *variable_value) { (void)var_num; (void)old_value; const char *category = (const char*) userdata; int needs_newline = variable_value[strlen(variable_value) - 1] != '\n'; // Silly terminal codes. Set to empty strings if not needed. const char *var_start = Log_color_allowed() ? "\033[1m\033[34m" : ""; const char *var_end = Log_color_allowed() ? "\033[0m" : ""; Log_info(category, "%s%s%s: %s%s", var_start, variable_name, var_end, variable_value, needs_newline ? "\n" : ""); } static void init_logging(const char *log_file) { char version[1024]; GetVersionInfo(version, sizeof(version)); if (log_file != NULL) { Log_init(log_file); Log_info("main", "%s log started [ %s ]", PACKAGE_STRING, version); } else { fprintf(stderr, "%s started [ %s ].\nLogging switched off. " "Enable with --logfile= " "(or --logfile=stdout for console)\n", PACKAGE_STRING, version); } } int main(int argc, char **argv) { int rc; struct upnp_device_descriptor *upnp_renderer; #if !GLIB_CHECK_VERSION(2,32,0) g_thread_init (NULL); // Was necessary < glib 2.32, deprecated since. #endif if (!process_cmdline(argc, argv)) { return EXIT_FAILURE; } if (show_version) { do_show_version(); exit(EXIT_SUCCESS); } if (show_connmgr_scpd) { upnp_renderer_dump_connmgr_scpd(); exit(EXIT_SUCCESS); } if (show_control_scpd) { upnp_renderer_dump_control_scpd(); exit(EXIT_SUCCESS); } if (show_transport_scpd) { upnp_renderer_dump_transport_scpd(); exit(EXIT_SUCCESS); } if (show_outputs) { output_dump_modules(); exit(EXIT_SUCCESS); } init_logging(log_file); // Now we're going to start threads etc, which means we need // to become a daemon before that. // We need to open the pid-file now because relative filenames will // break if we're becoming a daemon and cwd changes. FILE *pid_file_stream = NULL; if (pid_file) { pid_file_stream = fopen(pid_file, "w"); } // TODO: check for availability of daemon() in configure. if (daemon_mode) { if (daemon(0, 0) < 0) { perror("Becoming daemon: "); return EXIT_FAILURE; } } if (pid_file_stream) { fprintf(pid_file_stream, "%d\n", getpid()); fclose(pid_file_stream); } upnp_renderer = upnp_renderer_descriptor(friendly_name, uuid, mime_filter); if (upnp_renderer == NULL) { return EXIT_FAILURE; } rc = output_init(output); if (rc != 0) { Log_error("main", "ERROR: Failed to initialize Output subsystem"); return EXIT_FAILURE; } struct upnp_device *device; if (listen_port != 0 && (listen_port < 49152 || listen_port > 65535)) { // Somewhere obscure internally in libupnp, they clamp the // port to be outside of the IANA range, so at least 49152. // Instead of surprising the user by ignoring lower port // numbers, complain loudly. Log_error("main", "Parameter error: --port needs to be in " "range [49152..65535] (but was set to %d)", listen_port); return EXIT_FAILURE; } device = upnp_device_init(upnp_renderer, interface_name, listen_port); if (device == NULL) { Log_error("main", "ERROR: Failed to initialize UPnP device"); return EXIT_FAILURE; } upnp_transport_init(device); upnp_control_init(device); if (show_devicedesc) { // This can only be run after all services have been // initialized. char *buf = upnp_create_device_desc(upnp_renderer); assert(buf != NULL); fputs(buf, stdout); exit(EXIT_SUCCESS); } if (Log_info_enabled()) { upnp_transport_register_variable_listener(log_variable_change, (void*) "transport"); upnp_control_register_variable_listener(log_variable_change, (void*) "control"); } // Write both to the log (which might be disabled) and console. Log_info("main", "Ready for rendering."); fprintf(stderr, "Ready for rendering.\n"); output_loop(); // We're here, because the loop exited. Probably due to catching // a signal. Log_info("main", "Exiting."); upnp_device_shutdown(device); return EXIT_SUCCESS; } gmrender-resurrect-0.0.9/src/output.c000066400000000000000000000116201400533760300176330ustar00rootroot00000000000000/* output.c - Output module frontend * * Copyright (C) 2007 Ivo Clarysse, (C) 2012 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include "logging.h" #include "output_module.h" #ifdef HAVE_GST #include "output_gstreamer.h" #endif #include "output.h" static struct output_module *modules[] = { #ifdef HAVE_GST &gstreamer_output, #else // this will be a runtime error, but there is not much point // in waiting till then. #error "No output configured. You need to ./configure --with-gstreamer" #endif }; static struct output_module *output_module = NULL; void output_dump_modules(void) { int count; count = sizeof(modules) / sizeof(struct output_module *); if (count == 0) { puts(" NONE!"); } else { int i; for (i=0; ishortname, modules[i]->description, (i==0) ? " (default)" : ""); } } } int output_init(const char *shortname) { int count; count = sizeof(modules) / sizeof(struct output_module *); if (count == 0) { Log_error("output", "No output module available"); return -1; } if (shortname == NULL) { output_module = modules[0]; } else { int i; for (i=0; ishortname, shortname)==0) { output_module = modules[i]; break; } } } if (output_module == NULL) { Log_error("error", "ERROR: No such output module: '%s'", shortname); return -1; } Log_info("output", "Using output module: %s (%s)", output_module->shortname, output_module->description); if (output_module->init) { return output_module->init(); } return 0; } static GMainLoop *main_loop_ = NULL; static void exit_loop_sighandler(int sig) { if (main_loop_) { // TODO(hzeller): revisit - this is not safe to do. g_main_loop_quit(main_loop_); } } int output_loop() { /* Create a main loop that runs the default GLib main context */ main_loop_ = g_main_loop_new(NULL, FALSE); signal(SIGINT, &exit_loop_sighandler); signal(SIGTERM, &exit_loop_sighandler); g_main_loop_run(main_loop_); return 0; } int output_add_options(GOptionContext *ctx) { int count, i; count = sizeof(modules) / sizeof(struct output_module *); for (i = 0; i < count; ++i) { if (modules[i]->add_options) { int result = modules[i]->add_options(ctx); if (result != 0) { return result; } } } return 0; } void output_set_uri(const char *uri, output_update_meta_cb_t meta_cb) { if (output_module && output_module->set_uri) { output_module->set_uri(uri, meta_cb); } } void output_set_next_uri(const char *uri) { if (output_module && output_module->set_next_uri) { output_module->set_next_uri(uri); } } int output_play(output_transition_cb_t transition_callback) { if (output_module && output_module->play) { return output_module->play(transition_callback); } return -1; } int output_pause(void) { if (output_module && output_module->pause) { return output_module->pause(); } return -1; } int output_stop(void) { if (output_module && output_module->stop) { return output_module->stop(); } return -1; } int output_seek(gint64 position_nanos) { if (output_module && output_module->seek) { return output_module->seek(position_nanos); } return -1; } int output_get_position(gint64 *track_dur, gint64 *track_pos) { if (output_module && output_module->get_position) { return output_module->get_position(track_dur, track_pos); } return -1; } int output_get_volume(float *value) { if (output_module && output_module->get_volume) { return output_module->get_volume(value); } return -1; } int output_set_volume(float value) { if (output_module && output_module->set_volume) { return output_module->set_volume(value); } return -1; } int output_get_mute(int *value) { if (output_module && output_module->get_mute) { return output_module->get_mute(value); } return -1; } int output_set_mute(int value) { if (output_module && output_module->set_mute) { return output_module->set_mute(value); } return -1; } gmrender-resurrect-0.0.9/src/output.h000066400000000000000000000037131400533760300176440ustar00rootroot00000000000000/* output.h - Output module frontend * * Copyright (C) 2007 Ivo Clarysse, (C) 2012 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _OUTPUT_H #define _OUTPUT_H #include #include "song-meta-data.h" // Feedback for the controlling part what is happening with the // output. enum PlayFeedback { PLAY_STOPPED, PLAY_STARTED_NEXT_STREAM, }; typedef void (*output_transition_cb_t)(enum PlayFeedback); // In case the stream gets to know details about the song, this is a // callback with changes we send back to the controlling layer. typedef void (*output_update_meta_cb_t)(const struct SongMetaData *); int output_init(const char *shortname); int output_add_options(GOptionContext *ctx); void output_dump_modules(void); int output_loop(void); void output_set_uri(const char *uri, output_update_meta_cb_t meta_info); void output_set_next_uri(const char *uri); int output_play(output_transition_cb_t done_callback); int output_stop(void); int output_pause(void); int output_get_position(gint64 *track_dur_nanos, gint64 *track_pos_nanos); int output_seek(gint64 position_nanos); int output_get_volume(float *v); int output_set_volume(float v); int output_get_mute(int *m); int output_set_mute(int m); #endif /* _OUTPUT_H */ gmrender-resurrect-0.0.9/src/output_gstreamer.c000066400000000000000000000420361400533760300217110ustar00rootroot00000000000000/* output_gstreamer.c - Output module for GStreamer * * Copyright (C) 2005-2007 Ivo Clarysse * * Adapted to gstreamer-0.10 2006 David Siorpaes * Adapted to output to snapcast 2017 Daniel Jäcksch * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include "logging.h" #include "upnp_connmgr.h" #include "output_module.h" #include "output_gstreamer.h" static double buffer_duration = 0.0; /* Buffer disbled by default, see #182 */ static void scan_mime_list(void) { GstRegistry* registry = NULL; #if (GST_VERSION_MAJOR < 1) registry = gst_registry_get_default(); #else registry = gst_registry_get(); #endif // Fetch a list of all element factories GList* features = gst_registry_get_feature_list(registry, GST_TYPE_ELEMENT_FACTORY); // Save a copy of the list root so we can properly free later GList* root = features; while (features != NULL) { GstPluginFeature* feature = GST_PLUGIN_FEATURE(features->data); // Advance list features = g_list_next(features); // Better be an element factory assert(GST_IS_ELEMENT_FACTORY(feature)); GstElementFactory* factory = GST_ELEMENT_FACTORY(feature); // Ignore elements without pads if (gst_element_factory_get_num_pad_templates(factory) == 0) continue; // Fetch a list of all pads const GList* pads = gst_element_factory_get_static_pad_templates(factory); while (pads) { GstStaticPadTemplate* padTemplate = (GstStaticPadTemplate*)pads->data; // Advance list pads = g_list_next(pads); // Skip pads that aren't sinks if (padTemplate->direction != GST_PAD_SINK) continue; // This is literally all known pad presences so it should be OK! assert(padTemplate->presence == GST_PAD_ALWAYS || padTemplate->presence == GST_PAD_SOMETIMES || padTemplate->presence == GST_PAD_REQUEST); GstCaps* capabilities = gst_static_caps_get(&padTemplate->static_caps); // Skip capabilities that they tell us nothing if (capabilities == NULL || gst_caps_is_any(capabilities) || gst_caps_is_empty(capabilities)) { gst_caps_unref(capabilities); continue; } for (guint i = 0; i < gst_caps_get_size(capabilities); i++) { GstStructure* structure = gst_caps_get_structure(capabilities, i); register_mime_type(gst_structure_get_name(structure)); } gst_caps_unref(capabilities); } } // Free any allocated memory gst_plugin_feature_list_free(root); // There seem to be all kinds of mime types out there that start with // "audio/" but are not explicitly supported by gstreamer. Let's just // tell the controller that we can handle everything "audio/*" and hope // for the best. register_mime_type("audio/*"); } static GstElement *player_ = NULL; static char *gsuri_ = NULL; // locally strdup()ed static char *gs_next_uri_ = NULL; // locally strdup()ed static struct SongMetaData song_meta_; static output_transition_cb_t play_trans_callback_ = NULL; static output_update_meta_cb_t meta_update_callback_ = NULL; struct track_time_info { gint64 duration; gint64 position; }; static struct track_time_info last_known_time_ = {0, 0}; static GstState get_current_player_state() { GstState state = GST_STATE_PLAYING; GstState pending = GST_STATE_NULL; gst_element_get_state(player_, &state, &pending, 0); return state; } static void output_gstreamer_set_next_uri(const char *uri) { Log_info("gstreamer", "Set next uri to '%s'", uri); free(gs_next_uri_); gs_next_uri_ = (uri && *uri) ? strdup(uri) : NULL; } static void output_gstreamer_set_uri(const char *uri, output_update_meta_cb_t meta_cb) { Log_info("gstreamer", "Set uri to '%s'", uri); free(gsuri_); gsuri_ = (uri && *uri) ? strdup(uri) : NULL; meta_update_callback_ = meta_cb; SongMetaData_clear(&song_meta_); } static int output_gstreamer_play(output_transition_cb_t callback) { play_trans_callback_ = callback; if (get_current_player_state() != GST_STATE_PAUSED) { if (gst_element_set_state(player_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { Log_error("gstreamer", "setting play state failed (1)"); // Error, but continue; can't get worse :) } g_object_set(G_OBJECT(player_), "uri", gsuri_, NULL); } if (gst_element_set_state(player_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { Log_error("gstreamer", "setting play state failed (2)"); return -1; } return 0; } static int output_gstreamer_stop(void) { if (gst_element_set_state(player_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { return -1; } else { return 0; } } static int output_gstreamer_pause(void) { if (gst_element_set_state(player_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) { return -1; } else { return 0; } } static int output_gstreamer_seek(gint64 position_nanos) { if (gst_element_seek(player_, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, position_nanos, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) { return -1; } else { return 0; } } #if 0 static const char *gststate_get_name(GstState state) { switch(state) { case GST_STATE_VOID_PENDING: return "VOID_PENDING"; case GST_STATE_NULL: return "NULL"; case GST_STATE_READY: return "READY"; case GST_STATE_PAUSED: return "PAUSED"; case GST_STATE_PLAYING: return "PLAYING"; default: return "Unknown"; } } #endif // This is crazy. I want C++ :) struct MetaModify { struct SongMetaData *meta; int any_change; }; static void MetaModify_add_tag(const GstTagList *list, const gchar *tag, gpointer user_data) { struct MetaModify *data = (struct MetaModify*) user_data; const char **destination = NULL; if (strcmp(tag, GST_TAG_TITLE) == 0) { destination = &data->meta->title; } else if (strcmp(tag, GST_TAG_ARTIST) == 0) { destination = &data->meta->artist; } else if (strcmp(tag, GST_TAG_ALBUM) == 0) { destination = &data->meta->album; } else if (strcmp(tag, GST_TAG_GENRE) == 0) { destination = &data->meta->genre; } else if (strcmp(tag, GST_TAG_COMPOSER) == 0) { destination = &data->meta->composer; } if (destination != NULL) { char *replace = NULL; gst_tag_list_get_string(list, tag, &replace); if (replace != NULL && (*destination == NULL || strcmp(replace, *destination) != 0)) { free((char*)*destination); *destination = replace; data->any_change++; } else { free(replace); } } } static gboolean my_bus_callback(GstBus * bus, GstMessage * msg, gpointer data) { (void)bus; (void)data; GstMessageType msgType; const GstObject *msgSrc; const gchar *msgSrcName; msgType = GST_MESSAGE_TYPE(msg); msgSrc = GST_MESSAGE_SRC(msg); msgSrcName = GST_OBJECT_NAME(msgSrc); switch (msgType) { case GST_MESSAGE_EOS: Log_info("gstreamer", "%s: End-of-stream", msgSrcName); if (gs_next_uri_ != NULL) { // If playbin does not support gapless (old // versions didn't), this will trigger. free(gsuri_); gsuri_ = gs_next_uri_; gs_next_uri_ = NULL; gst_element_set_state(player_, GST_STATE_READY); g_object_set(G_OBJECT(player_), "uri", gsuri_, NULL); gst_element_set_state(player_, GST_STATE_PLAYING); if (play_trans_callback_) { play_trans_callback_(PLAY_STARTED_NEXT_STREAM); } } else if (play_trans_callback_) { play_trans_callback_(PLAY_STOPPED); } break; case GST_MESSAGE_ERROR: { gchar *debug; GError *err; gst_message_parse_error(msg, &err, &debug); Log_error("gstreamer", "%s: Error: %s (Debug: %s)", msgSrcName, err->message, debug); g_error_free(err); g_free(debug); break; } case GST_MESSAGE_STATE_CHANGED: { GstState oldstate, newstate, pending; gst_message_parse_state_changed(msg, &oldstate, &newstate, &pending); /* g_print("GStreamer: %s: State change: '%s' -> '%s', " "PENDING: '%s'\n", msgSrcName, gststate_get_name(oldstate), gststate_get_name(newstate), gststate_get_name(pending)); */ break; } case GST_MESSAGE_TAG: { GstTagList *tags = NULL; if (meta_update_callback_ != NULL) { gst_message_parse_tag(msg, &tags); /*g_print("GStreamer: Got tags from element %s\n", GST_OBJECT_NAME (msg->src)); */ struct MetaModify modify; modify.meta = &song_meta_; modify.any_change = 0; gst_tag_list_foreach(tags, &MetaModify_add_tag, &modify); gst_tag_list_free(tags); if (modify.any_change) { meta_update_callback_(&song_meta_); } } break; } case GST_MESSAGE_BUFFERING: { if (buffer_duration <= 0.0) break; /* nothing to buffer */ gint percent = 0; gst_message_parse_buffering (msg, &percent); /* Pause playback until buffering is complete. */ if (percent < 100) gst_element_set_state(player_, GST_STATE_PAUSED); else gst_element_set_state(player_, GST_STATE_PLAYING); break; } default: /* g_print("GStreamer: %s: unhandled message type %d (%s)\n", msgSrcName, msgType, gst_message_type_get_name(msgType)); */ break; } return TRUE; } static gchar *audio_sink = NULL; static gchar *audio_device = NULL; static gchar *audio_pipe = NULL; static gchar *videosink = NULL; static double initial_db = 0.0; /* Options specific to output_gstreamer */ static GOptionEntry option_entries[] = { { "gstout-audiosink", 0, 0, G_OPTION_ARG_STRING, &audio_sink, "GStreamer audio sink to use " "(autoaudiosink, alsasink, osssink, esdsink, ...)", NULL }, { "gstout-audiodevice", 0, 0, G_OPTION_ARG_STRING, &audio_device, "GStreamer device for the given audiosink. ", NULL }, { "gstout-audiopipe", 0, 0, G_OPTION_ARG_STRING, &audio_pipe, "GStreamer audio sink to pipeline" "(gst-launch format) useful for further output format conversion.", NULL }, { "gstout-videosink", 0, 0, G_OPTION_ARG_STRING, &videosink, "GStreamer video sink to use " "(autovideosink, xvimagesink, ximagesink, ...)", NULL }, { "gstout-buffer-duration", 0, 0, G_OPTION_ARG_DOUBLE, &buffer_duration, "The size of the buffer in seconds. Set to zero to disable buffering.", NULL }, { "gstout-initial-volume-db", 0, 0, G_OPTION_ARG_DOUBLE, &initial_db, "GStreamer initial volume in decibel (e.g. 0.0 = max; -6 = 1/2 max) ", NULL }, { NULL } }; static int output_gstreamer_add_options(GOptionContext *ctx) { GOptionGroup *option_group; option_group = g_option_group_new("gstout", "GStreamer Output Options", "Show GStreamer Output Options", NULL, NULL); g_option_group_add_entries(option_group, option_entries); g_option_context_add_group (ctx, option_group); g_option_context_add_group (ctx, gst_init_get_option_group ()); return 0; } static int output_gstreamer_get_position(gint64 *track_duration, gint64 *track_pos) { *track_duration = last_known_time_.duration; *track_pos = last_known_time_.position; int rc = 0; if (get_current_player_state() != GST_STATE_PLAYING) { return rc; // playbin2 only returns valid values then. } #if (GST_VERSION_MAJOR < 1) GstFormat fmt = GST_FORMAT_TIME; GstFormat* query_type = &fmt; #else GstFormat query_type = GST_FORMAT_TIME; #endif if (!gst_element_query_duration(player_, query_type, track_duration)) { Log_error("gstreamer", "Failed to get track duration."); rc = -1; } if (!gst_element_query_position(player_, query_type, track_pos)) { Log_error("gstreamer", "Failed to get track pos"); rc = -1; } // playbin2 does not allow to query while paused. Remember in case // we're asked then (it actually returns something, but it is bogus). last_known_time_.duration = *track_duration; last_known_time_.position = *track_pos; return rc; } static int output_gstreamer_get_volume(float *v) { double volume; g_object_get(player_, "volume", &volume, NULL); Log_info("gstreamer", "Query volume fraction: %f", volume); *v = volume; return 0; } static int output_gstreamer_set_volume(float value) { Log_info("gstreamer", "Set volume fraction to %f", value); g_object_set(player_, "volume", (double) value, NULL); return 0; } static int output_gstreamer_get_mute(int *m) { gboolean val; g_object_get(player_, "mute", &val, NULL); *m = val; return 0; } static int output_gstreamer_set_mute(int m) { Log_info("gstreamer", "Set mute to %s", m ? "on" : "off"); g_object_set(player_, "mute", (gboolean) m, NULL); return 0; } static void prepare_next_stream(GstElement *obj, gpointer userdata) { (void)obj; (void)userdata; Log_info("gstreamer", "about-to-finish cb: setting uri %s", gs_next_uri_); free(gsuri_); gsuri_ = gs_next_uri_; gs_next_uri_ = NULL; if (gsuri_ != NULL) { g_object_set(G_OBJECT(player_), "uri", gsuri_, NULL); if (play_trans_callback_) { // TODO(hzeller): can we figure out when we _actually_ // start playing this ? there are probably a couple // of seconds between now and actual start. play_trans_callback_(PLAY_STARTED_NEXT_STREAM); } } } static int output_gstreamer_init(void) { GstBus *bus; SongMetaData_init(&song_meta_); scan_mime_list(); #if (GST_VERSION_MAJOR < 1) const char player_element_name[] = "playbin2"; #else const char player_element_name[] = "playbin"; #endif player_ = gst_element_factory_make(player_element_name, "play"); assert(player_ != NULL); /* set buffer size */ if (buffer_duration > 0) { gint64 buffer_duration_ns = round(buffer_duration * 1.0e9); Log_info("gstreamer", "Setting buffer duration to %" PRId64 "ms", buffer_duration_ns / 1000000); g_object_set(G_OBJECT(player_), "buffer-duration", buffer_duration_ns, NULL); } else { Log_info("gstreamer", "Buffering disabled (--gstout-buffer-duration)"); } bus = gst_pipeline_get_bus(GST_PIPELINE(player_)); gst_bus_add_watch(bus, my_bus_callback, NULL); gst_object_unref(bus); if (audio_sink != NULL && audio_pipe != NULL) { Log_error("gstreamer", "--gstout-audosink and --gstout-audiopipe are mutually exclusive."); return 1; } if (audio_sink != NULL) { GstElement *sink = NULL; Log_info("gstreamer", "Setting audio sink to %s; device=%s\n", audio_sink, audio_device ? audio_device : ""); sink = gst_element_factory_make (audio_sink, "sink"); if (sink == NULL) { Log_error("gstreamer", "Couldn't create sink '%s'", audio_sink); } else { if (audio_device != NULL) { g_object_set (G_OBJECT(sink), "device", audio_device, NULL); } g_object_set (G_OBJECT (player_), "audio-sink", sink, NULL); } } if (audio_pipe != NULL) { GstElement *sink = NULL; Log_info("gstreamer", "Setting audio sink-pipeline to %s\n",audio_pipe); sink = gst_parse_bin_from_description(audio_pipe, TRUE, NULL); if (sink == NULL) { Log_error("gstreamer", "Could not create pipeline."); } else { g_object_set (G_OBJECT (player_), "audio-sink", sink, NULL); } } if (videosink != NULL) { GstElement *sink = NULL; Log_info("gstreamer", "Setting video sink to %s", videosink); sink = gst_element_factory_make (videosink, "sink"); g_object_set (G_OBJECT (player_), "video-sink", sink, NULL); } if (gst_element_set_state(player_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { Log_error("gstreamer", "Error: pipeline doesn't become ready."); } g_signal_connect(G_OBJECT(player_), "about-to-finish", G_CALLBACK(prepare_next_stream), NULL); output_gstreamer_set_mute(0); if (initial_db < 0) { output_gstreamer_set_volume(exp(initial_db / 20 * log(10))); } return 0; } struct output_module gstreamer_output = { .shortname = "gst", .description = "GStreamer multimedia framework", .add_options = output_gstreamer_add_options, .init = output_gstreamer_init, .set_uri = output_gstreamer_set_uri, .set_next_uri= output_gstreamer_set_next_uri, .play = output_gstreamer_play, .stop = output_gstreamer_stop, .pause = output_gstreamer_pause, .seek = output_gstreamer_seek, .get_position = output_gstreamer_get_position, .get_volume = output_gstreamer_get_volume, .set_volume = output_gstreamer_set_volume, .get_mute = output_gstreamer_get_mute, .set_mute = output_gstreamer_set_mute, }; gmrender-resurrect-0.0.9/src/output_gstreamer.h000066400000000000000000000020061400533760300217070ustar00rootroot00000000000000/* output_gstreamer.h - Definitions for GStreamer output module * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _OUTPUT_GSTREAMER_H #define _OUTPUT_GSTREAMER_H extern struct output_module gstreamer_output; #endif /* _OUTPUT_GSTREAMER_H */ gmrender-resurrect-0.0.9/src/output_module.h000066400000000000000000000030231400533760300212030ustar00rootroot00000000000000/* output_module.h - Output module interface definition * * Copyright (C) 2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _OUTPUT_MODULE_H #define _OUTPUT_MODULE_H #include "output.h" struct output_module { const char *shortname; const char *description; int (*add_options)(GOptionContext *ctx); // Commands. int (*init)(void); void (*set_uri)(const char *uri, output_update_meta_cb_t meta_info); void (*set_next_uri)(const char *uri); int (*play)(output_transition_cb_t transition_callback); int (*stop)(void); int (*pause)(void); int (*seek)(gint64 position_nanos); // parameters int (*get_position)(gint64 *track_duration, gint64 *track_pos); int (*get_volume)(float *); int (*set_volume)(float); int (*get_mute)(int *); int (*set_mute)(int); }; #endif gmrender-resurrect-0.0.9/src/song-meta-data.c000066400000000000000000000162111400533760300210750ustar00rootroot00000000000000/* song-meta-data - Object holding meta data for a song. * * Copyright (C) 2012 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ // TODO: we're assuming that the namespaces are abbreviated with 'dc' and 'upnp' // ... but if I understand that correctly, that doesn't need to be the case. #include "song-meta-data.h" #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include "xmlescape.h" #include "xmldoc.h" void SongMetaData_init(struct SongMetaData *value) { memset(value, 0, sizeof(struct SongMetaData)); } void SongMetaData_clear(struct SongMetaData *value) { free((char*)value->title); value->title = NULL; free((char*)value->artist); value->artist = NULL; free((char*)value->album); value->album = NULL; free((char*)value->genre); value->genre = NULL; } static const char kDidlHeader[] = ""; static const char kDidlFooter[] = ""; // Allocates a new DIDL formatted XML and fill it with given data. // The input fields are expected to be already xml escaped. static char *generate_DIDL(const char *id, const char *title, const char *artist, const char *album, const char *genre, const char *composer) { char *result = NULL; int ret = asprintf(&result, "%s\n\n" "\t%s\n" "\t%s\n" "\t%s\n" "\t%s\n" "\t%s\n" "\n%s", kDidlHeader, id, title ? title : "", artist ? artist : "", album ? album : "", genre ? genre : "", composer ? composer : "", kDidlFooter); return ret >= 0 ? result : NULL; } // Takes input, if it finds the given tag, then replaces the content between // these with 'content'. It might re-allocate the original string; only the // returned string is valid. // updates "edit_count" if there was a change. // Very crude way to edit XML. static char *replace_range(char *const input, const char *tag_start, const char *tag_end, const char *content, int *edit_count) { if (content == NULL) // unknown content; document unchanged. return input; const int total_len = strlen(input); const char *start_pos = strstr(input, tag_start); if (start_pos == NULL) return input; start_pos += strlen(tag_start); const int offset = start_pos - input; const char *end_pos = strstr(start_pos, tag_end); if (end_pos == NULL) return input; const int old_content_len = end_pos - start_pos; const int new_content_len = strlen(content); char *result = NULL; if (old_content_len != new_content_len) { result = (char*)malloc(total_len + new_content_len - old_content_len + 1); memcpy(result, input, start_pos - input); memcpy(result + offset, content, new_content_len); strcpy(result + offset + new_content_len, end_pos); // remaining free(input); ++*edit_count; } else { // Typically, we replace the same content with itself - same // length. No realloc in this case. if (strncmp(start_pos, content, new_content_len) != 0) { memcpy(input + offset, content, new_content_len); ++*edit_count; } result = input; } return result; } int SongMetaData_parse_DIDL(struct SongMetaData *object, const char *xml) { struct xmldoc *doc = xmldoc_parsexml(xml); if (doc == NULL) return 0; // ... did I mention that I hate navigating XML documents ? struct xmlelement *didl_node = find_element_in_doc(doc, "DIDL-Lite"); if (didl_node == NULL) return 0; struct xmlelement *item_node = find_element_in_element(didl_node, "item"); if (item_node == NULL) return 0; struct xmlelement *value_node = NULL; value_node = find_element_in_element(item_node, "dc:title"); if (value_node) object->title = get_node_value(value_node); value_node = find_element_in_element(item_node, "upnp:artist"); if (value_node) object->artist = get_node_value(value_node); value_node = find_element_in_element(item_node, "upnp:album"); if (value_node) object->album = get_node_value(value_node); value_node = find_element_in_element(item_node, "upnp:genre"); if (value_node) object->genre = get_node_value(value_node); xmldoc_free(doc); return 1; } // TODO: actually use some XML library for this, but spending too much time // with XML is not good for the brain :) Worst thing that came out of the 90ies. char *SongMetaData_to_DIDL(const struct SongMetaData *object, const char *original_xml) { // Generating a unique ID in case the players cache the content by // the item-ID. Right now this is experimental and not known to make // any difference - it seems that players just don't display changes // in the input stream. Grmbl. static unsigned int xml_id = 42; char unique_id[4 + 8 + 1]; snprintf(unique_id, sizeof(unique_id), "gmr-%08x", xml_id++); char *result; char *title, *artist, *album, *genre, *composer; title = object->title ? xmlescape(object->title, 0) : NULL; artist = object->artist ? xmlescape(object->artist, 0) : NULL; album = object->album ? xmlescape(object->album, 0) : NULL; genre = object->genre ? xmlescape(object->genre, 0) : NULL; composer = object->composer ? xmlescape(object->composer, 0) : NULL; if (original_xml == NULL || strlen(original_xml) == 0) { result = generate_DIDL(unique_id, title, artist, album, genre, composer); } else { int edits = 0; // Otherwise, surgically edit the original document to give // control points as close as possible what they sent themself. result = strdup(original_xml); result = replace_range(result, "", "", title, &edits); result = replace_range(result, "", "", artist, &edits); result = replace_range(result, "", "", album, &edits); result = replace_range(result, "", "", genre, &edits); result = replace_range(result, "", "", composer, &edits); if (edits) { // Only if we changed the content, we generate a new // unique id. result = replace_range(result, " id=\"", "\"", unique_id, &edits); } } free(title); free(artist); free(album); free(genre); free(composer); return result; } gmrender-resurrect-0.0.9/src/song-meta-data.h000066400000000000000000000033471400533760300211100ustar00rootroot00000000000000/* song-meta-data - Object holding meta data for a song. * * Copyright (C) 2012 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _SONG_META_DATA_H #define _SONG_META_DATA_H // An 'object' dealing with the meta data of a song. struct SongMetaData { const char *title; const char *artist; const char *album; const char *genre; const char *composer; }; // Construct song meta data object. void SongMetaData_init(struct SongMetaData *object); // Clear meta data strings and deallocate them. void SongMetaData_clear(struct SongMetaData *object); // Returns a newly allocated xml string with the song meta data encoded as // DIDL-Lite. If we get a non-empty original xml document, returns an // edited version of that document. char *SongMetaData_to_DIDL(const struct SongMetaData *object, const char *original_xml); // Parse DIDL-Lite and fill SongMetaData struct. Returns 1 when successful. int SongMetaData_parse_DIDL(struct SongMetaData *object, const char *xml); #endif // _SONG_META_DATA_H gmrender-resurrect-0.0.9/src/upnp_compat.h000066400000000000000000000161641400533760300206350ustar00rootroot00000000000000/* upnp_compat.h - libupnp v1.8.x/v1.6.x compatibilty layer * * Copyright (C) 2019 Tucker Kern * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_COMPAT_H #define _UPNP_COMPAT_H #include #include #if UPNP_VERSION >= 10803 #define UpnpAddVirtualDir(x) UpnpAddVirtualDir(x, NULL, NULL) #define VD_GET_INFO_CALLBACK(NAME, FILENAME, INFO, COOKIE) int NAME(const char* FILENAME, UpnpFileInfo* INFO, const void* COOKIE) #define VD_OPEN_CALLBACK(NAME, FILENAME, MODE, COOKIE) UpnpWebFileHandle NAME(const char* FILENAME, enum UpnpOpenFileMode MODE, const void* COOKIE) #define VD_READ_CALLBACK(NAME, HANDLE, BUFFER, LENGTH, COOKIE) int NAME(UpnpWebFileHandle HANDLE, char* BUFFER, size_t LENGTH, const void* COOKIE) #define VD_WRITE_CALLBACK(...) VD_READ_CALLBACK(__VA_ARGS__) #define VD_SEEK_CALLBACK(NAME, HANDLE, OFFSET, ORIGIN, COOKIE) int NAME(UpnpWebFileHandle HANDLE, off_t OFFSET, int ORIGIN, const void* COOKIE) #define VD_CLOSE_CALLBACK(NAME, HANDLE, COOKIE) int NAME(UpnpWebFileHandle HANDLE, const void* COOKIE) #else #define VD_GET_INFO_CALLBACK(NAME, FILENAME, INFO, COOKIE) int NAME(const char* FILENAME, UpnpFileInfo* INFO) #define VD_OPEN_CALLBACK(NAME, FILENAME, MODE, COOKIE) UpnpWebFileHandle NAME(const char* FILENAME, enum UpnpOpenFileMode MODE) #define VD_READ_CALLBACK(NAME, HANDLE, BUFFER, LENGTH, COOKIE) int NAME(UpnpWebFileHandle HANDLE, char* BUFFER, size_t LENGTH) #define VD_WRITE_CALLBACK(...) VD_READ_CALLBACK(__VA_ARGS__) #define VD_SEEK_CALLBACK(NAME, HANDLE, OFFSET, ORIGIN, COOKIE) int NAME(UpnpWebFileHandle HANDLE, off_t OFFSET, int ORIGIN) #define VD_CLOSE_CALLBACK(NAME, HANDLE, COOKIE) int NAME(UpnpWebFileHandle HANDLE) #endif #if UPNP_VERSION >= 10800 #define UPNP_CALLBACK(NAME, TYPE, EVENT, COOKIE) int NAME(Upnp_EventType TYPE, const void* EVENT, void* COOKIE) #else #define UPNP_CALLBACK(NAME, TYPE, EVENT, COOKIE) int NAME(Upnp_EventType TYPE, void* EVENT, void* COOKIE) #endif #if UPNP_VERSION < 10626 // Compatibility defines from libupnp 1.6.26 to allow code targeting v1.8.x // to compile for v1.6.x /* compat code for libupnp-1.8 */ typedef struct Upnp_Action_Request UpnpActionRequest; #define UpnpActionRequest_get_ErrCode(x) ((x)->ErrCode) #define UpnpActionRequest_set_ErrCode(x, v) ((x)->ErrCode = (v)) #define UpnpActionRequest_get_Socket(x) ((x)->Socket) #define UpnpActionRequest_get_ErrStr_cstr(x) ((x)->ErrStr) #define UpnpActionRequest_set_ErrStr(x, v) (strncpy((x)->ErrStr, UpnpString_get_String((v)), LINE_SIZE)) #define UpnpActionRequest_get_ActionName_cstr(x) ((x)->ActionName) #define UpnpActionRequest_get_DevUDN_cstr(x) ((x)->DevUDN) #define UpnpActionRequest_get_ServiceID_cstr(x) ((x)->ServiceID) #define UpnpActionRequest_get_ActionRequest(x) ((x)->ActionRequest) #define UpnpActionRequest_set_ActionRequest(x, v) ((x)->ActionRequest = (v)) #define UpnpActionRequest_get_ActionResult(x) ((x)->ActionResult) #define UpnpActionRequest_set_ActionResult(x, v) ((x)->ActionResult = (v)) /* compat code for libupnp-1.8 */ typedef struct Upnp_Action_Complete UpnpActionComplete; #define UpnpActionComplete_get_ErrCode(x) ((x)->ErrCode) #define UpnpActionComplete_get_CtrlUrl_cstr(x) ((x)->CtrlUrl) #define UpnpActionComplete_get_ActionRequest(x) ((x)->ActionRequest) #define UpnpActionComplete_get_ActionResult(x) ((x)->ActionResult) /* compat code for libupnp-1.8 */ typedef struct Upnp_State_Var_Request UpnpStateVarRequest; #define UpnpStateVarRequest_get_ErrCode(x) ((x)->ErrCode) #define UpnpStateVarRequest_set_ErrCode(x, v) ((x)->ErrCode = (v)) #define UpnpStateVarRequest_get_Socket(x) ((x)->Socket) #define UpnpStateVarRequest_get_ErrStr_cstr(x) ((x)->ErrStr) #define UpnpStateVarRequest_get_DevUDN_cstr(x) ((x)->DevUDN) #define UpnpStateVarRequest_get_ServiceID_cstr(x) ((x)->ServiceID) #define UpnpStateVarRequest_get_StateVarName_cstr(x) ((x)->StateVarName) #define UpnpStateVarRequest_get_CurrentVal(x) ((x)->CurrentVal) #define UpnpStateVarRequest_set_CurrentVal(x, v) ((x)->CurrentVal = (v)) /* compat code for libupnp-1.8 */ typedef struct Upnp_State_Var_Complete UpnpStateVarComplete; #define UpnpStateVarComplete_get_ErrCode(x) ((x)->ErrCode) #define UpnpStateVarComplete_get_CtrlUrl_cstr(x) ((x)->CtrlUrl) #define UpnpStateVarComplete_get_StateVarName_cstr(x) ((x)->StateVarName) /* compat code for libupnp-1.8 */ typedef struct Upnp_Event UpnpEvent; #define UpnpEvent_get_SID_cstr(x) ((x)->Sid) #define UpnpEvent_get_EventKey(x) ((x)->EventKey) #define UpnpEvent_get_ChangedVariables(x) ((x)->ChangedVariables) /* compat code for libupnp-1.8 */ typedef struct Upnp_Discovery UpnpDiscovery; #define UpnpDiscovery_get_ErrCode(x) ((x)->ErrCode) #define UpnpDiscovery_get_Expires(x) ((x)->Expires) #define UpnpDiscovery_get_DeviceID_cstr(x) ((x)->DeviceId) #define UpnpDiscovery_get_DeviceType_cstr(x) ((x)->DeviceType) #define UpnpDiscovery_get_ServiceType_cstr(x) ((x)->ServiceType) #define UpnpDiscovery_get_ServiceVer_cstr(x) ((x)->ServiceVer) #define UpnpDiscovery_get_Location_cstr(x) ((x)->Location) #define UpnpDiscovery_get_Os_cstr(x) ((x)->Os) #define UpnpDiscovery_get_Date_cstr(x) ((x)->Date) #define UpnpDiscovery_get_Ext_cstr(x) ((x)->Ext) /* compat code for libupnp-1.8 */ typedef struct Upnp_Event_Subscribe UpnpEventSubscribe; #define UpnpEventSubscribe_get_SID_cstr(x) ((x)->Sid) #define UpnpEventSubscribe_get_ErrCode(x) ((x)->ErrCode) #define UpnpEventSubscribe_get_PublisherUrl_cstr(x) ((x)->PublisherUrl) #define UpnpEventSubscribe_get_TimeOut(x) ((x)->TimeOut) /* compat code for libupnp-1.8 */ typedef struct Upnp_Subscription_Request UpnpSubscriptionRequest; #define UpnpSubscriptionRequest_get_ServiceId_cstr(x) ((x)->ServiceId) #define UpnpSubscriptionRequest_get_UDN_cstr(x) ((x)->UDN) #define UpnpSubscriptionRequest_get_SID_cstr(x) ((x)->Sid) /* compat code for libupnp-1.8 */ typedef struct File_Info UpnpFileInfo; #define UpnpFileInfo_get_FileLength(x) ((x)->file_length) #define UpnpFileInfo_set_FileLength(x, v) ((x)->file_length = (v)) #define UpnpFileInfo_get_LastModified(x) ((x)->last_modified) #define UpnpFileInfo_set_LastModified(x, v) ((x)->last_modified = (v)) #define UpnpFileInfo_get_IsDirectory(x) ((x)->is_directory) #define UpnpFileInfo_set_IsDirectory(x, v) ((x)->is_directory = (v)) #define UpnpFileInfo_get_IsReadable(x) ((x)->is_readable) #define UpnpFileInfo_set_IsReadable(x, v) ((x)->is_readable = (v)) #define UpnpFileInfo_get_ContentType(x) ((x)->content_type) #define UpnpFileInfo_set_ContentType(x, v) ((x)->content_type = (v)) #endif #endif /* _UPNP_COMPAT_H */ gmrender-resurrect-0.0.9/src/upnp_connmgr.c000066400000000000000000000347551400533760300210160ustar00rootroot00000000000000/* upnp_connmgr.c - UPnP Connection Manager routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include // Can't include above upnp.h breaks stdbool? #include #include #include "upnp_connmgr.h" #include "logging.h" #include "upnp_service.h" #include "upnp_device.h" #include "variable-container.h" #define CONNMGR_TYPE "urn:schemas-upnp-org:service:ConnectionManager:1" // Changing this back now to what it is supposed to be, let's see what happens. // For some reason (predates me), this was explicitly commented out and // set to the service type; were there clients that were confused about the // right use of the service-ID ? Setting this back, let's see what happens. #define CONNMGR_SERVICE_ID "urn:upnp-org:serviceId:ConnectionManager" //#define CONNMGR_SERVICE_ID CONNMGR_TYPE #define CONNMGR_SCPD_URL "/upnp/renderconnmgrSCPD.xml" #define CONNMGR_CONTROL_URL "/upnp/control/renderconnmgr1" #define CONNMGR_EVENT_URL "/upnp/event/renderconnmgr1" typedef enum { CONNMGR_VAR_AAT_CONN_MGR, CONNMGR_VAR_SINK_PROTO_INFO, CONNMGR_VAR_AAT_CONN_STATUS, CONNMGR_VAR_AAT_AVT_ID, CONNMGR_VAR_AAT_DIR, CONNMGR_VAR_AAT_RCS_ID, CONNMGR_VAR_AAT_PROTO_INFO, CONNMGR_VAR_AAT_CONN_ID, CONNMGR_VAR_SRC_PROTO_INFO, CONNMGR_VAR_CUR_CONN_IDS, CONNMGR_VAR_COUNT } connmgr_variable; typedef enum { CONNMGR_CMD_GETCURRENTCONNECTIONIDS, CONNMGR_CMD_SETCURRENTCONNECTIONINFO, CONNMGR_CMD_GETPROTOCOLINFO, CONNMGR_CMD_PREPAREFORCONNECTION, //CONNMGR_CMD_CONNECTIONCOMPLETE, CONNMGR_CMD_COUNT } connmgr_cmd; static struct argument arguments_getprotocolinfo[] = { { "Source", PARAM_DIR_OUT, CONNMGR_VAR_SRC_PROTO_INFO }, { "Sink", PARAM_DIR_OUT, CONNMGR_VAR_SINK_PROTO_INFO }, { NULL }, }; static struct argument arguments_getcurrentconnectionids[] = { { "ConnectionIDs", PARAM_DIR_OUT, CONNMGR_VAR_CUR_CONN_IDS }, { NULL } }; static struct argument arguments_setcurrentconnectioninfo[] = { { "ConnectionID", PARAM_DIR_IN, CONNMGR_VAR_AAT_CONN_ID }, { "RcsID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_RCS_ID }, { "AVTransportID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_AVT_ID }, { "ProtocolInfo", PARAM_DIR_OUT, CONNMGR_VAR_AAT_PROTO_INFO }, { "PeerConnectionManager", PARAM_DIR_OUT, CONNMGR_VAR_AAT_CONN_MGR }, { "PeerConnectionID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_CONN_ID }, { "Direction", PARAM_DIR_OUT, CONNMGR_VAR_AAT_DIR }, { "Status", PARAM_DIR_OUT, CONNMGR_VAR_AAT_CONN_STATUS }, { NULL } }; static struct argument arguments_prepareforconnection[] = { { "RemoteProtocolInfo", PARAM_DIR_IN, CONNMGR_VAR_AAT_PROTO_INFO }, { "PeerConnectionManager", PARAM_DIR_IN, CONNMGR_VAR_AAT_CONN_MGR }, { "PeerConnectionID", PARAM_DIR_IN, CONNMGR_VAR_AAT_CONN_ID }, { "Direction", PARAM_DIR_IN, CONNMGR_VAR_AAT_DIR }, { "ConnectionID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_CONN_ID }, { "AVTransportID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_AVT_ID }, { "RcsID", PARAM_DIR_OUT, CONNMGR_VAR_AAT_RCS_ID }, { NULL } }; //static struct argument *arguments_connectioncomplete[] = { // { "ConnectionID", PARAM_DIR_IN, CONNMGR_VAR_AAT_CONN_ID }, // NULL //}; static struct argument *argument_list[] = { [CONNMGR_CMD_GETCURRENTCONNECTIONIDS] = arguments_getcurrentconnectionids, [CONNMGR_CMD_SETCURRENTCONNECTIONINFO] = arguments_setcurrentconnectioninfo, [CONNMGR_CMD_GETPROTOCOLINFO] = arguments_getprotocolinfo, [CONNMGR_CMD_PREPAREFORCONNECTION] = arguments_prepareforconnection, //[CONNMGR_CMD_CONNECTIONCOMPLETE] = arguments_connectioncomplete, [CONNMGR_CMD_COUNT] = NULL }; static const char *connstatus_values[] = { "OK", "ContentFormatMismatch", "InsufficientBandwidth", "UnreliableChannel", "Unknown", NULL }; static const char *direction_values[] = { "Input", "Output", NULL }; static ithread_mutex_t connmgr_mutex; static GSList* supported_types_list; static bool add_mime_type(const char* mime_type) { // Check for duplicate MIME type if (g_slist_find_custom(supported_types_list, mime_type, (GCompareFunc) strcmp) != NULL) return false; // Sorted insert into list supported_types_list = g_slist_insert_sorted(supported_types_list, strdup(mime_type), (GCompareFunc) strcmp); return true; } static bool remove_mime_type(const char* mime_type) { // Check that the list exists if (supported_types_list == NULL) return false; // Search for the MIME type GSList* entry = g_slist_find_custom(supported_types_list, mime_type, (GCompareFunc) strcmp); if (entry != NULL) { // Free the string pointer free(entry->data); // Free the list entry supported_types_list = g_slist_delete_link(supported_types_list, entry); return true; } return false; } static gint g_compare_mime_root(gconstpointer a, gconstpointer b) { size_t aLen = strlen((const char*)a); size_t bLen = strlen((const char*)b); // Only compare up to the small string int min = (aLen < bLen) ? aLen : bLen; return strncmp((const char*) a, (const char*) b, min); } static void g_add_mime_type(gpointer data, gpointer user_data) { add_mime_type((const char*) data); } static void g_remove_mime_type(gpointer data, gpointer user_data) { remove_mime_type((const char*) data); } static void register_mime_type_internal(const char *mime_type) { add_mime_type(mime_type); } void register_mime_type(const char *mime_type) { register_mime_type_internal(mime_type); if (strcmp("audio/mpeg", mime_type) == 0) { register_mime_type_internal("audio/x-mpeg"); // BubbleUPnP does not seem to match generic "audio/*" types, // but only matches mime-types _exactly_, so we add some here. // TODO(hzeller): we already add the "audio/*" mime-type // output_gstream.c:scan_caps() which should just work once // BubbleUPnP allows for matching "audio/*". Remove the code // here. // BubbleUPnP uses audio/x-scpl as an indicator to know if the // renderer can handle it (otherwise it will proxy). // Simple claim: if we can handle mpeg, then we can handle // shoutcast. // (For more accurate answer: we'd to check if all of // mpeg, aac, aacp, ogg are supported). register_mime_type_internal("audio/x-scpls"); // This is apparently something sent by the spotifyd // https://gitorious.org/spotifyd register_mime_type("audio/L16;rate=44100;channels=2"); } // Some workaround: some controllers seem to match the version without // x-, some with; though the mime-type is correct with x-, these formats // seem to be common enough to sometimes be used without. // If this works, we should probably collect all of these // in a set emit always both, foo/bar and foo/x-bar, as it is a similar // work-around as seen above with mpeg -> x-mpeg. if (strcmp("audio/x-alac", mime_type) == 0) { register_mime_type_internal("audio/alac"); } if (strcmp("audio/x-aiff", mime_type) == 0) { register_mime_type_internal("audio/aiff"); } if (strcmp("audio/x-m4a", mime_type) == 0) { register_mime_type_internal("audio/m4a"); register_mime_type_internal("audio/mp4"); } } static mime_type_filters_t connmgr_parse_mime_filter_string(const char* filter_string) { mime_type_filters_t mime_filter; mime_filter.allowed_roots = NULL; mime_filter.added_types = NULL; mime_filter.removed_types = NULL; if (filter_string == NULL) return mime_filter; char* filters = strdup(filter_string); char* saveptr = NULL; // State pointer for strtok_r char* token = strtok_r(filters, ",", &saveptr); while(token != NULL) { if (token[0] == '+') { mime_filter.added_types = g_slist_prepend(mime_filter.added_types, strdup(&token[1])); } else if (token[0] == '-') { mime_filter.removed_types = g_slist_prepend(mime_filter.removed_types, strdup(&token[1])); } else { mime_filter.allowed_roots = g_slist_prepend(mime_filter.allowed_roots, strdup(token)); } token = strtok_r(NULL, ",", &saveptr); } free(filters); return mime_filter; } static void connmgr_filter_mime_type_root(const mime_type_filters_t* mime_filter) { if (mime_filter == NULL || mime_filter->allowed_roots == NULL) return; // Iterate through the supported types and filter by root GSList* entry = supported_types_list; while (entry != NULL) { GSList* next = entry->next; if (g_slist_find_custom(mime_filter->allowed_roots, entry->data, g_compare_mime_root) == NULL) { // Free matching MIME type and remove the entry free(entry->data); supported_types_list = g_slist_delete_link(supported_types_list, entry); } entry = next; } } int connmgr_init(const char* mime_filter_string) { struct service *srv = upnp_connmgr_get_service(); // Parse MIME filter into separate fields mime_type_filters_t mime_filter = connmgr_parse_mime_filter_string(mime_filter_string); // Filter MIME types by root connmgr_filter_mime_type_root(&mime_filter); // Manually add additional MIME types g_slist_foreach(mime_filter.added_types, g_add_mime_type, NULL); // Manually remove specific MIME types g_slist_foreach(mime_filter.removed_types, g_remove_mime_type, NULL); GString* protoInfo = g_string_new(NULL); for (GSList* entry = supported_types_list; entry != NULL; entry = g_slist_next(entry)) { Log_info("connmgr", "Registering support for '%s'", (const char*) entry->data); g_string_append_printf(protoInfo, "http-get:*:%s:*,", (const char*) entry->data); } if (protoInfo->len > 0) { // Truncate final comma protoInfo = g_string_truncate(protoInfo, protoInfo->len - 1); VariableContainer_change(srv->variable_container, CONNMGR_VAR_SINK_PROTO_INFO, protoInfo->str); } // Free string and its data g_string_free(protoInfo, TRUE); // Free all lists that were generated g_slist_free_full(supported_types_list, free); g_slist_free_full(mime_filter.allowed_roots, free); g_slist_free_full(mime_filter.added_types, free); g_slist_free_full(mime_filter.removed_types, free); return 0; } static int get_protocol_info(struct action_event *event) { upnp_append_variable(event, CONNMGR_VAR_SRC_PROTO_INFO, "Source"); upnp_append_variable(event, CONNMGR_VAR_SINK_PROTO_INFO, "Sink"); return event->status; } static int get_current_conn_ids(struct action_event *event) { int rc = -1; upnp_add_response(event, "ConnectionIDs", "0"); ///rc = upnp_append_variable(event, CONNMGR_VAR_CUR_CONN_IDS, // "ConnectionIDs"); return rc; } static int prepare_for_connection(struct action_event *event) { upnp_append_variable(event, CONNMGR_VAR_CUR_CONN_IDS, "ConnectionID"); upnp_append_variable(event, CONNMGR_VAR_AAT_AVT_ID, "AVTransportID"); upnp_append_variable(event, CONNMGR_VAR_AAT_RCS_ID, "RcsID"); return 0; } static int get_current_conn_info(struct action_event *event) { const char *value = upnp_get_string(event, "ConnectionID"); if (value == NULL) { return -1; } Log_info("connmgr", "Query ConnectionID='%s'", value); upnp_append_variable(event, CONNMGR_VAR_AAT_RCS_ID, "RcsID"); upnp_append_variable(event, CONNMGR_VAR_AAT_AVT_ID, "AVTransportID"); upnp_append_variable(event, CONNMGR_VAR_AAT_PROTO_INFO, "ProtocolInfo"); upnp_append_variable(event, CONNMGR_VAR_AAT_CONN_MGR, "PeerConnectionManager"); upnp_append_variable(event, CONNMGR_VAR_AAT_CONN_ID, "PeerConnectionID"); upnp_append_variable(event, CONNMGR_VAR_AAT_DIR, "Direction"); upnp_append_variable(event, CONNMGR_VAR_AAT_CONN_STATUS, "Status"); return 0; } static struct action connmgr_actions[] = { [CONNMGR_CMD_GETCURRENTCONNECTIONIDS] = {"GetCurrentConnectionIDs", get_current_conn_ids}, [CONNMGR_CMD_SETCURRENTCONNECTIONINFO] ={"GetCurrentConnectionInfo", get_current_conn_info}, [CONNMGR_CMD_GETPROTOCOLINFO] = {"GetProtocolInfo", get_protocol_info}, [CONNMGR_CMD_PREPAREFORCONNECTION] = {"PrepareForConnection", prepare_for_connection}, /* optional */ //[CONNMGR_CMD_CONNECTIONCOMPLETE] = {"ConnectionComplete", NULL}, /* optional */ [CONNMGR_CMD_COUNT] = {NULL, NULL} }; struct service *upnp_connmgr_get_service(void) { static struct service connmgr_service_ = { .service_mutex = &connmgr_mutex, .service_id = CONNMGR_SERVICE_ID, .service_type = CONNMGR_TYPE, .scpd_url = CONNMGR_SCPD_URL, .control_url = CONNMGR_CONTROL_URL, .event_url = CONNMGR_EVENT_URL, .event_xml_ns = NULL, // we never send change events. .actions = connmgr_actions, .action_arguments = argument_list, .variable_container = NULL, // initialized below .last_change = NULL, .command_count = CONNMGR_CMD_COUNT, }; static struct var_meta connmgr_var_meta[] = { { CONNMGR_VAR_SRC_PROTO_INFO, "SourceProtocolInfo", "", EV_YES, DATATYPE_STRING, NULL, NULL }, { CONNMGR_VAR_SINK_PROTO_INFO, "SinkProtocolInfo", "http-get:*:audio/mpeg:*", EV_YES, DATATYPE_STRING, NULL, NULL }, { CONNMGR_VAR_CUR_CONN_IDS, "CurrentConnectionIDs", "0", EV_YES, DATATYPE_STRING, NULL, NULL }, { CONNMGR_VAR_AAT_CONN_STATUS,"A_ARG_TYPE_ConnectionStatus", "Unknown", EV_NO, DATATYPE_STRING, connstatus_values, NULL }, { CONNMGR_VAR_AAT_CONN_MGR, "A_ARG_TYPE_ConnectionManager", "/", EV_NO, DATATYPE_STRING, NULL, NULL }, { CONNMGR_VAR_AAT_DIR, "A_ARG_TYPE_Direction", "Input", EV_NO, DATATYPE_STRING, direction_values, NULL }, { CONNMGR_VAR_AAT_PROTO_INFO, "A_ARG_TYPE_ProtocolInfo", ":::", EV_NO, DATATYPE_STRING, NULL, NULL }, { CONNMGR_VAR_AAT_CONN_ID, "A_ARG_TYPE_ConnectionID", "-1", EV_NO, DATATYPE_I4, NULL, NULL }, { CONNMGR_VAR_AAT_AVT_ID, "A_ARG_TYPE_AVTransportID", "0", EV_NO, DATATYPE_I4, NULL, NULL }, { CONNMGR_VAR_AAT_RCS_ID, "A_ARG_TYPE_RcsID", "0", EV_NO, DATATYPE_I4, NULL, NULL }, { CONNMGR_VAR_COUNT, NULL, NULL, EV_NO, DATATYPE_UNKNOWN, NULL, NULL } }; if (connmgr_service_.variable_container == NULL) { connmgr_service_.variable_container = VariableContainer_new(CONNMGR_VAR_COUNT, connmgr_var_meta); // no changes expected; no collector. } return &connmgr_service_; } gmrender-resurrect-0.0.9/src/upnp_connmgr.h000066400000000000000000000023401400533760300210040ustar00rootroot00000000000000/* upnp_connmgr.h - UPnP Connection Manager definitions * * Copyright (C) 2005 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_CONNMGR_H #define _UPNP_CONNMGR_H #include typedef struct mime_type_filters_t { GSList* allowed_roots; GSList* removed_types; GSList* added_types; } mime_type_filters_t; struct service *upnp_connmgr_get_service(void); int connmgr_init(const char* mime_filter); void register_mime_type(const char *mime_type); #endif /* _UPNP_CONNMGR_H */ gmrender-resurrect-0.0.9/src/upnp_control.c000066400000000000000000000713261400533760300210260ustar00rootroot00000000000000/* upnp_control.c - UPnP RenderingControl routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "upnp_control.h" #ifndef _GNU_SOURCE # define _GNU_SOURCE /* See feature_test_macros(7) */ #endif #include #include #include #include #include #include #include #include #include "logging.h" #include "webserver.h" #include "upnp_service.h" #include "upnp_device.h" #include "output.h" #include "xmlescape.h" #include "variable-container.h" #define CONTROL_TYPE "urn:schemas-upnp-org:service:RenderingControl:1" // For some reason (predates me), this was explicitly commented out and // set to the service type; were there clients that were confused about the // right use of the service-ID ? Setting this back, let's see what happens. #define CONTROL_SERVICE_ID "urn:upnp-org:serviceId:RenderingControl" //#define CONTROL_SERVICE_ID CONTROL_TYPE #define CONTROL_SCPD_URL "/upnp/rendercontrolSCPD.xml" #define CONTROL_CONTROL_URL "/upnp/control/rendercontrol1" #define CONTROL_EVENT_URL "/upnp/event/rendercontrol1" // Namespace, see UPnP-av-RenderingControl-v3-Service-20101231.pdf page 19 #define CONTROL_EVENT_XML_NS "urn:schemas-upnp-org:metadata-1-0/RCS/" typedef enum { CONTROL_CMD_GET_BLUE_BLACK, CONTROL_CMD_GET_BLUE_GAIN, CONTROL_CMD_GET_BRIGHTNESS, CONTROL_CMD_GET_COLOR_TEMP, CONTROL_CMD_GET_CONTRAST, CONTROL_CMD_GET_GREEN_BLACK, CONTROL_CMD_GET_GREEN_GAIN, CONTROL_CMD_GET_HOR_KEYSTONE, CONTROL_CMD_GET_LOUDNESS, CONTROL_CMD_GET_MUTE, CONTROL_CMD_GET_RED_BLACK, CONTROL_CMD_GET_RED_GAIN, CONTROL_CMD_GET_SHARPNESS, CONTROL_CMD_GET_VERT_KEYSTONE, CONTROL_CMD_GET_VOL, CONTROL_CMD_GET_VOL_DB, CONTROL_CMD_GET_VOL_DBRANGE, CONTROL_CMD_LIST_PRESETS, //CONTROL_CMD_SELECT_PRESET, //CONTROL_CMD_SET_BLUE_BLACK, //CONTROL_CMD_SET_BLUE_GAIN, //CONTROL_CMD_SET_BRIGHTNESS, //CONTROL_CMD_SET_COLOR_TEMP, //CONTROL_CMD_SET_CONTRAST, //CONTROL_CMD_SET_GREEN_BLACK, //CONTROL_CMD_SET_GREEN_GAIN, //CONTROL_CMD_SET_HOR_KEYSTONE, //CONTROL_CMD_SET_LOUDNESS, CONTROL_CMD_SET_MUTE, //CONTROL_CMD_SET_RED_BLACK, //CONTROL_CMD_SET_RED_GAIN, //CONTROL_CMD_SET_SHARPNESS, //CONTROL_CMD_SET_VERT_KEYSTONE, CONTROL_CMD_SET_VOL, CONTROL_CMD_SET_VOL_DB, CONTROL_CMD_COUNT } control_cmd; static const char *aat_presetnames[] = { "FactoryDefaults", "InstallationDefaults", "Vendor defined", NULL }; static const char *aat_channels[] = { "Master", "LF", "RF", //"CF", //"LFE", //"LS", //"RS", //"LFC", //"RFC", //"SD", //"SL", //"SR", //"T", //"B", NULL }; // We split our volume range into two ranges with different slope. // The first half goes from min_db ... mid_db, the second half // from mid_db .. max_db. static const float vol_min_db = -60.0; static const float vol_mid_db = -20.0; static const float vol_max_db = 0.0; static const int vol_mid_point = 50; // volume_range.max / 2 // Note, some players don't read the range and assume 0..100. So better leave // it like this. static struct param_range volume_range = { 0, 100, 1 }; static struct param_range volume_db_range = { -60 * 256, 0, 0 }; // volume_min_db // The following are not really relevant for a sound renderer. static struct param_range brightness_range = { 0, 100, 1 }; static struct param_range contrast_range = { 0, 100, 1 }; static struct param_range sharpness_range = { 0, 100, 1 }; static struct param_range vid_gain_range = { 0, 100, 1 }; static struct param_range vid_black_range = { 0, 100, 1 }; static struct param_range colortemp_range = { 0, 65535, 1 }; static struct param_range keystone_range = { -32768, 32767, 1 }; typedef enum { CONTROL_VAR_G_GAIN, CONTROL_VAR_B_BLACK, CONTROL_VAR_VER_KEYSTONE, CONTROL_VAR_G_BLACK, CONTROL_VAR_VOLUME, CONTROL_VAR_LOUDNESS, CONTROL_VAR_AAT_INSTANCE_ID, CONTROL_VAR_R_GAIN, CONTROL_VAR_COLOR_TEMP, CONTROL_VAR_SHARPNESS, CONTROL_VAR_AAT_PRESET_NAME, CONTROL_VAR_R_BLACK, CONTROL_VAR_B_GAIN, CONTROL_VAR_MUTE, CONTROL_VAR_LAST_CHANGE, CONTROL_VAR_AAT_CHANNEL, CONTROL_VAR_HOR_KEYSTONE, CONTROL_VAR_VOLUME_DB, CONTROL_VAR_PRESET_NAME_LIST, CONTROL_VAR_CONTRAST, CONTROL_VAR_BRIGHTNESS, CONTROL_VAR_COUNT } control_variable_t; static variable_container_t *state_variables_ = NULL; static ithread_mutex_t control_mutex; static void service_lock(void) { ithread_mutex_lock(&control_mutex); struct upnp_last_change_collector* collector = upnp_control_get_service()->last_change; if (collector) { UPnPLastChangeCollector_start(collector); } } static void service_unlock(void) { struct upnp_last_change_collector* collector = upnp_control_get_service()->last_change; if (collector) { UPnPLastChangeCollector_finish(collector); } ithread_mutex_unlock(&control_mutex); } static struct argument arguments_list_presets[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentPresetNameList", PARAM_DIR_OUT, CONTROL_VAR_PRESET_NAME_LIST }, { NULL } }; // static struct argument arguments_select_preset[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "PresetName", PARAM_DIR_IN, CONTROL_VAR_AAT_PRESET_NAME }, // { NULL } // }; static struct argument arguments_get_brightness[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentBrightness", PARAM_DIR_OUT, CONTROL_VAR_BRIGHTNESS }, { NULL } }; // static struct argument arguments_set_brightness[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredBrightness", PARAM_DIR_IN, CONTROL_VAR_BRIGHTNESS }, // { NULL } // }; static struct argument arguments_get_contrast[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentContrast", PARAM_DIR_OUT, CONTROL_VAR_CONTRAST }, { NULL } }; // static struct argument arguments_set_contrast[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredContrast", PARAM_DIR_IN, CONTROL_VAR_CONTRAST }, // { NULL } // }; static struct argument arguments_get_sharpness[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentSharpness", PARAM_DIR_OUT, CONTROL_VAR_SHARPNESS }, { NULL } }; // static struct argument arguments_set_sharpness[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredSharpness", PARAM_DIR_IN, CONTROL_VAR_SHARPNESS }, // { NULL } // }; static struct argument arguments_get_red_gain[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentRedVideoGain", PARAM_DIR_OUT, CONTROL_VAR_R_GAIN }, { NULL } }; // static struct argument arguments_set_red_gain[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredRedVideoGain", PARAM_DIR_IN, CONTROL_VAR_R_GAIN }, // { NULL } // }; static struct argument arguments_get_green_gain[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentGreenVideoGain", PARAM_DIR_OUT, CONTROL_VAR_G_GAIN }, { NULL } }; // static struct argument arguments_set_green_gain[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredGreenVideoGain", PARAM_DIR_IN, CONTROL_VAR_G_GAIN }, // { NULL } // }; static struct argument arguments_get_blue_gain[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentBlueVideoGain", PARAM_DIR_OUT, CONTROL_VAR_B_GAIN }, { NULL } }; // static struct argument arguments_set_blue_gain[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredBlueVideoGain", PARAM_DIR_IN, CONTROL_VAR_B_GAIN }, // { NULL } // }; static struct argument arguments_get_red_black[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentRedVideoBlackLevel", PARAM_DIR_OUT, CONTROL_VAR_R_BLACK }, { NULL } }; // static struct argument arguments_set_red_black[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredRedVideoBlackLevel", PARAM_DIR_IN, CONTROL_VAR_R_BLACK }, // { NULL } // }; static struct argument arguments_get_green_black[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentGreenVideoBlackLevel", PARAM_DIR_OUT, CONTROL_VAR_G_BLACK }, { NULL } }; // static struct argument arguments_set_green_black[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredGreenVideoBlackLevel", PARAM_DIR_IN, CONTROL_VAR_G_BLACK }, // { NULL } // }; static struct argument arguments_get_blue_black[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentBlueVideoBlackLevel", PARAM_DIR_OUT, CONTROL_VAR_B_BLACK }, { NULL } }; // static struct argument arguments_set_blue_black[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredBlueVideoBlackLevel", PARAM_DIR_IN, CONTROL_VAR_B_BLACK }, // { NULL } // }; static struct argument arguments_get_color_temp[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentColorTemperature", PARAM_DIR_OUT, CONTROL_VAR_COLOR_TEMP }, { NULL } }; // static struct argument arguments_set_color_temp[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredColorTemperature", PARAM_DIR_IN, CONTROL_VAR_COLOR_TEMP }, // { NULL } // }; static struct argument arguments_get_hor_keystone[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentHorizontalKeystone", PARAM_DIR_OUT, CONTROL_VAR_HOR_KEYSTONE }, { NULL } }; // static struct argument arguments_set_hor_keystone[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredHorizontalKeystone", PARAM_DIR_IN, CONTROL_VAR_HOR_KEYSTONE }, // { NULL } // }; static struct argument arguments_get_vert_keystone[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "CurrentVerticalKeystone", PARAM_DIR_OUT, CONTROL_VAR_VER_KEYSTONE }, { NULL } }; // static struct argument arguments_set_vert_keystone[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "DesiredVerticalKeystone", PARAM_DIR_IN, CONTROL_VAR_VER_KEYSTONE }, // { NULL } // }; static struct argument arguments_get_mute[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "CurrentMute", PARAM_DIR_OUT, CONTROL_VAR_MUTE }, { NULL } }; static struct argument arguments_set_mute[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "DesiredMute", PARAM_DIR_IN, CONTROL_VAR_MUTE }, { NULL } }; static struct argument arguments_get_vol[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "CurrentVolume", PARAM_DIR_OUT, CONTROL_VAR_VOLUME }, { NULL } }; static struct argument arguments_set_vol[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "DesiredVolume", PARAM_DIR_IN, CONTROL_VAR_VOLUME }, { NULL } }; static struct argument arguments_get_vol_db[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "CurrentVolume", PARAM_DIR_OUT, CONTROL_VAR_VOLUME_DB }, { NULL } }; static struct argument arguments_set_vol_db[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "DesiredVolume", PARAM_DIR_IN, CONTROL_VAR_VOLUME_DB }, { NULL } }; static struct argument arguments_get_vol_dbrange[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "MinValue", PARAM_DIR_OUT, CONTROL_VAR_VOLUME_DB }, { "MaxValue", PARAM_DIR_OUT, CONTROL_VAR_VOLUME_DB }, { NULL } }; static struct argument arguments_get_loudness[] = { { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, { "CurrentLoudness", PARAM_DIR_OUT, CONTROL_VAR_LOUDNESS }, { NULL } }; // static struct argument arguments_set_loudness[] = { // { "InstanceID", PARAM_DIR_IN, CONTROL_VAR_AAT_INSTANCE_ID }, // { "Channel", PARAM_DIR_IN, CONTROL_VAR_AAT_CHANNEL }, // { "DesiredLoudness", PARAM_DIR_IN, CONTROL_VAR_LOUDNESS }, // { NULL } // }; static struct argument *argument_list[] = { [CONTROL_CMD_GET_BLUE_BLACK] = arguments_get_blue_black, [CONTROL_CMD_GET_BLUE_GAIN] = arguments_get_blue_gain, [CONTROL_CMD_GET_BRIGHTNESS] = arguments_get_brightness, [CONTROL_CMD_GET_COLOR_TEMP] = arguments_get_color_temp, [CONTROL_CMD_GET_CONTRAST] = arguments_get_contrast, [CONTROL_CMD_GET_GREEN_BLACK] = arguments_get_green_black, [CONTROL_CMD_GET_GREEN_GAIN] = arguments_get_green_gain, [CONTROL_CMD_GET_HOR_KEYSTONE] = arguments_get_hor_keystone, [CONTROL_CMD_GET_LOUDNESS] = arguments_get_loudness, [CONTROL_CMD_GET_MUTE] = arguments_get_mute, [CONTROL_CMD_GET_RED_BLACK] = arguments_get_red_black, [CONTROL_CMD_GET_RED_GAIN] = arguments_get_red_gain, [CONTROL_CMD_GET_SHARPNESS] = arguments_get_sharpness, [CONTROL_CMD_GET_VERT_KEYSTONE] = arguments_get_vert_keystone, [CONTROL_CMD_GET_VOL] = arguments_get_vol, [CONTROL_CMD_GET_VOL_DB] = arguments_get_vol_db, [CONTROL_CMD_GET_VOL_DBRANGE] = arguments_get_vol_dbrange, [CONTROL_CMD_LIST_PRESETS] = arguments_list_presets, [CONTROL_CMD_SET_MUTE] = arguments_set_mute, [CONTROL_CMD_SET_VOL] = arguments_set_vol, [CONTROL_CMD_SET_VOL_DB] = arguments_set_vol_db, //[CONTROL_CMD_SELECT_PRESET] = arguments_select_preset, //[CONTROL_CMD_SET_BRIGHTNESS] = arguments_set_brightness, //[CONTROL_CMD_SET_CONTRAST] = arguments_set_contrast, //[CONTROL_CMD_SET_SHARPNESS] = arguments_set_sharpness, //[CONTROL_CMD_SET_RED_GAIN] = arguments_set_red_gain, //[CONTROL_CMD_SET_GREEN_GAIN] = arguments_set_green_gain, //[CONTROL_CMD_SET_BLUE_GAIN] = arguments_set_blue_gain, //[CONTROL_CMD_SET_RED_BLACK] = arguments_set_red_black, //[CONTROL_CMD_SET_GREEN_BLACK] = arguments_set_green_black, //[CONTROL_CMD_SET_BLUE_BLACK] = arguments_set_blue_black, //[CONTROL_CMD_SET_COLOR_TEMP] = arguments_set_color_temp, //[CONTROL_CMD_SET_HOR_KEYSTONE] = arguments_set_hor_keystone, //[CONTROL_CMD_SET_VERT_KEYSTONE] = arguments_set_vert_keystone, //[CONTROL_CMD_SET_LOUDNESS] = arguments_set_loudness, [CONTROL_CMD_COUNT] = NULL }; // Replace given variable without sending an state-change event. static void replace_var(control_variable_t varnum, const char *new_value) { VariableContainer_change(state_variables_, varnum, new_value); } static void change_volume(const char *volume, const char *db_volume) { replace_var(CONTROL_VAR_VOLUME, volume); replace_var(CONTROL_VAR_VOLUME_DB, db_volume); } static int cmd_obtain_variable(struct action_event *event, control_variable_t varnum, const char *paramname) { const char *instance = upnp_get_string(event, "InstanceID"); if (instance == NULL) { return -1; } Log_info("control", "%s: %s for instance %s\n", __FUNCTION__, paramname, instance); upnp_append_variable(event, varnum, paramname); return 0; } static int list_presets(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_PRESET_NAME_LIST, "CurrentPresetNameList"); } static int get_brightness(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_BRIGHTNESS, "CurrentBrightness"); } static int get_contrast(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_CONTRAST, "CurrentContrast"); } static int get_sharpness(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_SHARPNESS, "CurrentSharpness"); } static int get_red_videogain(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_R_GAIN, "CurrentRedVideoGain"); } static int get_green_videogain(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_G_GAIN, "CurrentGreenVideoGain"); } static int get_blue_videogain(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_B_GAIN, "CurrentBlueVideoGain"); } static int get_red_videoblacklevel(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_R_BLACK, "CurrentRedVideoBlackLevel"); } static int get_green_videoblacklevel(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_G_BLACK, "CurrentGreenVideoBlackLevel"); } static int get_blue_videoblacklevel(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_B_BLACK, "CurrentBlueVideoBlackLevel"); } static int get_colortemperature(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_COLOR_TEMP, "CurrentColorTemperature"); } static int get_horizontal_keystone(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_HOR_KEYSTONE, "CurrentHorizontalKeystone"); } static int get_vertical_keystone(struct action_event *event) { return cmd_obtain_variable(event, CONTROL_VAR_VER_KEYSTONE, "CurrentVerticalKeystone"); } static int get_mute(struct action_event *event) { /* FIXME - Channel */ return cmd_obtain_variable(event, CONTROL_VAR_MUTE, "CurrentMute"); } static void set_mute_toggle(int do_mute) { replace_var(CONTROL_VAR_MUTE, do_mute ? "1" : "0"); output_set_mute(do_mute); } static int set_mute(struct action_event *event) { const char *value = upnp_get_string(event, "DesiredMute"); service_lock(); const int do_mute = atoi(value); set_mute_toggle(do_mute); replace_var(CONTROL_VAR_MUTE, do_mute ? "1" : "0"); service_unlock(); return 0; } static int get_volume(struct action_event *event) { /* FIXME - Channel */ return cmd_obtain_variable(event, CONTROL_VAR_VOLUME, "CurrentVolume"); } static float volume_level_to_decibel(int volume) { if (volume < volume_range.min) volume = volume_range.min; if (volume > volume_range.max) volume = volume_range.max; if (volume < volume_range.max / 2) { return vol_min_db + (vol_mid_db - vol_min_db) / vol_mid_point * volume; } else { const int range = volume_range.max - vol_mid_point; return vol_mid_db + ((vol_max_db - vol_mid_db) / range * (volume - vol_mid_point)); } } static int volume_decibel_to_level(float decibel) { if (decibel < vol_min_db) return volume_range.min; if (decibel > vol_max_db) return volume_range.max; if (decibel < vol_mid_db) { return (decibel - vol_min_db) * vol_mid_point / (vol_mid_db - vol_min_db); } else { const int range = volume_range.max - vol_mid_point; return (decibel - vol_mid_db) * range / (vol_max_db - vol_mid_db) + vol_mid_point; } } // Change volume variables from the given decibel. Quantize value according to // our ranges. static float change_volume_decibel(float raw_decibel) { int volume_level = volume_decibel_to_level(raw_decibel); // Since we quantize it to the level, lets calculate the // actual level. float decibel = volume_level_to_decibel(volume_level); char volume[10]; snprintf(volume, sizeof(volume), "%d", volume_level); char db_volume[10]; snprintf(db_volume, sizeof(db_volume), "%d", (int) (256 * decibel)); Log_info("control", "Setting volume-db to %.2fdb == #%d", decibel, volume_level); change_volume(volume, db_volume); return decibel; } static int set_volume_db(struct action_event *event) { const char *str_decibel_in = upnp_get_string(event, "DesiredVolume"); service_lock(); float raw_decibel_in = atof(str_decibel_in); float decibel = change_volume_decibel(raw_decibel_in); output_set_volume(exp(decibel / 20 * log(10))); service_unlock(); return 0; } static int set_volume(struct action_event *event) { const char *volume = upnp_get_string(event, "DesiredVolume"); service_lock(); int volume_level = atoi(volume); // range 0..100 if (volume_level < volume_range.min) volume_level = volume_range.min; if (volume_level > volume_range.max) volume_level = volume_range.max; const float decibel = volume_level_to_decibel(volume_level); char db_volume[10]; snprintf(db_volume, sizeof(db_volume), "%d", (int) (256 * decibel)); const double fraction = exp(decibel / 20 * log(10)); change_volume(volume, db_volume); output_set_volume(fraction); set_mute_toggle(volume_level == 0); service_unlock(); return 0; } static int get_volume_db(struct action_event *event) { /* FIXME - Channel */ return cmd_obtain_variable(event, CONTROL_VAR_VOLUME_DB, "CurrentVolumeDB"); } static int get_volume_dbrange(struct action_event *event) { // Ignoring instanceID and Channel char minval[16]; snprintf(minval, sizeof(minval), "%lld", volume_db_range.min); upnp_add_response(event, "MinValue", minval); upnp_add_response(event, "MaxValue", "0"); return 0; } static int get_loudness(struct action_event *event) { /* FIXME - Channel */ return cmd_obtain_variable(event, CONTROL_VAR_LOUDNESS, "CurrentLoudness"); } static struct action control_actions[] = { [CONTROL_CMD_GET_BLUE_BLACK] = {"GetBlueVideoBlackLevel", get_blue_videoblacklevel}, /* optional */ [CONTROL_CMD_GET_BLUE_GAIN] = {"GetBlueVideoGain", get_blue_videogain}, /* optional */ [CONTROL_CMD_GET_BRIGHTNESS] = {"GetBrightness", get_brightness}, /* optional */ [CONTROL_CMD_GET_COLOR_TEMP] = {"GetColorTemperature", get_colortemperature}, /* optional */ [CONTROL_CMD_GET_CONTRAST] = {"GetContrast", get_contrast}, /* optional */ [CONTROL_CMD_GET_GREEN_BLACK] = {"GetGreenVideoBlackLevel", get_green_videoblacklevel}, /* optional */ [CONTROL_CMD_GET_GREEN_GAIN] = {"GetGreenVideoGain", get_green_videogain}, /* optional */ [CONTROL_CMD_GET_HOR_KEYSTONE] = {"GetHorizontalKeystone", get_horizontal_keystone}, /* optional */ [CONTROL_CMD_GET_LOUDNESS] = {"GetLoudness", get_loudness}, /* optional */ [CONTROL_CMD_GET_MUTE] = {"GetMute", get_mute}, /* optional */ [CONTROL_CMD_GET_RED_BLACK] = {"GetRedVideoBlackLevel", get_red_videoblacklevel}, /* optional */ [CONTROL_CMD_GET_RED_GAIN] = {"GetRedVideoGain", get_red_videogain}, /* optional */ [CONTROL_CMD_GET_SHARPNESS] = {"GetSharpness", get_sharpness}, /* optional */ [CONTROL_CMD_GET_VERT_KEYSTONE] = {"GetVerticalKeystone", get_vertical_keystone}, /* optional */ [CONTROL_CMD_GET_VOL] = {"GetVolume", get_volume}, /* optional */ [CONTROL_CMD_GET_VOL_DB] = {"GetVolumeDB", get_volume_db}, /* optional */ [CONTROL_CMD_GET_VOL_DBRANGE] = {"GetVolumeDBRange", get_volume_dbrange}, /* optional */ [CONTROL_CMD_LIST_PRESETS] = {"ListPresets", list_presets}, [CONTROL_CMD_SET_MUTE] = {"SetMute", set_mute}, /* optional */ [CONTROL_CMD_SET_VOL] = {"SetVolume", set_volume}, /* optional */ [CONTROL_CMD_SET_VOL_DB] = {"SetVolumeDB", set_volume_db}, /* optional */ //[CONTROL_CMD_SELECT_PRESET] = {"SelectPreset", NULL}, //[CONTROL_CMD_SET_BRIGHTNESS] = {"SetBrightness", NULL}, /* optional */ //[CONTROL_CMD_SET_CONTRAST] = {"SetContrast", NULL}, /* optional */ //[CONTROL_CMD_SET_SHARPNESS] = {"SetSharpness", NULL}, /* optional */ //[CONTROL_CMD_SET_RED_GAIN] = {"SetRedVideoGain", NULL}, /* optional */ //[CONTROL_CMD_SET_GREEN_GAIN] = {"SetGreenVideoGain", NULL}, /* optional */ //[CONTROL_CMD_SET_BLUE_GAIN] = {"SetBlueVideoGain", NULL}, /* optional */ //[CONTROL_CMD_SET_RED_BLACK] = {"SetRedVideoBlackLevel", NULL}, /* optional */ //[CONTROL_CMD_SET_GREEN_BLACK] = {"SetGreenVideoBlackLevel", NULL}, /* optional */ //[CONTROL_CMD_SET_BLUE_BLACK] = {"SetBlueVideoBlackLevel", NULL}, /* optional */ //[CONTROL_CMD_SET_COLOR_TEMP] = {"SetColorTemperature", NULL}, /* optional */ //[CONTROL_CMD_SET_HOR_KEYSTONE] = {"SetHorizontalKeystone", NULL}, /* optional */ //[CONTROL_CMD_SET_VERT_KEYSTONE] = {"SetVerticalKeystone", NULL}, /* optional */ //[CONTROL_CMD_SET_LOUDNESS] = {"SetLoudness", NULL}, /* optional */ [CONTROL_CMD_COUNT] = {NULL, NULL} }; struct service *upnp_control_get_service(void) { static struct service control_service_ = { .service_mutex = &control_mutex, .service_id = CONTROL_SERVICE_ID, .service_type = CONTROL_TYPE, .scpd_url = CONTROL_SCPD_URL, .control_url = CONTROL_CONTROL_URL, .event_url = CONTROL_EVENT_URL, .event_xml_ns = CONTROL_EVENT_XML_NS, .actions = control_actions, .action_arguments = argument_list, .variable_container = NULL, // set later. .last_change = NULL, .command_count = CONTROL_CMD_COUNT, }; static struct var_meta control_var_meta[] = { {CONTROL_VAR_LAST_CHANGE, "LastChange", "", EV_YES, DATATYPE_STRING, NULL, NULL }, {CONTROL_VAR_PRESET_NAME_LIST, "PresetNameList", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {CONTROL_VAR_AAT_CHANNEL, "A_ARG_TYPE_Channel", "", EV_NO, DATATYPE_STRING, aat_channels, NULL }, {CONTROL_VAR_AAT_INSTANCE_ID, "A_ARG_TYPE_InstanceID", "0", EV_NO, DATATYPE_UI4, NULL, NULL }, {CONTROL_VAR_AAT_PRESET_NAME, "A_ARG_TYPE_PresetName", "", EV_NO, DATATYPE_STRING, aat_presetnames, NULL }, {CONTROL_VAR_BRIGHTNESS, "Brightness", "0", EV_NO, DATATYPE_UI2, NULL, &brightness_range }, {CONTROL_VAR_CONTRAST, "Contrast", "0", EV_NO, DATATYPE_UI2, NULL, &contrast_range }, {CONTROL_VAR_SHARPNESS, "Sharpness", "0", EV_NO, DATATYPE_UI2, NULL, &sharpness_range }, {CONTROL_VAR_R_GAIN, "RedVideoGain", "0", EV_NO, DATATYPE_UI2, NULL, &vid_gain_range }, {CONTROL_VAR_G_GAIN, "GreenVideoGain", "0", EV_NO, DATATYPE_UI2, NULL, &vid_gain_range }, {CONTROL_VAR_B_GAIN, "BlueVideoGain", "0", EV_NO, DATATYPE_UI2, NULL, &vid_gain_range }, {CONTROL_VAR_R_BLACK, "RedVideoBlackLevel", "0", EV_NO, DATATYPE_UI2, NULL, &vid_black_range }, {CONTROL_VAR_G_BLACK, "GreenVideoBlackLevel", "0", EV_NO, DATATYPE_UI2, NULL, &vid_black_range }, {CONTROL_VAR_B_BLACK, "BlueVideoBlackLevel", "0", EV_NO, DATATYPE_UI2, NULL, &vid_black_range }, {CONTROL_VAR_COLOR_TEMP, "ColorTemperature", "0", EV_NO, DATATYPE_UI2, NULL, &colortemp_range }, {CONTROL_VAR_HOR_KEYSTONE, "HorizontalKeystone", "0", EV_NO, DATATYPE_I2, NULL, &keystone_range }, {CONTROL_VAR_VER_KEYSTONE, "VerticalKeystone", "0", EV_NO, DATATYPE_I2, NULL, &keystone_range }, {CONTROL_VAR_MUTE, "Mute", "0", EV_NO, DATATYPE_BOOLEAN, NULL, NULL }, {CONTROL_VAR_VOLUME, "Volume", "0", EV_NO, DATATYPE_UI2, NULL, &volume_range }, {CONTROL_VAR_VOLUME_DB, "VolumeDB", "0", EV_NO, DATATYPE_I2, NULL, &volume_db_range }, {CONTROL_VAR_LOUDNESS, "Loudness", "0", EV_NO, DATATYPE_BOOLEAN, NULL, NULL }, {CONTROL_VAR_COUNT, NULL, NULL, EV_NO, DATATYPE_UNKNOWN, NULL, NULL } }; if (control_service_.variable_container == NULL) { state_variables_ = VariableContainer_new(CONTROL_VAR_COUNT, control_var_meta); control_service_.variable_container = state_variables_; } return &control_service_; } void upnp_control_init(struct upnp_device *device) { struct service *service = upnp_control_get_service(); // Set initial volume. float volume_fraction = 0; if (output_get_volume(&volume_fraction) == 0) { Log_info("control", "Output initial volume is %f; setting " "control variables accordingly.", volume_fraction); change_volume_decibel(20 * log(volume_fraction) / log(10)); } assert(service->last_change == NULL); service->last_change = UPnPLastChangeCollector_new(service->variable_container, CONTROL_EVENT_XML_NS, device, CONTROL_SERVICE_ID); // According to UPnP-av-RenderingControl-v3-Service-20101231.pdf, 2.3.1 // page 51, the A_ARG_TYPE* variables are not evented. UPnPLastChangeCollector_add_ignore(service->last_change, CONTROL_VAR_AAT_CHANNEL); UPnPLastChangeCollector_add_ignore(service->last_change, CONTROL_VAR_AAT_INSTANCE_ID); UPnPLastChangeCollector_add_ignore(service->last_change, CONTROL_VAR_AAT_PRESET_NAME); } void upnp_control_register_variable_listener(variable_change_listener_t cb, void *userdata) { VariableContainer_register_callback(state_variables_, cb, userdata); } gmrender-resurrect-0.0.9/src/upnp_control.h000066400000000000000000000022741400533760300210270ustar00rootroot00000000000000/* upnp_control.h - UPnP RenderingControl definitions * * Copyright (C) 2005 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_CONTROL_H #define _UPNP_CONTROL_H #include "variable-container.h" struct upnp_device; void upnp_control_init(struct upnp_device *device); struct service *upnp_control_get_service(void); void upnp_control_register_variable_listener(variable_change_listener_t cb, void *userdata); #endif /* _UPNP_CONTROL_H */ gmrender-resurrect-0.0.9/src/upnp_device.c000066400000000000000000000477031400533760300206070ustar00rootroot00000000000000/* upnp_device.c - Generic UPnP device handling * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include "xmlescape.h" #include "webserver.h" #include "xmldoc.h" #include "upnp_service.h" #include "upnp_device.h" #include "variable-container.h" // Enable logging of action requests. //#define ENABLE_ACTION_LOGGING struct upnp_device { struct upnp_device_descriptor *upnp_device_descriptor; ithread_mutex_t device_mutex; UpnpDevice_Handle device_handle; }; int upnp_add_response(struct action_event *event, const char *key, const char *value) { assert(event != NULL); assert(key != NULL); assert(value != NULL); if (event->status) { return -1; } IXML_Document* actionResult = UpnpActionRequest_get_ActionResult(event->request); const char* actionName = UpnpActionRequest_get_ActionName_cstr(event->request); int rc = UpnpAddToActionResponse(&actionResult, actionName, event->service->service_type, key, value); if (rc != UPNP_E_SUCCESS) { /* report custom error */ UpnpString *errorMessage = UpnpString_new(); UpnpString_set_String(errorMessage, UpnpGetErrorMessage(rc)); UpnpActionRequest_set_ActionResult(event->request, NULL); UpnpActionRequest_set_ErrCode(event->request, UPNP_SOAP_E_ACTION_FAILED); UpnpActionRequest_set_ErrStr(event->request, errorMessage); return -1; } UpnpActionRequest_set_ActionResult(event->request, actionResult); return 0; } void upnp_append_variable(struct action_event *event, int varnum, const char *paramname) { const char *value; struct service *service = event->service; assert(event != NULL); assert(paramname != NULL); ithread_mutex_lock(service->service_mutex); value = VariableContainer_get(service->variable_container, varnum, NULL); assert(value != NULL); // triggers on invalid variable. upnp_add_response(event, paramname, value); ithread_mutex_unlock(service->service_mutex); } void upnp_set_error(struct action_event *event, int error_code, const char *format, ...) { event->status = -1; va_list ap; va_start(ap, format); char buffer[LINE_SIZE]; vsnprintf(buffer, sizeof(buffer), format, ap); va_end(ap); UpnpActionRequest_set_ActionResult(event->request, NULL); UpnpActionRequest_set_ErrCode(event->request, UPNP_SOAP_E_ACTION_FAILED); UpnpString *errStr = UpnpString_new(); UpnpString_set_String(errStr, buffer); UpnpActionRequest_set_ErrStr(event->request, errStr); Log_error("upnp", "%s: %s (%d)\n", __FUNCTION__, UpnpActionRequest_get_ErrStr_cstr(event->request), error_code); } const char *upnp_get_string(struct action_event *event, const char *key) { IXML_Node *node; node = (IXML_Node *)UpnpActionRequest_get_ActionRequest(event->request); if (node == NULL) { upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS, "Invalid action request document"); return NULL; } node = ixmlNode_getFirstChild(node); if (node == NULL) { upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS, "Invalid action request document"); return NULL; } node = ixmlNode_getFirstChild(node); for (/**/; node != NULL; node = ixmlNode_getNextSibling(node)) { if (strcmp(ixmlNode_getNodeName(node), key) == 0) { node = ixmlNode_getFirstChild(node); const char *node_value = (node != NULL ? ixmlNode_getNodeValue(node) : NULL); return node_value != NULL ? node_value : ""; } } upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS, "Missing action request argument (%s)", key); return NULL; } static int handle_subscription_request(struct upnp_device *priv, const UpnpSubscriptionRequest *sr_event) { struct service *srv; int rc; assert(priv != NULL); const char *serviceId = UpnpSubscriptionRequest_get_ServiceId_cstr(sr_event); const char *udn = UpnpSubscriptionRequest_get_UDN_cstr(sr_event); Log_info("upnp", "Subscription request for %s (%s)", serviceId, udn); srv = find_service(priv->upnp_device_descriptor, serviceId); if (srv == NULL) { Log_error("upnp", "%s: Unknown service '%s'", __FUNCTION__, serviceId); return -1; } int result = -1; ithread_mutex_lock(&(priv->device_mutex)); // There is really only one variable evented: LastChange const char *eventvar_names[] = { "LastChange", NULL }; const char *eventvar_values[] = { NULL, NULL }; // Build the current state of the variables as one gigantic initial // LastChange update. ithread_mutex_lock(srv->service_mutex); const int var_count = VariableContainer_get_num_vars(srv->variable_container); // TODO(hzeller): maybe use srv->last_change directly ? upnp_last_change_builder_t *builder = UPnPLastChangeBuilder_new(srv->event_xml_ns); for (int i = 0; i < var_count; ++i) { const char *name; const char *value = VariableContainer_get(srv->variable_container, i, &name); // Send over all variables except "LastChange" itself. Also all // A_ARG_TYPE variables are not evented. if (value && strcmp("LastChange", name) != 0 && strncmp("A_ARG_TYPE_", name, strlen("A_ARG_TYPE_")) != 0) { UPnPLastChangeBuilder_add(builder, name, value); } } ithread_mutex_unlock(srv->service_mutex); char *xml_value = UPnPLastChangeBuilder_to_xml(builder); Log_info("upnp", "Initial variable sync: %s", xml_value); eventvar_values[0] = xmlescape(xml_value, 0); free(xml_value); UPnPLastChangeBuilder_delete(builder); const char *sid = UpnpSubscriptionRequest_get_SID_cstr(sr_event); rc = UpnpAcceptSubscription(priv->device_handle, udn, serviceId, eventvar_names, eventvar_values, 1, sid); if (rc == UPNP_E_SUCCESS) { result = 0; } else { Log_error("upnp", "Accept Subscription Error: %s (%d)", UpnpGetErrorMessage(rc), rc); } ithread_mutex_unlock(&(priv->device_mutex)); free((char*)eventvar_values[0]); return result; } int upnp_device_notify(struct upnp_device *device, const char *serviceID, const char **varnames, const char **varvalues, int varcount) { UpnpNotify(device->device_handle, device->upnp_device_descriptor->udn, serviceID, varnames, varvalues, varcount); return 0; } static int handle_var_request(struct upnp_device *priv, UpnpStateVarRequest *event) { const char *serviceID = UpnpStateVarRequest_get_ServiceID_cstr(event); struct service *srv = find_service(priv->upnp_device_descriptor, serviceID); if (srv == NULL) { UpnpStateVarRequest_set_ErrCode(event, UPNP_SOAP_E_INVALID_ARGS); return -1; } ithread_mutex_lock(srv->service_mutex); char *result = NULL; const int var_count = VariableContainer_get_num_vars(srv->variable_container); for (int i = 0; i < var_count; ++i) { const char *name = NULL; const char *value = VariableContainer_get(srv->variable_container, i, &name); const char *stateVarName = UpnpStateVarRequest_get_StateVarName_cstr(event); if (value && strcmp(stateVarName, name) == 0) { result = strdup(value); break; } } ithread_mutex_unlock(srv->service_mutex); UpnpStateVarRequest_set_CurrentVal(event, result); int errCode = (result == NULL) ? UPNP_SOAP_E_INVALID_VAR : UPNP_E_SUCCESS; UpnpStateVarRequest_set_ErrCode(event, errCode); Log_info("upnp", "Variable request %s -> %s (%s)", UpnpStateVarRequest_get_StateVarName_cstr(event), result, serviceID); return 0; } static int handle_action_request(struct upnp_device *priv, UpnpActionRequest *ar_event) { const char *serviceID = UpnpActionRequest_get_ServiceID_cstr(ar_event); const char *actionName = UpnpActionRequest_get_ActionName_cstr(ar_event); struct service *event_service = find_service(priv->upnp_device_descriptor, serviceID); struct action *event_action = find_action(event_service, actionName); if (event_action == NULL) { Log_error("upnp", "Unknown action '%s' for service '%s'", actionName, serviceID); UpnpActionRequest_set_ActionResult(ar_event, NULL); UpnpActionRequest_set_ErrCode(ar_event, 401); return -1; } // We want to send the LastChange event only after the action is // finished - just to be conservative, we don't know how clients // react to get LastChange notifictions while in the middle of // issuing an action. // // So we nest the change collector level here, so that we only send the // LastChange after the action is finished (). // // Note, this is, in fact, only a preparation and not yet working as // described above: we are still in the middle // of executing the event-callback while sending the last change // event implicitly when calling UPnPLastChangeCollector_finish() below. // It would be good to enqueue the upnp_device_notify() after // the action event is finished. if (event_service->last_change) { ithread_mutex_lock(event_service->service_mutex); UPnPLastChangeCollector_start(event_service->last_change); ithread_mutex_unlock(event_service->service_mutex); } #ifdef ENABLE_ACTION_LOGGING { char *action_request_xml = NULL; if (UpnpActionRequest_get_ActionRequest(ar_event)) { action_request_xml = ixmlDocumenttoString( UpnpActionRequest_get_ActionRequest(ar_event)); } Log_info("upnp", "Action '%s'; Request: %s", UpnpActionRequest_get_ActionName_cstr(ar_event), action_request_xml); free(action_request_xml); } #endif if (event_action->callback) { struct action_event event; int rc; event.request = ar_event; event.status = 0; event.service = event_service; event.device = priv; rc = (event_action->callback) (&event); if (rc == 0) { UpnpActionRequest_set_ErrCode(event.request, UPNP_E_SUCCESS); #ifdef ENABLE_ACTION_LOGGING if (UpnpActionRequest_get_ActionResult(ar_event)) { char *action_result_xml = ixmlDocumenttoString( UpnpActionRequest_get_ActionResult(ar_event)); Log_info("upnp", "Action '%s' OK; Response %s", UpnpActionRequest_get_ActionName_cstr(ar_event), action_result_xml); free(action_result_xml); } else { Log_info("upnp", "Action '%s' OK", UpnpActionRequest_get_ActionName_cstr(ar_event)); } #endif } IXML_Document *actionResult = UpnpActionRequest_get_ActionResult(ar_event); if (actionResult == NULL) { actionResult = UpnpMakeActionResponse(actionName, event_service->service_type, 0, NULL); UpnpActionRequest_set_ActionResult(event.request, actionResult); } } else { int errCode = UpnpActionRequest_get_ErrCode(ar_event); int sock = UpnpActionRequest_get_Socket(ar_event); const char *errStr = UpnpActionRequest_get_ErrStr_cstr(ar_event); const char *actionName = UpnpActionRequest_get_ActionName_cstr(ar_event); const char *devUDN = UpnpActionRequest_get_DevUDN_cstr(ar_event); const char *serviceID = UpnpActionRequest_get_ServiceID_cstr(ar_event); Log_error("upnp", "Got a valid action, but no handler defined (!)\n" " ErrCode: %d\n" " Socket: %d\n" " ErrStr: '%s'\n" " ActionName: '%s'\n" " DevUDN: '%s'\n" " ServiceID: '%s'\n", errCode, sock, errStr, actionName, devUDN, serviceID); UpnpActionRequest_set_ErrCode(ar_event, UPNP_E_SUCCESS); } if (event_service->last_change) { // See comment above. ithread_mutex_lock(event_service->service_mutex); UPnPLastChangeCollector_finish(event_service->last_change); ithread_mutex_unlock(event_service->service_mutex); } return 0; } static UPNP_CALLBACK(event_handler, EventType, event, userdata) { struct upnp_device *priv = (struct upnp_device *) userdata; switch (EventType) { case UPNP_CONTROL_ACTION_REQUEST: handle_action_request(priv, (UpnpActionRequest*)event); break; case UPNP_CONTROL_GET_VAR_REQUEST: handle_var_request(priv, (UpnpStateVarRequest*)event); break; case UPNP_EVENT_SUBSCRIPTION_REQUEST: handle_subscription_request(priv, (const UpnpSubscriptionRequest*)event); break; default: Log_error("upnp", "Unknown event type: %d", EventType); break; } return 0; } static gboolean initialize_device(struct upnp_device_descriptor *device_def, struct upnp_device *result_device, const char *interface_name, unsigned short port) { int rc; char *buf; rc = UpnpInit2(interface_name, port); /* There have been situations reported in which UPNP had issues * initializing right after network came up. #129 */ int retries_left = 60; static const int kRetryTimeMs = 1000; while (rc != UPNP_E_SUCCESS && retries_left--) { usleep(kRetryTimeMs * 1000); Log_error("upnp", "UpnpInit2(interface=%s, port=%d) Error: %s (%d). Retrying... (%ds)", interface_name, port, UpnpGetErrorMessage(rc), rc, retries_left); rc = UpnpInit2(interface_name, port); } if (UPNP_E_SUCCESS != rc) { Log_error("upnp", "UpnpInit2(interface=%s, port=%d) Error: %s (%d). Giving up.", interface_name, port, UpnpGetErrorMessage(rc), rc); return FALSE; } Log_info("upnp", "Registered IP=%s port=%d\n", UpnpGetServerIpAddress(), UpnpGetServerPort()); rc = UpnpEnableWebserver(TRUE); if (UPNP_E_SUCCESS != rc) { Log_error("upnp", "UpnpEnableWebServer() Error: %s (%d)", UpnpGetErrorMessage(rc), rc); return FALSE; } if (!webserver_register_callbacks()) return FALSE; rc = UpnpAddVirtualDir("/upnp"); if (UPNP_E_SUCCESS != rc) { Log_error("upnp", "UpnpAddVirtualDir() Error: %s (%d)", UpnpGetErrorMessage(rc), rc); return FALSE; } buf = upnp_create_device_desc(device_def); rc = UpnpRegisterRootDevice2(UPNPREG_BUF_DESC, buf, strlen(buf), 1, &event_handler, result_device, &(result_device->device_handle)); free(buf); if (UPNP_E_SUCCESS != rc) { Log_error("upnp", "UpnpRegisterRootDevice2() Error: %s (%d)", UpnpGetErrorMessage(rc), rc); return FALSE; } rc = UpnpSendAdvertisement(result_device->device_handle, 100); if (UPNP_E_SUCCESS != rc) { Log_error("unpp", "Error sending advertisements: %s (%d)", UpnpGetErrorMessage(rc), rc); return FALSE; } return TRUE; } struct upnp_device *upnp_device_init(struct upnp_device_descriptor *device_def, const char *interface_name, unsigned short port) { int rc; char *buf; struct service *srv; struct icon *icon_entry; assert(device_def != NULL); if (device_def->init_function) { rc = device_def->init_function(); if (rc != 0) { return NULL; } } struct upnp_device *result_device = (struct upnp_device*)malloc(sizeof(*result_device)); result_device->upnp_device_descriptor = device_def; ithread_mutex_init(&(result_device->device_mutex), NULL); /* register icons in web server */ for (int i = 0; (icon_entry = device_def->icons[i]); i++) { webserver_register_file(icon_entry->url, "image/png"); } /* generate and register service schemas in web server */ for (int i = 0; (srv = device_def->services[i]); i++) { buf = upnp_get_scpd(srv); assert(buf != NULL); webserver_register_buf(srv->scpd_url, buf, "text/xml"); } if (!initialize_device(device_def, result_device, interface_name, port)) { UpnpFinish(); free(result_device); return NULL; } return result_device; } void upnp_device_shutdown(struct upnp_device *device) { UpnpFinish(); } struct service *find_service(struct upnp_device_descriptor *device_def, const char *service_id) { struct service *event_service; int serviceNum = 0; assert(device_def != NULL); assert(service_id != NULL); while (event_service = device_def->services[serviceNum], event_service != NULL) { if (strcmp(event_service->service_id, service_id) == 0) return event_service; serviceNum++; } return NULL; } /// ---- code to generate device descriptor static struct xmlelement *gen_specversion(struct xmldoc *doc, int major, int minor) { struct xmlelement *top; top=xmlelement_new(doc, "specVersion"); add_value_element_int(doc, top, "major", major); add_value_element_int(doc, top, "minor", minor); return top; } static struct xmlelement *gen_desc_iconlist(struct xmldoc *doc, struct icon **icons) { struct xmlelement *top; struct xmlelement *parent; struct icon *icon_entry; top=xmlelement_new(doc, "iconList"); for (int i = 0; (icon_entry=icons[i]); i++) { parent=xmlelement_new(doc, "icon"); add_value_element(doc,parent,"mimetype", icon_entry->mimetype); add_value_element_int(doc,parent,"width",icon_entry->width); add_value_element_int(doc,parent,"height",icon_entry->height); add_value_element_int(doc,parent,"depth",icon_entry->depth); add_value_element(doc,parent,"url",icon_entry->url); xmlelement_add_element(doc, top, parent); } return top; } static struct xmlelement * gen_desc_servicelist(struct upnp_device_descriptor *device_def, struct xmldoc *doc) { int i; struct service *srv; struct xmlelement *top; struct xmlelement *parent; top=xmlelement_new(doc, "serviceList"); for (i=0; (srv = device_def->services[i]); i++) { parent = xmlelement_new(doc, "service"); add_value_element(doc, parent, "serviceType",srv->service_type); add_value_element(doc, parent, "serviceId", srv->service_id); add_value_element(doc, parent, "SCPDURL", srv->scpd_url); add_value_element(doc, parent, "controlURL", srv->control_url); add_value_element(doc, parent, "eventSubURL", srv->event_url); xmlelement_add_element(doc, top, parent); } return top; } static struct xmldoc *generate_desc(struct upnp_device_descriptor *device_def) { struct xmldoc *doc; struct xmlelement *root; struct xmlelement *child; struct xmlelement *parent; doc = xmldoc_new(); root=xmldoc_new_topelement(doc, "root", "urn:schemas-upnp-org:device-1-0"); child=gen_specversion(doc,1,0); xmlelement_add_element(doc, root, child); parent=xmlelement_new(doc, "device"); xmlelement_add_element(doc, root, parent); add_value_element(doc,parent,"deviceType", device_def->device_type); add_value_element(doc,parent,"presentationURL", device_def->presentation_url); add_value_element(doc,parent,"friendlyName", device_def->friendly_name); add_value_element(doc,parent,"manufacturer", device_def->manufacturer); add_value_element(doc,parent,"manufacturerURL", device_def->manufacturer_url); add_value_element(doc,parent,"modelDescription", device_def->model_description); add_value_element(doc,parent,"modelName", device_def->model_name); add_value_element(doc,parent,"modelNumber", device_def->model_number); add_value_element(doc,parent,"modelURL", device_def->model_url); add_value_element(doc,parent,"UDN", device_def->udn); //add_value_element(doc,parent,"serialNumber", device_def->serial_number); //add_value_element(doc,parent,"UPC", device_def->upc); if (device_def->icons) { child=gen_desc_iconlist(doc,device_def->icons); xmlelement_add_element(doc,parent,child); } child=gen_desc_servicelist(device_def, doc); xmlelement_add_element(doc, parent, child); return doc; } char *upnp_create_device_desc(struct upnp_device_descriptor *device_def) { char *result = NULL; struct xmldoc *doc; doc = generate_desc(device_def); if (doc != NULL) { result = xmldoc_tostring(doc); xmldoc_free(doc); } return result; } gmrender-resurrect-0.0.9/src/upnp_device.h000066400000000000000000000056101400533760300206030ustar00rootroot00000000000000/* upnp_device.h - Generic UPnP Device handler * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_DEVICE_H #define _UPNP_DEVICE_H struct upnp_device_descriptor { int (*init_function) (void); const char *device_type; const char *friendly_name; const char *manufacturer; const char *manufacturer_url; const char *model_description; const char *model_name; const char *model_number; const char *model_url; const char *serial_number; const char *udn; const char *upc; const char *presentation_url; const char *mime_filter; struct icon **icons; struct service **services; }; // .. and this 'device'. This is an opaque type containing internals. struct upnp_device; struct action_event; struct upnp_device *upnp_device_init(struct upnp_device_descriptor *device_def, const char *interface_name, unsigned short port); void upnp_device_shutdown(struct upnp_device *device); int upnp_add_response(struct action_event *event, const char *key, const char *value); void upnp_set_error(struct action_event *event, int error_code, const char *format, ...); // Returns a readonly value stored in the action event. Returned value // only valid for the life-time of "event". const char *upnp_get_string(struct action_event *event, const char *key); // Append variable, identified by the variable number, to the event, // store the value under the given parameter name. The caller needs to provide // a valid variable number (assert()-ed). void upnp_append_variable(struct action_event *event, int varnum, const char *paramname); int upnp_device_notify(struct upnp_device *device, const char *serviceID, const char **varnames, const char **varvalues, int varcount); struct service *find_service(struct upnp_device_descriptor *device_def, const char *service_name); // Returns a newly allocated string with the device descriptor. char *upnp_create_device_desc(struct upnp_device_descriptor *device_def); #endif /* _UPNP_DEVICE_H */ gmrender-resurrect-0.0.9/src/upnp_renderer.c000066400000000000000000000077621400533760300211570ustar00rootroot00000000000000/* upnp_renderer.c - UPnP renderer routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include "webserver.h" #include "upnp_service.h" #include "upnp_device.h" #include "upnp_connmgr.h" #include "upnp_control.h" #include "upnp_transport.h" #include "upnp_renderer.h" #include "git-version.h" static struct icon icon1 = { .width = 64, .height = 64, .depth = 24, .url = "/upnp/grender-64x64.png", .mimetype = "image/png" }; static struct icon icon2 = { .width = 128, .height = 128, .depth = 24, .url = "/upnp/grender-128x128.png", .mimetype = "image/png" }; static struct icon *renderer_icon[] = { &icon1, &icon2, NULL }; static int upnp_renderer_init(void); static struct upnp_device_descriptor render_device = { .init_function = upnp_renderer_init, .device_type = "urn:schemas-upnp-org:device:MediaRenderer:1", .friendly_name = "GMediaRender", .manufacturer = "Ivo Clarysse, Henner Zeller", .manufacturer_url = "http://github.com/hzeller/gmrender-resurrect", .model_description = PACKAGE_STRING, .model_name = PACKAGE_NAME, .model_number = GM_COMPILE_VERSION, .model_url = "http://github.com/hzeller/gmrender-resurrect", .serial_number = "1", .udn = "uuid:GMediaRender-1_0-000-000-002", .upc = "", .presentation_url = "", // TODO(hzeller) show something useful. .mime_filter = NULL, .icons = renderer_icon, .services = NULL, /* set later */ }; void upnp_renderer_dump_connmgr_scpd(void) { char *buf; buf = upnp_get_scpd(upnp_connmgr_get_service()); assert(buf != NULL); fputs(buf, stdout); } void upnp_renderer_dump_control_scpd(void) { char *buf; buf = upnp_get_scpd(upnp_control_get_service()); assert(buf != NULL); fputs(buf, stdout); } void upnp_renderer_dump_transport_scpd(void) { char *buf; buf = upnp_get_scpd(upnp_transport_get_service()); assert(buf != NULL); fputs(buf, stdout); } static int upnp_renderer_init(void) { static struct service *upnp_services[4]; upnp_services[0] = upnp_transport_get_service(); upnp_services[1] = upnp_connmgr_get_service(); upnp_services[2] = upnp_control_get_service(); upnp_services[3] = NULL; render_device.services = upnp_services; return connmgr_init(render_device.mime_filter); } struct upnp_device_descriptor * upnp_renderer_descriptor(const char *friendly_name, const char *uuid, const char* mime_filter) { render_device.friendly_name = friendly_name; render_device.mime_filter = mime_filter; char *udn = NULL; if (asprintf(&udn, "uuid:%s", uuid) > 0) { render_device.udn = udn; } return &render_device; } gmrender-resurrect-0.0.9/src/upnp_renderer.h000066400000000000000000000023471400533760300211560ustar00rootroot00000000000000/* upnp_renderer.h - UPnP renderer definitions * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_RENDERER_H #define _UPNP_RENDERER_H void upnp_renderer_dump_connmgr_scpd(void); void upnp_renderer_dump_control_scpd(void); void upnp_renderer_dump_transport_scpd(void); // Returned pointer not owned. struct upnp_device_descriptor *upnp_renderer_descriptor(const char *name, const char *uuid, const char* mime_filter); #endif /* _UPNP_RENDERER_H */ gmrender-resurrect-0.0.9/src/upnp_service.c000066400000000000000000000161651400533760300210060ustar00rootroot00000000000000/* upnp.c - Generic UPnP routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #include #include "xmldoc.h" #include "upnp_service.h" #include "variable-container.h" static const char *param_datatype_names[] = { [DATATYPE_STRING] = "string", [DATATYPE_BOOLEAN] = "boolean", [DATATYPE_I2] = "i2", [DATATYPE_I4] = "i4", [DATATYPE_UI2] = "ui2", [DATATYPE_UI4] = "ui4", [DATATYPE_UNKNOWN] = NULL }; static struct xmlelement *gen_specversion(struct xmldoc *doc, int major, int minor) { struct xmlelement *top; top=xmlelement_new(doc, "specVersion"); add_value_element_int(doc, top, "major", major); add_value_element_int(doc, top, "minor", minor); return top; } static struct xmlelement *gen_scpd_action(struct xmldoc *doc, struct action *act, struct argument *arglist, const struct var_meta *meta_array) { struct xmlelement *top; struct xmlelement *parent,*child; top=xmlelement_new(doc, "action"); add_value_element(doc, top, "name", act->action_name); if (arglist) { struct argument *arg; int j; parent=xmlelement_new(doc, "argumentList"); xmlelement_add_element(doc, top, parent); /* a NULL name is the sentinel for 'end of list' */ for(j=0; (arg=&arglist[j], arg->name); j++) { child=xmlelement_new(doc, "argument"); add_value_element(doc,child,"name", arg->name); add_value_element(doc,child,"direction", (arg->direction == PARAM_DIR_IN) ? "in" : "out"); add_value_element(doc,child,"relatedStateVariable", meta_array[arg->statevar].name); xmlelement_add_element(doc, parent,child); } } return top; } static struct xmlelement *gen_scpd_actionlist(struct xmldoc *doc, struct service *srv) { struct xmlelement *top; struct xmlelement *child; int i; const struct var_meta* meta_array = VariableContainer_get_meta( srv->variable_container, NULL); top=xmlelement_new(doc, "actionList"); for(i=0; icommand_count; i++) { struct action *act; struct argument *arglist; act=&(srv->actions[i]); arglist=srv->action_arguments[i]; if (act) { child=gen_scpd_action(doc, act, arglist, meta_array); xmlelement_add_element(doc, top, child); } } return top; } static struct xmlelement *gen_scpd_statevar(struct xmldoc *doc, const struct var_meta *meta) { struct xmlelement *top,*parent; const char **valuelist; struct param_range *range; valuelist = meta->allowed_values; range = meta->allowed_range; top=xmlelement_new(doc, "stateVariable"); xmlelement_set_attribute(doc, top, "sendEvents",(meta->sendevents==EV_YES)?"yes":"no"); add_value_element(doc,top,"name", meta->name); add_value_element(doc,top,"dataType", param_datatype_names[meta->datatype]); if (valuelist) { const char *allowed_value; int i; parent=xmlelement_new(doc, "allowedValueList"); xmlelement_add_element(doc, top, parent); for(i=0; (allowed_value=valuelist[i]); i++) { add_value_element(doc,parent,"allowedValue", allowed_value); } } if (range) { parent=xmlelement_new(doc, "allowedValueRange"); xmlelement_add_element(doc, top, parent); add_value_element_long(doc,parent,"minimum",range->min); add_value_element_long(doc,parent,"maximum",range->max); if (range->step != 0L) { add_value_element_long(doc,parent,"step",range->step); } } assert(!(valuelist && range)); // Discrete values _and_ range ? if (meta->default_value) { // Reconsider: we never set the default value before, mostly // because there/ were 'initial values' (also called default) // and default values in the service meta-data. Was it confusion // or intent ? Hard to tell. // // Now, the default/init values are all in the meta-data struct, // the XML would be populated with a lot more default values. // Let's find what the reference documentation says about this. // // Previously, there was exactly _one_ variable in the XML that // set the default value, which is CurrentPlayMode = "NORMAL". // To make sure that we're not breaking anything accidentally, // the following condition is _very_ specific keep it this way. // (technical changes should be separate from functional changes) if (strcmp(meta->name, "CurrentPlayMode") == 0 && strcmp(meta->default_value, "NORMAL") == 0) { add_value_element(doc,top,"defaultValue", meta->default_value); } } return top; } static struct xmlelement *gen_scpd_servicestatetable(struct xmldoc *doc, struct service *srv) { struct xmlelement *top; struct xmlelement *child; int i; top=xmlelement_new(doc, "serviceStateTable"); int var_count; const struct var_meta* meta_array = VariableContainer_get_meta( srv->variable_container, &var_count); for (i = 0; i < var_count; i++) { const struct var_meta *meta = &(meta_array[i]); child=gen_scpd_statevar(doc, meta); xmlelement_add_element(doc, top, child); } return top; } static struct xmldoc *generate_scpd(struct service *srv) { struct xmldoc *doc; struct xmlelement *root; struct xmlelement *child; doc = xmldoc_new(); root=xmldoc_new_topelement(doc, "scpd", "urn:schemas-upnp-org:service-1-0"); child=gen_specversion(doc,1,0); xmlelement_add_element(doc, root, child); child=gen_scpd_actionlist(doc,srv); xmlelement_add_element(doc, root, child); child=gen_scpd_servicestatetable(doc,srv); xmlelement_add_element(doc, root, child); return doc; } struct action *find_action(struct service *event_service, const char *action_name) { struct action *event_action; int actionNum = 0; if (event_service == NULL) return NULL; while (event_action = &(event_service->actions[actionNum]), event_action->action_name != NULL) { if (strcmp(event_action->action_name, action_name) == 0) return event_action; actionNum++; } return NULL; } char *upnp_get_scpd(struct service *srv) { char *result = NULL; struct xmldoc *doc; doc = generate_scpd(srv); if (doc != NULL) { result = xmldoc_tostring(doc); xmldoc_free(doc); } return result; } gmrender-resurrect-0.0.9/src/upnp_service.h000066400000000000000000000053261400533760300210100ustar00rootroot00000000000000/* upnp.h - Generic UPnP definitions * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_SERVICE_H #define _UPNP_SERVICE_H #include #include #include "upnp_compat.h" struct action; struct service; struct action_event; struct variable_container; struct upnp_last_change_collector; struct action { const char *action_name; int (*callback) (struct action_event *); }; typedef enum { PARAM_DIR_IN, PARAM_DIR_OUT, } param_dir; struct argument { const char *name; param_dir direction; int statevar; }; typedef enum { DATATYPE_STRING, DATATYPE_BOOLEAN, DATATYPE_I2, DATATYPE_I4, DATATYPE_UI2, DATATYPE_UI4, DATATYPE_UNKNOWN } param_datatype; typedef enum { EV_NO, EV_YES } param_event; struct param_range { long long min; long long max; long long step; }; struct var_meta { int id; const char *name; const char *default_value; param_event sendevents; param_datatype datatype; const char **allowed_values; struct param_range *allowed_range; }; struct icon { int width; int height; int depth; const char *url; const char *mimetype; }; struct service { ithread_mutex_t *service_mutex; const char *service_id; const char *service_type; const char *scpd_url; const char *control_url; const char *event_url; const char *event_xml_ns; struct action *actions; struct argument **action_arguments; struct variable_container *variable_container; struct upnp_last_change_collector *last_change; int command_count; }; struct action_event { UpnpActionRequest *request; int status; struct service *service; struct upnp_device *device; }; struct action *find_action(struct service *event_service, const char *action_name); char *upnp_get_scpd(struct service *srv); #endif /* _UPNP_SERVICE_H */ gmrender-resurrect-0.0.9/src/upnp_transport.c000066400000000000000000000776331400533760300214110ustar00rootroot00000000000000/* upnp_transport.c - UPnP AVTransport routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "upnp_transport.h" #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include "output.h" #include "upnp_service.h" #include "upnp_device.h" #include "variable-container.h" #include "xmlescape.h" #define TRANSPORT_TYPE "urn:schemas-upnp-org:service:AVTransport:1" #define TRANSPORT_SERVICE_ID "urn:upnp-org:serviceId:AVTransport" #define TRANSPORT_SCPD_URL "/upnp/rendertransportSCPD.xml" #define TRANSPORT_CONTROL_URL "/upnp/control/rendertransport1" #define TRANSPORT_EVENT_URL "/upnp/event/rendertransport1" // Namespace, see UPnP-av-AVTransport-v3-Service-20101231.pdf page 15 #define TRANSPORT_EVENT_XML_NS "urn:schemas-upnp-org:metadata-1-0/AVT/" enum { TRANSPORT_CMD_GETCURRENTTRANSPORTACTIONS, TRANSPORT_CMD_GETDEVICECAPABILITIES, TRANSPORT_CMD_GETMEDIAINFO, TRANSPORT_CMD_GETPOSITIONINFO, TRANSPORT_CMD_GETTRANSPORTINFO, TRANSPORT_CMD_GETTRANSPORTSETTINGS, TRANSPORT_CMD_PAUSE, TRANSPORT_CMD_PLAY, TRANSPORT_CMD_SEEK, TRANSPORT_CMD_SETAVTRANSPORTURI, TRANSPORT_CMD_STOP, TRANSPORT_CMD_SETNEXTAVTRANSPORTURI, // Not implemented //TRANSPORT_CMD_NEXT, //TRANSPORT_CMD_PREVIOUS, //TRANSPORT_CMD_SETPLAYMODE, //TRANSPORT_CMD_RECORD, //TRANSPORT_CMD_SETRECORDQUALITYMODE, TRANSPORT_CMD_COUNT }; enum UPNPTransportError { UPNP_TRANSPORT_E_TRANSITION_NA = 701, UPNP_TRANSPORT_E_NO_CONTENTS = 702, UPNP_TRANSPORT_E_READ_ERROR = 703, UPNP_TRANSPORT_E_PLAY_FORMAT_NS = 704, UPNP_TRANSPORT_E_TRANSPORT_LOCKED = 705, UPNP_TRANSPORT_E_WRITE_ERROR = 706, UPNP_TRANSPORT_E_REC_MEDIA_WP = 707, UPNP_TRANSPORT_E_REC_FORMAT_NS = 708, UPNP_TRANSPORT_E_REC_MEDIA_FULL = 709, UPNP_TRANSPORT_E_SEEKMODE_NS = 710, UPNP_TRANSPORT_E_ILL_SEEKTARGET = 711, UPNP_TRANSPORT_E_PLAYMODE_NS = 712, UPNP_TRANSPORT_E_RECQUAL_NS = 713, UPNP_TRANSPORT_E_ILLEGAL_MIME = 714, UPNP_TRANSPORT_E_CONTENT_BUSY = 715, UPNP_TRANSPORT_E_RES_NOT_FOUND = 716, UPNP_TRANSPORT_E_PLAYSPEED_NS = 717, UPNP_TRANSPORT_E_INVALID_IID = 718, }; static const char kZeroTime[] = "0:00:00"; enum transport_state { TRANSPORT_STOPPED, TRANSPORT_PLAYING, TRANSPORT_TRANSITIONING, /* optional */ TRANSPORT_PAUSED_PLAYBACK, /* optional */ TRANSPORT_PAUSED_RECORDING, /* optional */ TRANSPORT_RECORDING, /* optional */ TRANSPORT_NO_MEDIA_PRESENT /* optional */ }; static const char *transport_states[] = { "STOPPED", "PLAYING", "TRANSITIONING", "PAUSED_PLAYBACK", "PAUSED_RECORDING", "RECORDING", "NO_MEDIA_PRESENT", NULL }; static const char *transport_stati[] = { "OK", "ERROR_OCCURRED", " vendor-defined ", NULL }; static const char *media[] = { "UNKNOWN", "DV", "MINI-DV", "VHS", "W-VHS", "S-VHS", "D-VHS", "VHSC", "VIDEO8", "HI8", "CD-ROM", "CD-DA", "CD-R", "CD-RW", "VIDEO-CD", "SACD", "MD-AUDIO", "MD-PICTURE", "DVD-ROM", "DVD-VIDEO", "DVD-R", "DVD+RW", "DVD-RW", "DVD-RAM", "DVD-AUDIO", "DAT", "LD", "HDD", "MICRO-MV", "NETWORK", "NONE", "NOT_IMPLEMENTED", " vendor-defined ", NULL }; static const char *playmodi[] = { "NORMAL", //"SHUFFLE", //"REPEAT_ONE", "REPEAT_ALL", //"RANDOM", //"DIRECT_1", "INTRO", NULL }; static const char *playspeeds[] = { "1", " vendor-defined ", NULL }; static const char *rec_write_stati[] = { "WRITABLE", "PROTECTED", "NOT_WRITABLE", "UNKNOWN", "NOT_IMPLEMENTED", NULL }; static const char *rec_quality_modi[] = { "0:EP", "1:LP", "2:SP", "0:BASIC", "1:MEDIUM", "2:HIGH", "NOT_IMPLEMENTED", " vendor-defined ", NULL }; static const char *aat_seekmodi[] = { "ABS_TIME", "REL_TIME", "ABS_COUNT", "REL_COUNT", "TRACK_NR", "CHANNEL_FREQ", "TAPE-INDEX", "FRAME", NULL }; static struct param_range track_range = { 0, 4294967295LL, 1 }; static struct param_range track_nr_range = { 0, 4294967295LL, 0 }; typedef enum { TRANSPORT_VAR_TRANSPORT_STATUS, TRANSPORT_VAR_NEXT_AV_URI, TRANSPORT_VAR_NEXT_AV_URI_META, TRANSPORT_VAR_CUR_TRACK_META, TRANSPORT_VAR_REL_CTR_POS, TRANSPORT_VAR_AAT_INSTANCE_ID, TRANSPORT_VAR_AAT_SEEK_TARGET, TRANSPORT_VAR_PLAY_MEDIUM, TRANSPORT_VAR_REL_TIME_POS, TRANSPORT_VAR_REC_MEDIA, TRANSPORT_VAR_CUR_PLAY_MODE, TRANSPORT_VAR_TRANSPORT_PLAY_SPEED, TRANSPORT_VAR_PLAY_MEDIA, TRANSPORT_VAR_ABS_TIME_POS, TRANSPORT_VAR_CUR_TRACK, TRANSPORT_VAR_CUR_TRACK_URI, TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS, TRANSPORT_VAR_NR_TRACKS, TRANSPORT_VAR_AV_URI, TRANSPORT_VAR_ABS_CTR_POS, TRANSPORT_VAR_CUR_REC_QUAL_MODE, TRANSPORT_VAR_CUR_MEDIA_DUR, TRANSPORT_VAR_AAT_SEEK_MODE, TRANSPORT_VAR_AV_URI_META, TRANSPORT_VAR_REC_MEDIUM, TRANSPORT_VAR_REC_MEDIUM_WR_STATUS, TRANSPORT_VAR_LAST_CHANGE, TRANSPORT_VAR_CUR_TRACK_DUR, TRANSPORT_VAR_TRANSPORT_STATE, TRANSPORT_VAR_POS_REC_QUAL_MODE, TRANSPORT_VAR_COUNT } transport_variable_t; static struct argument arguments_setavtransporturi[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "CurrentURI", PARAM_DIR_IN, TRANSPORT_VAR_AV_URI }, { "CurrentURIMetaData", PARAM_DIR_IN, TRANSPORT_VAR_AV_URI_META }, { NULL } }; static struct argument arguments_setnextavtransporturi[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "NextURI", PARAM_DIR_IN, TRANSPORT_VAR_NEXT_AV_URI }, { "NextURIMetaData", PARAM_DIR_IN, TRANSPORT_VAR_NEXT_AV_URI_META }, { NULL } }; static struct argument arguments_getmediainfo[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "NrTracks", PARAM_DIR_OUT, TRANSPORT_VAR_NR_TRACKS }, { "MediaDuration", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_MEDIA_DUR }, { "CurrentURI", PARAM_DIR_OUT, TRANSPORT_VAR_AV_URI }, { "CurrentURIMetaData", PARAM_DIR_OUT, TRANSPORT_VAR_AV_URI_META }, { "NextURI", PARAM_DIR_OUT, TRANSPORT_VAR_NEXT_AV_URI }, { "NextURIMetaData", PARAM_DIR_OUT, TRANSPORT_VAR_NEXT_AV_URI_META }, { "PlayMedium", PARAM_DIR_OUT, TRANSPORT_VAR_PLAY_MEDIUM }, { "RecordMedium", PARAM_DIR_OUT, TRANSPORT_VAR_REC_MEDIUM }, { "WriteStatus", PARAM_DIR_OUT, TRANSPORT_VAR_REC_MEDIUM_WR_STATUS }, { NULL } }; static struct argument arguments_gettransportinfo[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "CurrentTransportState", PARAM_DIR_OUT, TRANSPORT_VAR_TRANSPORT_STATE }, { "CurrentTransportStatus", PARAM_DIR_OUT, TRANSPORT_VAR_TRANSPORT_STATUS }, { "CurrentSpeed", PARAM_DIR_OUT, TRANSPORT_VAR_TRANSPORT_PLAY_SPEED }, { NULL } }; static struct argument arguments_getpositioninfo[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "Track", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_TRACK }, { "TrackDuration", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_TRACK_DUR }, { "TrackMetaData", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_TRACK_META }, { "TrackURI", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_TRACK_URI }, { "RelTime", PARAM_DIR_OUT, TRANSPORT_VAR_REL_TIME_POS }, { "AbsTime", PARAM_DIR_OUT, TRANSPORT_VAR_ABS_TIME_POS }, { "RelCount", PARAM_DIR_OUT, TRANSPORT_VAR_REL_CTR_POS }, { "AbsCount", PARAM_DIR_OUT, TRANSPORT_VAR_ABS_CTR_POS }, { NULL } }; static struct argument arguments_getdevicecapabilities[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "PlayMedia", PARAM_DIR_OUT, TRANSPORT_VAR_PLAY_MEDIA }, { "RecMedia", PARAM_DIR_OUT, TRANSPORT_VAR_REC_MEDIA }, { "RecQualityModes", PARAM_DIR_OUT, TRANSPORT_VAR_POS_REC_QUAL_MODE }, { NULL } }; static struct argument arguments_gettransportsettings[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "PlayMode", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_PLAY_MODE }, { "RecQualityMode", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_REC_QUAL_MODE }, { NULL } }; static struct argument arguments_stop[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { NULL } }; static struct argument arguments_play[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "Speed", PARAM_DIR_IN, TRANSPORT_VAR_TRANSPORT_PLAY_SPEED }, { NULL } }; static struct argument arguments_pause[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { NULL } }; //static struct argument arguments_record[] = { // { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, // { NULL } //}; static struct argument arguments_seek[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "Unit", PARAM_DIR_IN, TRANSPORT_VAR_AAT_SEEK_MODE }, { "Target", PARAM_DIR_IN, TRANSPORT_VAR_AAT_SEEK_TARGET }, { NULL } }; //static struct argument arguments_next[] = { // { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, // { NULL } //}; //static struct argument arguments_previous[] = { // { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, // { NULL } //}; //static struct argument arguments_setplaymode[] = { // { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, // { "NewPlayMode", PARAM_DIR_IN, TRANSPORT_VAR_CUR_PLAY_MODE }, // { NULL } //}; //static struct argument arguments_setrecordqualitymode[] = { // { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, // { "NewRecordQualityMode", PARAM_DIR_IN, TRANSPORT_VAR_CUR_REC_QUAL_MODE }, // { NULL } //}; static struct argument arguments_getcurrenttransportactions[] = { { "InstanceID", PARAM_DIR_IN, TRANSPORT_VAR_AAT_INSTANCE_ID }, { "Actions", PARAM_DIR_OUT, TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS }, { NULL } }; static struct argument *argument_list[] = { [TRANSPORT_CMD_GETCURRENTTRANSPORTACTIONS] = arguments_getcurrenttransportactions, [TRANSPORT_CMD_GETDEVICECAPABILITIES] = arguments_getdevicecapabilities, [TRANSPORT_CMD_GETMEDIAINFO] = arguments_getmediainfo, [TRANSPORT_CMD_GETPOSITIONINFO] = arguments_getpositioninfo, [TRANSPORT_CMD_GETTRANSPORTINFO] = arguments_gettransportinfo, [TRANSPORT_CMD_GETTRANSPORTSETTINGS] = arguments_gettransportsettings, [TRANSPORT_CMD_PAUSE] = arguments_pause, [TRANSPORT_CMD_PLAY] = arguments_play, [TRANSPORT_CMD_SEEK] = arguments_seek, [TRANSPORT_CMD_SETAVTRANSPORTURI] = arguments_setavtransporturi, [TRANSPORT_CMD_STOP] = arguments_stop, [TRANSPORT_CMD_SETNEXTAVTRANSPORTURI] = arguments_setnextavtransporturi, //[TRANSPORT_CMD_RECORD] = arguments_record, //[TRANSPORT_CMD_NEXT] = arguments_next, //[TRANSPORT_CMD_PREVIOUS] = arguments_previous, //[TRANSPORT_CMD_SETPLAYMODE] = arguments_setplaymode, //[TRANSPORT_CMD_SETRECORDQUALITYMODE] = arguments_setrecordqualitymode, [TRANSPORT_CMD_COUNT] = NULL }; // Our 'instance' variables. static enum transport_state transport_state_ = TRANSPORT_STOPPED; static variable_container_t *state_variables_ = NULL; /* protects transport_values, and service-specific state */ static ithread_mutex_t transport_mutex; static void service_lock(void) { ithread_mutex_lock(&transport_mutex); struct upnp_last_change_collector * collector = upnp_transport_get_service()->last_change; if (collector) { UPnPLastChangeCollector_start(collector); } } static void service_unlock(void) { struct upnp_last_change_collector * collector = upnp_transport_get_service()->last_change; if (collector) { UPnPLastChangeCollector_finish(collector); } ithread_mutex_unlock(&transport_mutex); } static char has_instance_id(struct action_event *event) { const char *const value = upnp_get_string(event, "InstanceID"); if (value == NULL) { upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS, "Missing InstanceID"); } return value != NULL; } static int get_media_info(struct action_event *event) { if (!has_instance_id(event)) { return -1; } upnp_append_variable(event, TRANSPORT_VAR_NR_TRACKS, "NrTracks"); upnp_append_variable(event, TRANSPORT_VAR_CUR_MEDIA_DUR, "MediaDuration"); upnp_append_variable(event, TRANSPORT_VAR_AV_URI, "CurrentURI"); upnp_append_variable(event, TRANSPORT_VAR_AV_URI_META, "CurrentURIMetaData"); upnp_append_variable(event, TRANSPORT_VAR_NEXT_AV_URI, "NextURI"); upnp_append_variable(event, TRANSPORT_VAR_NEXT_AV_URI_META, "NextURIMetaData"); upnp_append_variable(event, TRANSPORT_VAR_REC_MEDIA, "PlayMedium"); upnp_append_variable(event, TRANSPORT_VAR_REC_MEDIUM, "RecordMedium"); upnp_append_variable(event, TRANSPORT_VAR_REC_MEDIUM_WR_STATUS, "WriteStatus"); return 0; } // Replace given variable without sending an state-change event. static int replace_var(transport_variable_t varnum, const char *new_value) { return VariableContainer_change(state_variables_, varnum, new_value); } static const char *get_var(transport_variable_t varnum) { return VariableContainer_get(state_variables_, varnum, NULL); } // Transport uri always comes in uri/meta pairs. Set these and also the related // track uri/meta variables. // Returns 1, if this meta-data likely needs to be updated while the stream // is playing (e.g. radio broadcast). static int replace_transport_uri_and_meta(const char *uri, const char *meta) { replace_var(TRANSPORT_VAR_AV_URI, uri); replace_var(TRANSPORT_VAR_AV_URI_META, meta); // This influences as well the tracks. If there is a non-empty URI, // we have exactly one track. const char *tracks = (uri != NULL && strlen(uri) > 0) ? "1" : "0"; replace_var(TRANSPORT_VAR_NR_TRACKS, tracks); // We only really want to send back meta data if we didn't get anything // useful or if this is an audio item. const int requires_stream_meta_callback = (strlen(meta) == 0) || strstr(meta, "object.item.audioItem"); return requires_stream_meta_callback; } // Similar to replace_transport_uri_and_meta() above, but current values. static void replace_current_uri_and_meta(const char *uri, const char *meta){ const char *tracks = (uri != NULL && strlen(uri) > 0) ? "1" : "0"; replace_var(TRANSPORT_VAR_CUR_TRACK, tracks); replace_var(TRANSPORT_VAR_CUR_TRACK_URI, uri); replace_var(TRANSPORT_VAR_CUR_TRACK_META, meta); } static void change_transport_state(enum transport_state new_state) { transport_state_ = new_state; assert(new_state >= TRANSPORT_STOPPED && new_state < TRANSPORT_NO_MEDIA_PRESENT); if (!replace_var(TRANSPORT_VAR_TRANSPORT_STATE, transport_states[new_state])) { return; // no change. } const char *available_actions = NULL; switch (new_state) { case TRANSPORT_STOPPED: if (strlen(get_var(TRANSPORT_VAR_AV_URI)) == 0) { available_actions = "PLAY"; } else { available_actions = "PLAY,SEEK"; } break; case TRANSPORT_PLAYING: available_actions = "PAUSE,STOP,SEEK"; break; case TRANSPORT_PAUSED_PLAYBACK: available_actions = "PLAY,STOP,SEEK"; break; case TRANSPORT_TRANSITIONING: case TRANSPORT_PAUSED_RECORDING: case TRANSPORT_RECORDING: case TRANSPORT_NO_MEDIA_PRESENT: // We should not switch to this state. break; } if (available_actions) { replace_var(TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS, available_actions); } } // Callback from our output if the song meta data changed. static void update_meta_from_stream(const struct SongMetaData *meta) { if (meta->title == NULL || strlen(meta->title) == 0) { return; } const char *original_xml = get_var(TRANSPORT_VAR_AV_URI_META); char *didl = SongMetaData_to_DIDL(meta, original_xml); service_lock(); replace_var(TRANSPORT_VAR_AV_URI_META, didl); replace_var(TRANSPORT_VAR_CUR_TRACK_META, didl); service_unlock(); free(didl); } /* UPnP action handlers */ static int set_avtransport_uri(struct action_event *event) { if (!has_instance_id(event)) { return -1; } const char *uri = upnp_get_string(event, "CurrentURI"); if (uri == NULL) { return -1; } service_lock(); const char *meta = upnp_get_string(event, "CurrentURIMetaData"); // Transport URI/Meta set now, current URI/Meta when it starts playing. int requires_meta_update = replace_transport_uri_and_meta(uri, meta); if (transport_state_ == TRANSPORT_PLAYING) { // Uh, wrong state. // Usually, this should not be called while we are PLAYING, only // STOPPED or PAUSED. But if actually some controller sets this // while playing, probably the best is to update the current // current URI/Meta as well to reflect the state best. replace_current_uri_and_meta(uri, meta); } output_set_uri(uri, (requires_meta_update ? update_meta_from_stream : NULL)); service_unlock(); return 0; } static int set_next_avtransport_uri(struct action_event *event) { if (!has_instance_id(event)) { return -1; } const char *next_uri = upnp_get_string(event, "NextURI"); if (next_uri == NULL) { return -1; } int rc = 0; service_lock(); output_set_next_uri(next_uri); replace_var(TRANSPORT_VAR_NEXT_AV_URI, next_uri); const char *next_uri_meta = upnp_get_string(event, "NextURIMetaData"); if (next_uri_meta == NULL) { rc = -1; } else { replace_var(TRANSPORT_VAR_NEXT_AV_URI_META, next_uri_meta); } service_unlock(); return rc; } static int get_transport_info(struct action_event *event) { if (!has_instance_id(event)) { return -1; } upnp_append_variable(event, TRANSPORT_VAR_TRANSPORT_STATE, "CurrentTransportState"); upnp_append_variable(event, TRANSPORT_VAR_TRANSPORT_STATUS, "CurrentTransportStatus"); upnp_append_variable(event, TRANSPORT_VAR_TRANSPORT_PLAY_SPEED, "CurrentSpeed"); return 0; } static int get_current_transportactions(struct action_event *event) { if (!has_instance_id(event)) { return -1; } upnp_append_variable(event, TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS, "Actions"); return 0; } static int get_transport_settings(struct action_event *event) { if (!has_instance_id(event)) { return -1; } // TODO: what variables to add ? return 0; } // Print UPnP formatted time into given buffer. time given in nanoseconds. static int divide_leave_remainder(gint64 *val, gint64 divisor) { int result = *val / divisor; *val %= divisor; return result; } static void print_upnp_time(char *result, size_t size, gint64 t) { const gint64 one_sec = 1000000000LL; // units are in nanoseconds. const int hour = divide_leave_remainder(&t, 3600LL * one_sec); const int minute = divide_leave_remainder(&t, 60LL * one_sec); const int second = divide_leave_remainder(&t, one_sec); snprintf(result, size, "%d:%02d:%02d", hour, minute, second); } static gint64 parse_upnp_time(const char *time_string) { int hour = 0; int minute = 0; int second = 0; sscanf(time_string, "%d:%02d:%02d", &hour, &minute, &second); const gint64 seconds = (hour * 3600 + minute * 60 + second); const gint64 one_sec_unit = 1000000000LL; return one_sec_unit * seconds; } // We constantly update the track time to event about it to our clients. static void *thread_update_track_time(void *userdata) { (void)userdata; const gint64 one_sec_unit = 1000000000LL; char tbuf[32]; gint64 last_duration = -1, last_position = -1; for (;;) { usleep(500000); // 500ms service_lock(); gint64 duration, position; const int pos_result = output_get_position(&duration, &position); if (pos_result == 0) { if (duration != last_duration) { print_upnp_time(tbuf, sizeof(tbuf), duration); replace_var(TRANSPORT_VAR_CUR_TRACK_DUR, tbuf); last_duration = duration; } if (position / one_sec_unit != last_position) { print_upnp_time(tbuf, sizeof(tbuf), position); replace_var(TRANSPORT_VAR_REL_TIME_POS, tbuf); last_position = position / one_sec_unit; } } service_unlock(); } return NULL; // not reached. } static int get_position_info(struct action_event *event) { if (!has_instance_id(event)) { return -1; } upnp_append_variable(event, TRANSPORT_VAR_CUR_TRACK, "Track"); upnp_append_variable(event, TRANSPORT_VAR_CUR_TRACK_DUR, "TrackDuration"); upnp_append_variable(event, TRANSPORT_VAR_CUR_TRACK_META, "TrackMetaData"); upnp_append_variable(event, TRANSPORT_VAR_CUR_TRACK_URI, "TrackURI"); upnp_append_variable(event, TRANSPORT_VAR_REL_TIME_POS, "RelTime"); upnp_append_variable(event, TRANSPORT_VAR_ABS_TIME_POS, "AbsTime"); upnp_append_variable(event, TRANSPORT_VAR_REL_CTR_POS, "RelCount"); upnp_append_variable(event, TRANSPORT_VAR_ABS_CTR_POS, "AbsCount"); return 0; } static int get_device_caps(struct action_event *event) { if (!has_instance_id(event)) { return -1; } // TODO: implement ? return 0; } static int stop(struct action_event *event) { if (!has_instance_id(event)) { return -1; } service_lock(); switch (transport_state_) { case TRANSPORT_STOPPED: // nothing to change. break; case TRANSPORT_PLAYING: case TRANSPORT_TRANSITIONING: case TRANSPORT_PAUSED_RECORDING: case TRANSPORT_RECORDING: case TRANSPORT_PAUSED_PLAYBACK: output_stop(); change_transport_state(TRANSPORT_STOPPED); break; case TRANSPORT_NO_MEDIA_PRESENT: /* action not allowed in these states - error 701 */ upnp_set_error(event, UPNP_TRANSPORT_E_TRANSITION_NA, "Transition to STOP not allowed; allowed=%s", get_var(TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS)); break; } service_unlock(); return 0; } static void inform_play_transition_from_output(enum PlayFeedback fb) { service_lock(); switch (fb) { case PLAY_STOPPED: replace_transport_uri_and_meta("", ""); replace_current_uri_and_meta("", ""); change_transport_state(TRANSPORT_STOPPED); break; case PLAY_STARTED_NEXT_STREAM: { const char *av_uri = get_var(TRANSPORT_VAR_NEXT_AV_URI); const char *av_meta = get_var(TRANSPORT_VAR_NEXT_AV_URI_META); replace_transport_uri_and_meta(av_uri, av_meta); replace_current_uri_and_meta(av_uri, av_meta); replace_var(TRANSPORT_VAR_NEXT_AV_URI, ""); replace_var(TRANSPORT_VAR_NEXT_AV_URI_META, ""); break; } } service_unlock(); } static int play(struct action_event *event) { if (!has_instance_id(event)) { return -1; } int rc = 0; service_lock(); switch (transport_state_) { case TRANSPORT_PLAYING: // Nothing to change. break; case TRANSPORT_STOPPED: // If we were stopped before, we start a new song now. So just // set the time to zero now; otherwise we will see the old // value of the previous song until it updates some fractions // of a second later. replace_var(TRANSPORT_VAR_REL_TIME_POS, kZeroTime); /* >>> fall through */ case TRANSPORT_PAUSED_PLAYBACK: if (output_play(&inform_play_transition_from_output)) { upnp_set_error(event, 704, "Playing failed"); rc = -1; } else { change_transport_state(TRANSPORT_PLAYING); const char *av_uri = get_var(TRANSPORT_VAR_AV_URI); const char *av_meta = get_var(TRANSPORT_VAR_AV_URI_META); replace_current_uri_and_meta(av_uri, av_meta); } break; case TRANSPORT_NO_MEDIA_PRESENT: case TRANSPORT_TRANSITIONING: case TRANSPORT_PAUSED_RECORDING: case TRANSPORT_RECORDING: /* action not allowed in these states - error 701 */ upnp_set_error(event, UPNP_TRANSPORT_E_TRANSITION_NA, "Transition to PLAY not allowed; allowed=%s", get_var(TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS)); rc = -1; break; } service_unlock(); return rc; } static int pause_stream(struct action_event *event) { if (!has_instance_id(event)) { return -1; } int rc = 0; service_lock(); switch (transport_state_) { case TRANSPORT_PAUSED_PLAYBACK: // Nothing to change. break; case TRANSPORT_PLAYING: if (output_pause()) { upnp_set_error(event, 704, "Pause failed"); rc = -1; } else { change_transport_state(TRANSPORT_PAUSED_PLAYBACK); } break; default: /* action not allowed in these states - error 701 */ upnp_set_error(event, UPNP_TRANSPORT_E_TRANSITION_NA, "Transition to PAUSE not allowed; allowed=%s", get_var(TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS)); rc = -1; } service_unlock(); return rc; } static int seek(struct action_event *event) { if (!has_instance_id(event)) { return -1; } const char *unit = upnp_get_string(event, "Unit"); if (strcmp(unit, "REL_TIME") == 0) { // This is the only thing we support right now. const char *target = upnp_get_string(event, "Target"); gint64 nanos = parse_upnp_time(target); service_lock(); if (output_seek(nanos) == 0) { // TODO(hzeller): Seeking might take some time, // pretend to already be there. Should we go into // TRANSITION mode ? // (gstreamer will go into PAUSE, then PLAYING) replace_var(TRANSPORT_VAR_REL_TIME_POS, target); } service_unlock(); } return 0; } static struct action transport_actions[] = { [TRANSPORT_CMD_GETCURRENTTRANSPORTACTIONS] = {"GetCurrentTransportActions", get_current_transportactions}, [TRANSPORT_CMD_GETDEVICECAPABILITIES] = {"GetDeviceCapabilities", get_device_caps}, [TRANSPORT_CMD_GETMEDIAINFO] = {"GetMediaInfo", get_media_info}, [TRANSPORT_CMD_GETPOSITIONINFO] = {"GetPositionInfo", get_position_info}, [TRANSPORT_CMD_GETTRANSPORTINFO] = {"GetTransportInfo", get_transport_info}, [TRANSPORT_CMD_GETTRANSPORTSETTINGS] = {"GetTransportSettings", get_transport_settings}, [TRANSPORT_CMD_PAUSE] = {"Pause", pause_stream}, [TRANSPORT_CMD_PLAY] = {"Play", play}, [TRANSPORT_CMD_SEEK] = {"Seek", seek}, [TRANSPORT_CMD_SETAVTRANSPORTURI] = {"SetAVTransportURI", set_avtransport_uri}, /* RC9800i */ [TRANSPORT_CMD_STOP] = {"Stop", stop}, [TRANSPORT_CMD_SETNEXTAVTRANSPORTURI] = {"SetNextAVTransportURI", set_next_avtransport_uri}, //[TRANSPORT_CMD_RECORD] = {"Record", NULL}, /* optional */ //[TRANSPORT_CMD_NEXT] = {"Next", next}, //[TRANSPORT_CMD_PREVIOUS] = {"Previous", previous}, //[TRANSPORT_CMD_SETPLAYMODE] = {"SetPlayMode", NULL}, /* optional */ //[TRANSPORT_CMD_SETRECORDQUALITYMODE] = {"SetRecordQualityMode", NULL}, /* optional */ [TRANSPORT_CMD_COUNT] = {NULL, NULL} }; struct service *upnp_transport_get_service(void) { static struct service transport_service_ = { .service_mutex = &transport_mutex, .service_id = TRANSPORT_SERVICE_ID, .service_type = TRANSPORT_TYPE, .scpd_url = TRANSPORT_SCPD_URL, .control_url = TRANSPORT_CONTROL_URL, .event_url = TRANSPORT_EVENT_URL, .event_xml_ns = TRANSPORT_EVENT_XML_NS, .actions = transport_actions, .action_arguments = argument_list, .variable_container = NULL, // set later. .last_change = NULL, .command_count = TRANSPORT_CMD_COUNT, }; static struct var_meta transport_var_meta[] = { {TRANSPORT_VAR_TRANSPORT_STATE, "TransportState", "STOPPED", EV_NO, DATATYPE_STRING, transport_states, NULL }, {TRANSPORT_VAR_TRANSPORT_STATUS, "TransportStatus", "OK", EV_NO, DATATYPE_STRING, transport_stati, NULL }, {TRANSPORT_VAR_PLAY_MEDIUM, "PlaybackStorageMedium", "UNKNOWN", EV_NO, DATATYPE_STRING, media, NULL }, {TRANSPORT_VAR_REC_MEDIUM, "RecordStorageMedium", "NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, media, NULL }, {TRANSPORT_VAR_PLAY_MEDIA, "PossiblePlaybackStorageMedia", "NETWORK,UNKNOWN", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_REC_MEDIA, "PossibleRecordStorageMedia","NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_CUR_PLAY_MODE, "CurrentPlayMode", "NORMAL", EV_NO, DATATYPE_STRING, playmodi, NULL}, {TRANSPORT_VAR_TRANSPORT_PLAY_SPEED, "TransportPlaySpeed", "1", EV_NO, DATATYPE_STRING, playspeeds, NULL }, {TRANSPORT_VAR_REC_MEDIUM_WR_STATUS, "RecordMediumWriteStatus", "NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, rec_write_stati, NULL }, {TRANSPORT_VAR_CUR_REC_QUAL_MODE, "CurrentRecordQualityMode","NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, rec_quality_modi, NULL }, {TRANSPORT_VAR_POS_REC_QUAL_MODE, "PossibleRecordQualityModes", "NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_NR_TRACKS, "NumberOfTracks", "0", EV_NO, DATATYPE_UI4, NULL, &track_nr_range }, /* no step */ {TRANSPORT_VAR_CUR_TRACK, "CurrentTrack", "0", EV_NO, DATATYPE_UI4, NULL, &track_range }, {TRANSPORT_VAR_CUR_TRACK_DUR, "CurrentTrackDuration", kZeroTime, EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_CUR_MEDIA_DUR, "CurrentMediaDuration", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_CUR_TRACK_META, "CurrentTrackMetaData", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_CUR_TRACK_URI, "CurrentTrackURI", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_AV_URI, "AVTransportURI", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_AV_URI_META, "AVTransportURIMetaData", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_NEXT_AV_URI, "NextAVTransportURI", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_NEXT_AV_URI_META, "NextAVTransportURIMetaData", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_REL_TIME_POS, "RelativeTimePosition", kZeroTime, EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_ABS_TIME_POS, "AbsoluteTimePosition", "NOT_IMPLEMENTED", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_REL_CTR_POS, "RelativeCounterPosition", "2147483647", EV_NO, DATATYPE_I4, NULL, NULL }, {TRANSPORT_VAR_ABS_CTR_POS, "AbsoluteCounterPosition", "2147483647", EV_NO, DATATYPE_I4, NULL, NULL }, {TRANSPORT_VAR_LAST_CHANGE, "LastChange", "", EV_YES, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_AAT_SEEK_MODE, "A_ARG_TYPE_SeekMode", "TRACK_NR", EV_NO, DATATYPE_STRING, aat_seekmodi, NULL }, {TRANSPORT_VAR_AAT_SEEK_TARGET, "A_ARG_TYPE_SeekTarget", "", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_AAT_INSTANCE_ID, "A_ARG_TYPE_InstanceID", "0", EV_NO, DATATYPE_UI4, NULL, NULL }, {TRANSPORT_VAR_CUR_TRANSPORT_ACTIONS, "CurrentTransportActions", "PLAY", EV_NO, DATATYPE_STRING, NULL, NULL }, {TRANSPORT_VAR_COUNT, NULL, NULL, EV_NO, DATATYPE_UNKNOWN, NULL, NULL } }; if (transport_service_.variable_container == NULL) { state_variables_ = VariableContainer_new(TRANSPORT_VAR_COUNT, transport_var_meta); transport_service_.variable_container = state_variables_; } return &transport_service_; } void upnp_transport_init(struct upnp_device *device) { struct service *service = upnp_transport_get_service(); assert(service->last_change == NULL); service->last_change = UPnPLastChangeCollector_new(service->variable_container, TRANSPORT_EVENT_XML_NS, device, TRANSPORT_SERVICE_ID); // Times and counters should not be evented. We only change REL_TIME // right now anyway (AVTransport-v1 document, 2.3.1 Event Model) UPnPLastChangeCollector_add_ignore(service->last_change, TRANSPORT_VAR_REL_TIME_POS); UPnPLastChangeCollector_add_ignore(service->last_change, TRANSPORT_VAR_ABS_TIME_POS); UPnPLastChangeCollector_add_ignore(service->last_change, TRANSPORT_VAR_REL_CTR_POS); UPnPLastChangeCollector_add_ignore(service->last_change, TRANSPORT_VAR_ABS_CTR_POS); pthread_t thread; pthread_create(&thread, NULL, thread_update_track_time, NULL); } void upnp_transport_register_variable_listener(variable_change_listener_t cb, void *userdata) { VariableContainer_register_callback(state_variables_, cb, userdata); } gmrender-resurrect-0.0.9/src/upnp_transport.h000066400000000000000000000024611400533760300214010ustar00rootroot00000000000000/* upnp_transport.h - UPnP AVTransport definitions * * Copyright (C) 2005 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _UPNP_TRANSPORT_H #define _UPNP_TRANSPORT_H #include "variable-container.h" struct service; struct upnp_device; struct service *upnp_transport_get_service(void); void upnp_transport_init(struct upnp_device *); // Register a callback to get informed when variables change. This should // return quickly. void upnp_transport_register_variable_listener(variable_change_listener_t cb, void *userdata); #endif /* _UPNP_TRANSPORT_H */ gmrender-resurrect-0.0.9/src/variable-container.c000066400000000000000000000273641400533760300220540ustar00rootroot00000000000000/* * Copyright (C) 2013 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #include "variable-container.h" #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include "upnp_device.h" #include "upnp_service.h" #include "xmlescape.h" #include "xmldoc.h" // -- VariableContainer struct cb_list { variable_change_listener_t callback; void *userdata; struct cb_list *next; }; struct variable_container { int variable_num; const struct var_meta *vars; char **values; struct cb_list *callbacks; }; static int cmp_meta_id(const void *a, const void *b) { return ((struct var_meta*)a)->id - ((struct var_meta*)b)->id; } static const struct var_meta *create_sorted_meta(int num, const struct var_meta *vars) { struct var_meta *result = (struct var_meta*)malloc(num * sizeof(struct var_meta)); memcpy(result, vars, num * sizeof(struct var_meta)); qsort(result, num, sizeof(struct var_meta), cmp_meta_id); return result; } variable_container_t *VariableContainer_new(int variable_num, const struct var_meta *unordered_vars) { assert(variable_num > 0); variable_container_t *result = (variable_container_t*)malloc(sizeof(variable_container_t)); result->variable_num = variable_num; // Right now, the model is to have a variable id, described as enum, that // can be used as an index in meta-data, variable names etc. // Previously, the order was maintained by using designated initializers // in a scattered set of array initialization, now everything is in a // meta array. // Since we want to get away from designated initializer arrays (to be // compatible with C++), we allow the meta-data to be in any order but // take care of it here. However accesses the meta-data does it through // VariableContainer result->vars = create_sorted_meta(variable_num, unordered_vars); result->values = (char **) malloc(variable_num * sizeof(char*)); result->callbacks = NULL; for (int i = 0; i < variable_num; ++i) { assert(result->vars[i].name != NULL); assert(result->vars[i].id == i); assert(result->vars[i].default_value != NULL); result->values[i] = strdup(result->vars[i].default_value); } return result; } void VariableContainer_delete(variable_container_t *object) { for (int i = 0; i < object->variable_num; ++i) { free(object->values[i]); } free(object->values); for (struct cb_list *list = object->callbacks; list; /**/) { struct cb_list *next = list->next; free(list); list = next; } free((void*)object->vars); free(object); } const struct var_meta *VariableContainer_get_meta(variable_container_t *object, int *count) { if (count) *count = object->variable_num; return object->vars; } int VariableContainer_get_num_vars(variable_container_t *object) { return object->variable_num; } const char *VariableContainer_get(variable_container_t *object, int var, const char **name) { if (var < 0 || var >= object->variable_num) return NULL; const char *varname = object->vars[var].name; if (name) *name = varname; // Names of not used variables are set to NULL. return varname ? object->values[var] : NULL; } // Change content of variable with given number to NUL terminated content. int VariableContainer_change(variable_container_t *object, int var_num, const char *value) { assert(var_num >= 0 && var_num < object->variable_num); if (value == NULL) value = ""; if (strcmp(value, object->values[var_num]) == 0) return 0; // no change. char *old_value = object->values[var_num]; char *new_value = strdup(value); object->values[var_num] = new_value; for (struct cb_list *it = object->callbacks; it; it = it->next) { it->callback(it->userdata, var_num, object->vars[var_num].name, old_value, new_value); } free(old_value); return 1; } void VariableContainer_register_callback(variable_container_t *object, variable_change_listener_t callback, void *userdata) { // Order is not guaranteed, so we just register it at the front. struct cb_list *item = (struct cb_list*) malloc(sizeof(struct cb_list)); item->next = object->callbacks; item->userdata = userdata; item->callback = callback; object->callbacks = item; } // -- UPnPLastChangeBuilder struct upnp_last_change_builder { const char *xml_namespace; struct xmldoc *change_event_doc; struct xmlelement *instance_element; }; upnp_last_change_builder_t *UPnPLastChangeBuilder_new(const char *xml_namespace) { upnp_last_change_builder_t *result = (upnp_last_change_builder_t*) malloc(sizeof(upnp_last_change_builder_t)); result->xml_namespace = xml_namespace; result->change_event_doc = NULL; result->instance_element = NULL; return result; } void UPnPLastChangeBuilder_delete(upnp_last_change_builder_t *builder) { if (builder->change_event_doc != NULL) { xmldoc_free(builder->change_event_doc); } free(builder); } void UPnPLastChangeBuilder_add(upnp_last_change_builder_t *builder, const char *name, const char *value) { assert(name != NULL); assert(value != NULL); if (builder->change_event_doc == NULL) { builder->change_event_doc = xmldoc_new(); struct xmlelement *toplevel = xmldoc_new_topelement(builder->change_event_doc, "Event", builder->xml_namespace); // Right now, we only have exactly one instance. builder->instance_element = add_attributevalue_element(builder->change_event_doc, toplevel, "InstanceID", "val", "0"); } struct xmlelement *xml_value; xml_value = add_attributevalue_element(builder->change_event_doc, builder->instance_element, name, "val", value); // HACK! // The volume related events need another qualifying // attribute that represents the channel. Since all other elements just // have one value to transmit without qualifier, the variable container // is oblivious about this notion of a qualifier. // So this is a bit ugly: if we see the variables in question, // we add the attribute manually. if (strcmp(name, "Volume") == 0 || strcmp(name, "VolumeDB") == 0 || strcmp(name, "Mute") == 0 || strcmp(name, "Loudness") == 0) { xmlelement_set_attribute(builder->change_event_doc, xml_value, "channel", "Master"); } } char *UPnPLastChangeBuilder_to_xml(upnp_last_change_builder_t *builder) { if (builder->change_event_doc == NULL) return NULL; char *xml_doc_string = xmldoc_tostring(builder->change_event_doc); xmldoc_free(builder->change_event_doc); builder->change_event_doc = NULL; builder->instance_element = NULL; return xml_doc_string; } // -- UPnPLastChangeCollector struct upnp_last_change_collector { variable_container_t *variable_container; int last_change_variable_num; // the variable we manipulate. uint32_t not_eventable_variables; // variables not to event on. struct upnp_device *upnp_device; const char *service_id; int open_transactions; upnp_last_change_builder_t *builder; }; static void UPnPLastChangeCollector_notify(upnp_last_change_collector_t *obj); static void UPnPLastChangeCollector_callback(void *userdata, int var_num, const char *var_name, const char *old_value, const char *new_value); upnp_last_change_collector_t * UPnPLastChangeCollector_new(variable_container_t *variable_container, const char *event_xml_namespace, struct upnp_device *upnp_device, const char *service_id) { upnp_last_change_collector_t *result = (upnp_last_change_collector_t*) malloc(sizeof(upnp_last_change_collector_t)); result->variable_container = variable_container; result->last_change_variable_num = -1; result->not_eventable_variables = 0; result->upnp_device = upnp_device; result->service_id = service_id; result->open_transactions = 0; result->builder = UPnPLastChangeBuilder_new(event_xml_namespace); // Create initial LastChange that contains all variables in their // current state. This might help devices that silently re-connect // without proper registration. // Also determine, which variable is actually the "LastChange" one. const int var_count = VariableContainer_get_num_vars(variable_container); assert(var_count < 32); // otherwise widen not_eventable_variables for (int i = 0; i < var_count; ++i) { const char *name; const char *value = VariableContainer_get(variable_container, i, &name); if (!value) { continue; } if (strcmp("LastChange", name) == 0) { result->last_change_variable_num = i; continue; } // Send over all variables except "LastChange" itself. UPnPLastChangeBuilder_add(result->builder, name, value); } assert(result->last_change_variable_num >= 0); // we expect to have one. // The state change variable itself is not eventable. UPnPLastChangeCollector_add_ignore(result, result->last_change_variable_num); UPnPLastChangeCollector_notify(result); VariableContainer_register_callback(variable_container, UPnPLastChangeCollector_callback, result); return result; } void UPnPLastChangeCollector_add_ignore(upnp_last_change_collector_t *object, int variable_num) { object->not_eventable_variables |= (1 << variable_num); } void UPnPLastChangeCollector_start(upnp_last_change_collector_t *object) { object->open_transactions += 1; } void UPnPLastChangeCollector_finish(upnp_last_change_collector_t *object) { assert(object->open_transactions >= 1); object->open_transactions -= 1; UPnPLastChangeCollector_notify(object); } // TODO(hzeller): add rate limiting. The standard talks about some limited // amount of events per time-unit. static void UPnPLastChangeCollector_notify(upnp_last_change_collector_t *obj) { if (obj->open_transactions != 0) return; char *xml_doc_string = UPnPLastChangeBuilder_to_xml(obj->builder); if (xml_doc_string == NULL) return; // Only if there is actually a change, send it over. if (VariableContainer_change(obj->variable_container, obj->last_change_variable_num, xml_doc_string)) { const char *varnames[] = { "LastChange", NULL }; const char *varvalues[] = { NULL, NULL }; // Yes, now, the whole XML document is encapsulated in // XML so needs to be XML quoted. The time around 2000 was // pretty sick - people did everything in XML. varvalues[0] = xmlescape(xml_doc_string, 0); upnp_device_notify(obj->upnp_device, obj->service_id, varnames, varvalues, 1); free((char*)varvalues[0]); } free(xml_doc_string); } // The actual callback collecting changes by building an XML document. // This is not very robust if in the same transaction, we get the same variable // changed twice -- it emits two changes. static void UPnPLastChangeCollector_callback(void *userdata, int var_num, const char *var_name, const char *old_value, const char *new_value) { (void)old_value; upnp_last_change_collector_t *object = (upnp_last_change_collector_t*) userdata; if (object->not_eventable_variables & (1 << var_num)) { return; // ignore changes on non-eventable variables. } UPnPLastChangeBuilder_add(object->builder, var_name, new_value); UPnPLastChangeCollector_notify(object); } gmrender-resurrect-0.0.9/src/variable-container.h000066400000000000000000000136311400533760300220510ustar00rootroot00000000000000/* * Copyright (C) 2013 Henner Zeller * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * * ----------------- * * Helpers for keeping track of server state variables. UPnP is about syncing * state between server and connected controllers and it does so by variables * (such as 'CurrentTrackDuration') that can be queried and whose changes * can be actively sent to parties that have registered for updates. * However, changes are not sent individually when a variable changes * but instead encapsulated in XML in a 'LastChange' variable, that contains * recent changes since the last update. * * These utility classes are here to help getting this done: * * variable_container - handling a bunch of variables containting NUL * terminated strings, allowing C-callbacks to be called when content changes * and differs from previous value. * * upnp_last_change_builder - a builder for the LastChange XML document * containing name/value pairs of variables. * * upnp_last_change_collector - handling of the LastChange variable in UPnP. * Hooks into the callback mechanism of the variable_container to assemble * the LastChange variable to be sent over (using the last change builder). * */ #ifndef VARIABLE_CONTAINER_H #define VARIABLE_CONTAINER_H // -- VariableContainer struct variable_container; typedef struct variable_container variable_container_t; struct var_meta; // Create a new variable container. The variable names mentioned // in the meta-data need to be valid for the lifetime of this object. variable_container_t *VariableContainer_new(int variable_num, const struct var_meta *var_array); void VariableContainer_delete(variable_container_t *object); // Get number of variables. int VariableContainer_get_num_vars(variable_container_t *object); // Get meta-data; returns count in return *count. // TODO(hzeller): this breaks abstraction, but this is to make sure to // simplify the transition. const struct var_meta *VariableContainer_get_meta(variable_container_t *object, int *count); // Get variable name/value. if OUT parameter 'name' is not NULL, returns // name of variable for given number. // Returns current value of variable or NULL if it does not exist. // Returned value owned by variable container; on variable change, this value // will be invalid. const char *VariableContainer_get(variable_container_t *object, int var, const char **name); // Change content of variable with given number to NUL terminated content. // Returns '1' if value actually changed and all callbacks were called, // '0' if no change was detected. int VariableContainer_change(variable_container_t *object, int variable_num, const char *value); // Callback handling. Whenever a variable changes, the callback is called. // Be careful when changing variables in the original container as this will // trigger recursive calls to the container. typedef void (*variable_change_listener_t)(void *userdata, int var_num, const char *var_name, const char *old_value, const char *new_value); void VariableContainer_register_callback(variable_container_t *object, variable_change_listener_t callback, void *userdata); // -- UPnP LastChange Builder - builds a LastChange XML document from // added name/value pairs. struct upnp_last_change_builder; typedef struct upnp_last_change_builder upnp_last_change_builder_t; // Create a new last change builder. The pointer to the xml_namespace string // must exist for the livetime of this object. upnp_last_change_builder_t *UPnPLastChangeBuilder_new(const char *xml_namespace); void UPnPLastChangeBuilder_delete(upnp_last_change_builder_t *builder); void UPnPLastChangeBuilder_add(upnp_last_change_builder_t *builder, const char *name, const char *value); // Returns a newly allocated XML string that needs to be free()'d by the caller. // Resets the document. If no changes have been added, NULL is returned. char *UPnPLastChangeBuilder_to_xml(upnp_last_change_builder_t *builder); // -- UPnP LastChange collector struct upnp_device; // forward declare. struct upnp_last_change_collector; typedef struct upnp_last_change_collector upnp_last_change_collector_t; // Create a new last change collector that registers at the // "variable_container" for changes in variables. It assembles a LastChange // event and sends it to the given "upnp_device". // The variable_container is expected to contain one variable with name // "LastChange", otherwise this collector is not applicable and fails. upnp_last_change_collector_t * UPnPLastChangeCollector_new(variable_container_t *variable_container, const char *event_xml_namespac, struct upnp_device *upnp_device, const char *service_id); // Set variable number that should be ignored in eventing. void UPnPLastChangeCollector_add_ignore(upnp_last_change_collector_t *object, int variable_num); // If we know that there are a couple of changes upcoming, we can // 'start' a transaction and tell the collector to keep collecting until we // 'finish'. This can be nested. void UPnPLastChangeCollector_start(upnp_last_change_collector_t *object); void UPnPLastChangeCollector_finish(upnp_last_change_collector_t *object); // no delete yet. We leak that. #endif /* VARIABLE_CONTAINER_H */ gmrender-resurrect-0.0.9/src/webserver.c000066400000000000000000000167761400533760300203200ustar00rootroot00000000000000/* webserver.c - Web server callback routines * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include // UpnpGetErrorMessage #include #include "logging.h" #include "webserver.h" #include "upnp_compat.h" typedef struct { off_t pos; const char *contents; size_t len; } WebServerFile; struct virtual_file; static struct virtual_file { const char *virtual_fname; const char *contents; const char *content_type; size_t len; struct virtual_file *next; } *virtual_files = NULL; int webserver_register_buf(const char *path, const char *contents, const char *content_type) { struct virtual_file *entry; Log_info("webserver", "Provide %s (%s) from buffer", path, content_type); assert(path != NULL); assert(contents != NULL); assert(content_type != NULL); entry = (struct virtual_file*)malloc(sizeof(struct virtual_file)); if (entry == NULL) { return -1; } entry->len = strlen(contents); entry->contents = contents; entry->virtual_fname = path; entry->content_type = content_type; entry->next = virtual_files; virtual_files = entry; return 0; } int webserver_register_file(const char *path, const char *content_type) { char local_fname[512]; // PATH_MAX, but that is not defined everywhere struct stat buf; struct virtual_file *entry; int rc; snprintf(local_fname, sizeof(local_fname), "%s%s", PKG_DATADIR, strrchr(path, '/')); Log_info("webserver", "Provide %s (%s) from %s", path, content_type, local_fname); rc = stat(local_fname, &buf); if (rc) { Log_error("webserver", "Could not stat '%s': %s", local_fname, strerror(errno)); return -1; } entry = (struct virtual_file*)malloc(sizeof(struct virtual_file)); if (entry == NULL) { return -1; } if (buf.st_size) { char *cbuf; FILE *in; in = fopen(local_fname, "r"); if (in == NULL) { free(entry); return -1; } cbuf = (char*)malloc(buf.st_size); if (cbuf == NULL) { free(entry); return -1; } if (fread(cbuf, buf.st_size, 1, in) != 1) { free(entry); free(cbuf); return -1; } fclose(in); entry->len = buf.st_size; entry->contents = cbuf; } else { entry->len = 0; entry->contents = NULL; } entry->virtual_fname = path; entry->content_type = content_type; entry->next = virtual_files; virtual_files = entry; return 0; } static VD_GET_INFO_CALLBACK(webserver_get_info, filename, info, cookie) { struct virtual_file *virtfile = virtual_files; while (virtfile != NULL) { if (strcmp(filename, virtfile->virtual_fname) == 0) { UpnpFileInfo_set_FileLength(info, virtfile->len); UpnpFileInfo_set_LastModified(info, 0); UpnpFileInfo_set_IsDirectory(info, 0); UpnpFileInfo_set_IsReadable(info, 1); const char *contentType = ixmlCloneDOMString(virtfile->content_type); UpnpFileInfo_set_ContentType(info, (char*) contentType); Log_info("webserver", "Access %s (%s) len=%zd", filename, contentType, virtfile->len); return 0; } virtfile = virtfile->next; } Log_info("webserver", "404 Not found. (attempt to access " "non-existent '%s')", filename); return -1; } static VD_OPEN_CALLBACK(webserver_open, filename, mode, cookie) { if (mode != UPNP_READ) { Log_error("webserver", "%s: ignoring request to open file for writing.", filename); return NULL; } for (struct virtual_file *vf = virtual_files; vf; vf = vf->next) { if (strcmp(filename, vf->virtual_fname) == 0) { WebServerFile *file = (WebServerFile*)malloc(sizeof(WebServerFile)); file->pos = 0; file->len = vf->len; file->contents = vf->contents; return file; } } return NULL; } static inline int minimum(int a, int b) { return (alen - file->pos); memcpy(buf, file->contents + file->pos, len); if (len < 0) { Log_error("webserver", "In %s: %s", __FUNCTION__, strerror(errno)); } else { file->pos += len; } return len; } static VD_WRITE_CALLBACK(webserver_write, fh, buf, buflen, cookie) { return -1; } static VD_SEEK_CALLBACK(webserver_seek, fh, offset, origin, cookie) { WebServerFile *file = (WebServerFile *) fh; off_t newpos = -1; switch (origin) { case SEEK_SET: newpos = offset; break; case SEEK_CUR: newpos = file->pos + offset; break; case SEEK_END: newpos = file->len + offset; break; } if (newpos < 0 || newpos > (off_t) file->len) { Log_error("webserver", "in %s: seek failed with %s", __FUNCTION__, strerror(errno)); return -1; } file->pos = newpos; return 0; } static VD_CLOSE_CALLBACK(webserver_close, fh, cookie) { WebServerFile *file = (WebServerFile *) fh; free(file); return 0; } #if (UPNP_VERSION < 10607) // Older versions had a nice struct to register callbacks, just as you would // expect from a proper C API static struct UpnpVirtualDirCallbacks virtual_dir_callbacks = { webserver_get_info, webserver_open, webserver_read, webserver_write, webserver_seek, webserver_close }; gboolean webserver_register_callbacks(void) { int rc = UpnpSetVirtualDirCallbacks(&virtual_dir_callbacks); if (UPNP_E_SUCCESS != rc) { Log_error("webserver", "UpnpSetVirtualDirCallbacks() Error: %s (%d)", UpnpGetErrorMessage(rc), rc); return FALSE; } return TRUE; } #else // With version 1.6.7 and above, the UPNP library maintainers made a questionable // API choice to register the callbacks one-by-one, instead of having a // struct with the callbacks which is certainly a better and 'typical' C API // choice. They removed the UpnpVirtualDirCallbacks in the course of that. // Because it breaks code, it has been reverted in version 1.6.16 // (see http://sourceforge.net/p/pupnp/bugs/29/ ), but we essentially have a // broken version between 1.6.7 ... 1.6.16. // Assuming that they will go on with this broken idea and eventually remove // the support for the VirtualDirCallbacks in new major versions, we use the // newer (may I emphasize: questionable) API to register the callbacks. gboolean webserver_register_callbacks(void) { gboolean result = (UpnpVirtualDir_set_GetInfoCallback(webserver_get_info) == UPNP_E_SUCCESS && UpnpVirtualDir_set_OpenCallback(webserver_open) == UPNP_E_SUCCESS && UpnpVirtualDir_set_ReadCallback(webserver_read) == UPNP_E_SUCCESS && UpnpVirtualDir_set_WriteCallback(webserver_write) == UPNP_E_SUCCESS && UpnpVirtualDir_set_SeekCallback(webserver_seek) == UPNP_E_SUCCESS && UpnpVirtualDir_set_CloseCallback(webserver_close) == UPNP_E_SUCCESS); return result; } #endif gmrender-resurrect-0.0.9/src/webserver.h000066400000000000000000000024041400533760300203040ustar00rootroot00000000000000/* webserver.h - Web server callback definitions * * Copyright (C) 2005-2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _WEBSERVER_H #define _WEBSERVER_H #include // Start the webserver with the registered files. gboolean webserver_register_callbacks(void); int webserver_register_buf(const char *path, const char *contents, const char *content_type); int webserver_register_file(const char *path, const char *content_type); #endif /* _WEBSERVER_H */ gmrender-resurrect-0.0.9/src/xmldoc.c000066400000000000000000000135611400533760300175670ustar00rootroot00000000000000/* xmldoc.h - XML builder abstraction * * Copyright (C) 2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include "xmldoc.h" // The structs are mere placeholders that we internally cast to the // objects used in the upnplib. struct xmlelement {}; struct xmldoc {}; static IXML_Document *to_idoc(struct xmldoc *x) { return (IXML_Document*) x; } static IXML_Element *to_ielem(struct xmlelement *x) { return (IXML_Element*) x; } struct xmldoc *xmldoc_new(void) { IXML_Document *doc; doc = ixmlDocument_createDocument(); return (struct xmldoc*) doc; } void xmldoc_free(struct xmldoc *doc) { assert(doc != NULL); ixmlDocument_free(to_idoc(doc)); } char *xmldoc_tostring(struct xmldoc *doc) { char *result = NULL; assert(doc != NULL); result = ixmlDocumenttoString(to_idoc(doc)); return result; } struct xmldoc *xmldoc_parsexml(const char *xml_text) { IXML_Document *doc = ixmlParseBuffer(xml_text); return (struct xmldoc*) doc; } struct xmlelement *xmldoc_new_topelement(struct xmldoc *doc, const char *elementName, const char *xmlns) { assert(doc != NULL); assert(elementName != NULL); IXML_Element *element; if (xmlns) { element = ixmlDocument_createElementNS(to_idoc(doc), xmlns, elementName); ixmlElement_setAttribute(element, "xmlns", xmlns); } else { element = ixmlDocument_createElement(to_idoc(doc), elementName); } ixmlNode_appendChild((IXML_Node *)(to_idoc(doc)),(IXML_Node *)element); return (struct xmlelement *) element; } struct xmlelement *xmlelement_new(struct xmldoc *doc, const char *elementName) { assert(doc != NULL); assert(elementName != NULL); IXML_Element *element; element = ixmlDocument_createElement(to_idoc(doc), elementName); return (struct xmlelement*) element; } static struct xmlelement *find_element(IXML_Node *node, const char *key) { node = ixmlNode_getFirstChild(node); for (/**/; node != NULL; node = ixmlNode_getNextSibling(node)) { if (strcmp(ixmlNode_getNodeName(node), key) == 0) { return (struct xmlelement*) node; } } return NULL; } struct xmlelement *find_element_in_doc(struct xmldoc *doc, const char *key) { return find_element((IXML_Node*) to_idoc(doc), key); } struct xmlelement *find_element_in_element(struct xmlelement *element, const char *key) { return find_element((IXML_Node*) to_ielem(element), key); } char *get_node_value(struct xmlelement *element) { IXML_Node *node = (IXML_Node*) to_ielem(element); node = ixmlNode_getFirstChild(node); const char *node_value = (node != NULL ? ixmlNode_getNodeValue(node) : NULL); return strdup(node_value != NULL ? node_value : ""); } void xmlelement_add_element(struct xmldoc *doc, struct xmlelement *parent, struct xmlelement *child) { assert(doc != NULL); assert(parent != NULL); assert(child != NULL); ixmlNode_appendChild((IXML_Node *) to_ielem(parent), (IXML_Node *) to_ielem(child)); } void xmlelement_add_text(struct xmldoc *doc, struct xmlelement *parent, const char *text) { assert(doc != NULL); assert(parent != NULL); assert(text != NULL); IXML_Node *textNode; textNode = ixmlDocument_createTextNode(to_idoc(doc), text); ixmlNode_appendChild((IXML_Node *) to_ielem(parent), textNode); } void xmlelement_set_attribute(struct xmldoc *doc, struct xmlelement *element, const char *name, const char *value) { assert(doc != NULL); assert(element != NULL); assert(name != NULL); assert(value != NULL); ixmlElement_setAttribute(to_ielem(element), name, value); } void add_value_element(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, const char *value) { struct xmlelement *top; top=xmlelement_new(doc, tagname); xmlelement_add_text(doc, top, value); xmlelement_add_element(doc, parent, top); } struct xmlelement *add_attributevalue_element(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, const char *attribute_name, const char *value) { struct xmlelement *top; top = xmlelement_new(doc, tagname); xmlelement_set_attribute(doc, top, attribute_name, value); xmlelement_add_element(doc, parent, top); return top; } void add_value_element_int(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, int value) { char *buf; if (asprintf(&buf,"%d",value) >= 0) { add_value_element(doc, parent, tagname, buf); free(buf); } } void add_value_element_long(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, long long value) { char *buf; if (asprintf(&buf,"%lld",value) >= 0) { add_value_element(doc, parent, tagname, buf); free(buf); } } gmrender-resurrect-0.0.9/src/xmldoc.h000066400000000000000000000055171400533760300175760ustar00rootroot00000000000000/* xmldoc.h - XML builder abstraction * * Copyright (C) 2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _XMLDOC_H #define _XMLDOC_H struct xmldoc; struct xmlelement; struct xmldoc *xmldoc_new(void); void xmldoc_free(struct xmldoc *doc); char *xmldoc_tostring(struct xmldoc *doc); struct xmldoc *xmldoc_parsexml(const char *xml_text); struct xmlelement *xmldoc_new_topelement(struct xmldoc *doc, const char *elementName, const char *xmlns); struct xmlelement *xmlelement_new(struct xmldoc *doc, const char *elementName); void xmlelement_add_element(struct xmldoc *doc, struct xmlelement *parent, struct xmlelement *child); void xmlelement_add_text(struct xmldoc *doc, struct xmlelement *parent, const char *text); void xmlelement_set_attribute(struct xmldoc *doc, struct xmlelement *element, const char *name, const char *value); void add_value_element(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, const char *value); // Find element in document. This returns a newly allocated struct. struct xmlelement *find_element_in_doc(struct xmldoc *doc, const char *key); // Find element in document. This returns a newly allocated struct. struct xmlelement *find_element_in_element(struct xmlelement *element, const char *key); // Returns a newly allocated string representing the element value. char *get_node_value(struct xmlelement *element); struct xmlelement *add_attributevalue_element(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, const char *attribute_name, const char *value); void add_value_element_int(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, int value); void add_value_element_long(struct xmldoc *doc, struct xmlelement *parent, const char *tagname, long long value); #endif /* _XMLDOC_H */ gmrender-resurrect-0.0.9/src/xmlescape.c000066400000000000000000000041261400533760300202570ustar00rootroot00000000000000/* xmlescape.c - helper routines for escaping XML strings * * Copyright (C) 2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include "xmlescape.h" static void xmlescape_real(const char *str, char *target, int *length, int attribute) { if (target != NULL) { int len = 0; for (/**/; *str; str++) { if (*str == '<') { memcpy(target + len, "<", 4); len += 4; } else if (attribute && (*str == '"')) { memcpy(target + len, "%22", 3); len += 3; } else if (*str == '>') { memcpy(target + len, ">", 4); len += 4; } else if (*str == '&') { memcpy(target + len, "&", 5); len += 5; } else { target[len++] = *str; } } target[len] = '\0'; if (length != NULL) *length = len; } else if (length != NULL) { int len = 0; for (/**/; *str; str++) { if (*str == '<') { len += 4; } else if (attribute && (*str == '"')) { len += 3; } else if (*str == '>') { len += 4; } else if (*str == '&') { len += 5; } else { len++; } } *length = len; } } char *xmlescape(const char *str, int attribute) { int len; char *out; xmlescape_real(str, NULL, &len, attribute); out = (char*)malloc(len + 1); xmlescape_real(str, out, NULL, attribute); return out; } gmrender-resurrect-0.0.9/src/xmlescape.h000066400000000000000000000022551400533760300202650ustar00rootroot00000000000000/* xmlescape.h - helper routines for escaping XML strings * * Copyright (C) 2007 Ivo Clarysse * * This file is part of GMediaRender. * * GMediaRender 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. * * GMediaRender 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GMediaRender; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ #ifndef _XMLESCAPE_H #define _XMLESCAPE_H // XML escape string "str". If "attribute" is 1, then this is considered // to be within an xml attribute (i.e. quotes are escaped as well). // Returns a malloc()ed string; caller needs to free(). char *xmlescape(const char *str, int attribute); #endif /* _XMLESCAPE_H */