gonzopi

git clone https://git.tarina.org/gonzopi
Log | Files | Refs | README | LICENSE

commit 13f3d8041c688c7cba8cdad87bb3820313684e8d
Author: rob <rob@tarina.org>
Date:   Mon, 20 Nov 2023 11:17:55 +0200

first

Diffstat:
ADEVELOPERS.md | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AINSTALL.md | 10++++++++++
ALICENSE | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 48++++++++++++++++++++++++++++++++++++++++++++++++
AVERSION | 2++
Aalsa-utils-1.1.3/aplay/aplay | 0
Aalsa-utils-1.1.3/aplay/arecord | 0
Acontributors.txt | 2++
Aextras/.vimrc | 34++++++++++++++++++++++++++++++++++
Aextras/bakg.jpg | 0
Aextras/bakg.xcf | 0
Aextras/beep.wav | 0
Aextras/beep_long.wav | 0
Aextras/buttons.png | 0
Aextras/buttons.xcf | 0
Aextras/debiancheck.sh | 10++++++++++
Aextras/h264streamer.py | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aextras/restorebak.sh | 25+++++++++++++++++++++++++
Aextras/sdcardhack.sh | 6++++++
Aextras/tarinaretake.conf | 34++++++++++++++++++++++++++++++++++
Aextras/wifiset.service | 10++++++++++
Aextras/wifiset.sh | 3+++
Aextras/youtubelive.sh | 1+
Aextras/ytstream.py | 25+++++++++++++++++++++++++
Agonzopi.py | 4846++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/Makefile.include | 28++++++++++++++++++++++++++++
Agui/VeraMono.ttf | 0
Agui/libs/ilclient/Makefile | 5+++++
Agui/libs/ilclient/ilclient.c | 1836+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/ilclient/ilclient.h | 1039+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/ilclient/ilcore.c | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/Makefile | 7+++++++
Agui/libs/vgfont/font.c | 355+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/graphics.c | 1608+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/graphics_x_private.h | 366+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/libvgfont.a | 0
Agui/libs/vgfont/vgfont.h | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/vgft.c | 416+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/libs/vgfont/vgft.h | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/src/Makefile | 7+++++++
Agui/src/VeraMono.ttf | 0
Agui/src/fixedsys.ttf | 0
Agui/src/main.c | 317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/src/main_new.c | 42++++++++++++++++++++++++++++++++++++++++++
Agui/tarinagui.bin | 0
Ainstall.sh | 392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alenses/cs6mm | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/collab-edit.sh | 7+++++++
Amods/collab-pull.sh | 7+++++++
Amods/collab-push.sh | 7+++++++
Amods/install-youtube-upload.sh | 20++++++++++++++++++++
Amods/tarina-upload.sh | 7+++++++
Amods/upload-mods-enabled | 5+++++
Amods/youtube-upload.sh | 44++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md | 23+++++++++++++++++++++++
Amods/youtube-upload/.gitignore | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/Dockerfile | 14++++++++++++++
Amods/youtube-upload/README.md | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/bin/youtube-upload | 10++++++++++
Amods/youtube-upload/bin/youtube-upload.bat | 1+
Amods/youtube-upload/examples/split_video_for_youtube.sh | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/setup.py | 37+++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/__init__.py | 1+
Amods/youtube-upload/youtube_upload/__main__.py | 19+++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/__init__.py | 42++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/browser.py | 19+++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/console.py | 23+++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/webkit_gtk.py | 48++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/webkit_qt.py | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/categories.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/lib.py | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/main.py | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/playlists.py | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/upload_video.py | 43+++++++++++++++++++++++++++++++++++++++++++
Asrv/static/Videos | 2++
Asrv/static/jquery-3.7.1.min.js | 2++
Asrv/static/style.css | 22++++++++++++++++++++++
Asrv/tarinaserver.py | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrv/templates/base.html | 14++++++++++++++
Asrv/templates/filmpage.html | 39+++++++++++++++++++++++++++++++++++++++
Asrv/templates/index.html | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astartinterface.sh | 5+++++
82 files changed, 14087 insertions(+), 0 deletions(-)

diff --git a/DEVELOPERS.md b/DEVELOPERS.md @@ -0,0 +1,90 @@ +DEVELOPERS +========== + +If you want to join me in this journey to make the best open filmmaking camera please do not hesitate to contact me on features. + +Teh best way to develop is using screen in terminal and starting tarina in one window then you'll get debug messages directly in that window. + +THINGS +====== + +Notes to myself as I'm the only developer at teh moment. +-Rob + +Quality / Usability +------------------- + +Found a sweet spot between usability and quality (23), bitrate (5 mb/s) +This produces about 25 mb per minute. The convertion to mp4 format does not feel so slow anymore likewise the compiling of the film. This is good stuff. + +Image sensors +------------- + +There's plenty of image sensors for the Raspberry Pi to choose from, I want to support as many sensors I can. Currently though you need to change your sensor in tarina.py, search for 'ov5' and you'll find where to select. They all have different oscillators so the framerate varies pretty much. But when you get it right the sound will be in sync. Will make a tarina config file in home folder so when you start tarina interface for the first time it will ask you about your sensor. + +Cancel renderer +--------------- + +This is a must feature, and I sort of know how to implement it. + +Viewfinder for 3.5 inch screen +------------------------------ + +There's no such thing to buy so I had to make a viewfinder myself, took a while before I found the right convex lens but I have it now. It works with magnet snap on. Really, really usefull! will write it in the manual as a addon thing... + +How to update alsa-tools arecord +================================= + +The vumeter comes directly from alsa-tools arecord, it's just modified so it writes the vumeter directly to /dev/shm/vumeter. If you need to update alsa-tools here's how: + +You need to be able to build alsa-utils from source +enable dev-src in /etc/apt/sources.list + +``` +apt source alsa-utils +cd alsa-utils* +sudo apt update +sudo apt install libncurses5-dev +sudo apt install libasound2-dev +./configure +make +cd aplay +make +make arecord +``` + + +Ecasound update +=============== + +### Python example + +``` +from pyeca import * +e = ECA_CONTROL_INTERFACE() +e.command("cs-add chainsetup") +e.command("c-add 1st_chain") +e.command("ai-add plughw,0,0 ") +e.command("ao-add /dev/dsp") +e.command("cop-add -efl:100") +e.command("cop-select 1") +e.command("copp-select 1") +e.command("cs-connect") +e.command("start") +``` + +### Ecasound config file + +``` +# ecasound chainsetup file + +# general +-B:rtlowlatency -n:"write_chain" -X -z:noxruns -z:nopsr -z:mixmode,sum + +# audio inputs +-a:1chain,2chain -f:s16_le,2,44100 -i:alsahw,3,0,0 + +# audio outputs +-a:1chain -f:s16_le,2,44100 -o:alsahw,3,0,0 +-a:2chain -f:s16_le,2,44100 -o:test.wav, +``` diff --git a/INSTALL.md b/INSTALL.md @@ -0,0 +1,10 @@ +## Tarina install instructions ## + +Run the automatic install script with sudo: +``` +sudo ./install.sh +``` +you are done! now reboot & Tarina will start at bootup: +``` +sudo reboot +``` diff --git a/LICENSE b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md @@ -0,0 +1,48 @@ +Gonzo Pi +============= + +Go Gonzo + +Software +-------- +- glue selected clips together and/or cutting them. +- making timelapses, voiceover, music track recording, slo-mo recording, fast-forward recording +- cut and copy and move clips around +- backup to usb harddrive or your own server +- upload or stream to youtube or your own server +- auto correction can easily be switched on or off for shutter, iso and colors so *operator* is in full control also for audio levels +- connect many Tarinas together for multicamera shooting +- stream a film a take or a scene through the network +- control the camera with silent physical buttons or a usb-wireless-keyboard or through https or ssh or ports, you choose. + +### Buttons +![Buttons](/extras/buttons.png) + +Installing +---------- +Download [Raspbian buster (not the latest!)](https://www.raspberrypi.org/downloads/raspbian/) and follow [install instructions | a simple install script should take care of it all!](https://www.raspberrypi.org/documentation/installation/installing-images/README.md). +[Ssh into](https://www.raspberrypi.org/documentation/remote-access/ssh/) Raspberry Pi and run: +``` +sudo apt-get install git +``` +Go to /home/pi/ folder +``` +cd /home/pi +``` +Git clone Gonzo Pi and then run install script with sudo: +``` +git clone https://github.com/rbckman/gonzopi.git +cd gonzopi +sudo ./install.sh +``` +You'r ready to go gonzo! +``` +python3 gonzopi.py +``` + +Connect +------- +Matrix [#tarina:matrix.tarina.org](https://riot.im/app/#/room/#tarina:matrix.tarina.org) + +Mail rob(at)tarina.org + diff --git a/VERSION b/VERSION @@ -0,0 +1,2 @@ +1.43 +Jackson diff --git a/alsa-utils-1.1.3/aplay/aplay b/alsa-utils-1.1.3/aplay/aplay Binary files differ. diff --git a/alsa-utils-1.1.3/aplay/arecord b/alsa-utils-1.1.3/aplay/arecord Binary files differ. diff --git a/contributors.txt b/contributors.txt @@ -0,0 +1,2 @@ +Robin Bäckman +Robin Bäckman diff --git a/extras/.vimrc b/extras/.vimrc @@ -0,0 +1,34 @@ +" File ~/.vim/ftplugin/html.vim + +set cursorline +set encoding=utf-8 +set fileencoding=utf-8 +set mouse=a +set showcmd +syntax enable +filetype indent on +set nobackup +set number +set ruler +set undolevels=1000 +set backspace=indent,eol,start +set tabstop=8 +set expandtab +set softtabstop=4 +set shiftwidth=4 +set nobackup +set nowb +set noswapfile +set linebreak +colorscheme desert +set background=dark +nnoremap j gj +nnoremap k gk +vnoremap j gj +vnoremap k gk +nnoremap <Down> gj +nnoremap <Up> gk +vnoremap <Down> gj +vnoremap <Up> gk +inoremap <Down> <C-o>gj +inoremap <Up> <C-o>gk diff --git a/extras/bakg.jpg b/extras/bakg.jpg Binary files differ. diff --git a/extras/bakg.xcf b/extras/bakg.xcf Binary files differ. diff --git a/extras/beep.wav b/extras/beep.wav Binary files differ. diff --git a/extras/beep_long.wav b/extras/beep_long.wav Binary files differ. diff --git a/extras/buttons.png b/extras/buttons.png Binary files differ. diff --git a/extras/buttons.xcf b/extras/buttons.xcf Binary files differ. diff --git a/extras/debiancheck.sh b/extras/debiancheck.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Basic if statement +version="$(lsb_release -c -s)" +echo $version +if [ "$version" = "buster" ] +then + echo "Debian Buster found" +else + echo "Debian Stretch found" +fi diff --git a/extras/h264streamer.py b/extras/h264streamer.py @@ -0,0 +1,64 @@ +import io +import picamerax as picamera +import time +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from wsgiref.simple_server import make_server +from ws4py.websocket import WebSocket +from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIHandler, WebSocketWSGIRequestHandler +from ws4py.server.wsgiutils import WebSocketWSGIApplication +from threading import Thread, Condition + + +class FrameBuffer(object): + def __init__(self): + self.frame = None + self.buffer = io.BytesIO() + self.condition = Condition() + + def write(self, buf): + if buf.startswith(b'\x00\x00\x00\x01'): + with self.condition: + self.buffer.seek(0) + self.buffer.write(buf) + self.buffer.truncate() + self.frame = self.buffer.getvalue() + self.condition.notify_all() + + +def stream(): + with picamera.PiCamera(resolution='1920x816', framerate=25) as camera: + broadcasting = True + frame_buffer = FrameBuffer() + camera.start_recording(frame_buffer, format='h264', profile="baseline") + try: + WebSocketWSGIHandler.http_version = '1.1' + websocketd = make_server('', 9000, server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=WebSocket)) + websocketd.initialize_websockets_manager() + websocketd_thread = Thread(target=websocketd.serve_forever) + + httpd = ThreadingHTTPServer(('', 8000), SimpleHTTPRequestHandler) + httpd_thread = Thread(target=httpd.serve_forever) + + try: + websocketd_thread.start() + httpd_thread.start() + while broadcasting: + with frame_buffer.condition: + frame_buffer.condition.wait() + websocketd.manager.broadcast(frame_buffer.frame, binary=True) + except KeyboardInterrupt: + pass + finally: + websocketd.shutdown() + httpd.shutdown() + broadcasting = False + raise KeyboardInterrupt + except KeyboardInterrupt: + pass + finally: + camera.stop_recording() + +if __name__ == "__main__": + stream() diff --git a/extras/restorebak.sh b/extras/restorebak.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +ROOT_UID=0 # Root has $UID 0. + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + echo "sudo ./restorebak.sh" + exit 0 +fi + +while true; do + read -p "Undo rpi-update? [y]es or [n]o?" yn + case $yn in + [Yy]* ) echo "Restoring from backup now..." +cp -r /boot.bak/* /boot/ +cp -r /lib/modules.bak/* /lib/modules/ + break;; + [Nn]* ) echo "Yes, sir! we are done!";break;; + * ) echo "Please answer yes or no.";; + esac +done + diff --git a/extras/sdcardhack.sh b/extras/sdcardhack.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo 3 >/proc/sys/vm/dirty_background_ratio +echo 50 >/proc/sys/vm/dirty_ratio +echo 300 >/proc/sys/vm/dirty_writeback_centisecs +echo 300 >/proc/sys/vm/dirty_expire_centisecs diff --git a/extras/tarinaretake.conf b/extras/tarinaretake.conf @@ -0,0 +1,34 @@ +<VirtualHost *:80> + ServerAdmin info@tarina.org + WSGIScriptAlias / /home/pi/tarinaretake/srv/tarinaserver.py + WSGIPassAuthorization On + AddType text/html .py + Alias /static /home/pi/tarinaretake/srv/static + <Directory /> + Options FollowSymLinks + AllowOverride None + </Directory> + <Directory /home/pi/srv> + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + </Directory> + + ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + <Directory "/usr/lib/cgi-bin"> + AllowOverride None + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + </Directory> + + ErrorLog ${APACHE_LOG_DIR}/error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/access.log combined + +</VirtualHost> diff --git a/extras/wifiset.service b/extras/wifiset.service @@ -0,0 +1,10 @@ +[Unit] +Description=Set wifi localization +After=multi-user.target + +[Service] +Type=idle +ExecStart=/home/pi/tarina/extras/wifiset.sh + +[Install] +WantedBy=multi-user.target diff --git a/extras/wifiset.sh b/extras/wifiset.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo 'Setting your wifi region' +sudo iw reg set FI diff --git a/extras/youtubelive.sh b/extras/youtubelive.sh @@ -0,0 +1 @@ +raspivid -o - -t 0 -w 1920 -h 816 -vf -hf -fps 25 -b 3000000 | ffmpeg -re -ar 44100 -acodec pcm_s16le -f alsa -ac 1 -i hw:0 -f h264 -i - -vcodec copy -acodec aac -ab 128k -g 50 -strict experimental -f flv rtmp://a.rtmp.youtube.com/live2/j1cs-7hac-ygrj-5gaq-3m5b diff --git a/extras/ytstream.py b/extras/ytstream.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import subprocess +import picamerax +import time +YOUTUBE="rtmp://a.rtmp.youtube.com/live2/" +KEY='j1cs-7hac-ygrj-5gaq-3m5b' +stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac 1 -i hw:0 -vcodec copy -acodec libmp3lame -ar 44100 -map 0:0 -map 1:0 -strict experimental -f flv ' + YOUTUBE + KEY +stream = subprocess.Popen(stream_cmd, shell=True, stdin=subprocess.PIPE) +camera = picamerax.PiCamera(resolution=(640, 480), framerate=25) +try: + now = time.strftime("%Y-%m-%d-%H:%M:%S") + camera.framerate = 25 + camera.vflip = True + camera.hflip = True + camera.start_recording(stream.stdin, format='h264', bitrate = 2000000) + while True: + camera.wait_recording(1) +except KeyboardInterrupt: + camera.stop_recording() +finally: + camera.close() + stream.stdin.close() + stream.wait() + print("Camera safely shut down") + print("Good bye") diff --git a/gonzopi.py b/gonzopi.py @@ -0,0 +1,4846 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ +# █ █ █ █ █ █ █ █ █ █ +# █ ▄▄▄▄█ ▄ █ █▄█ █▄▄▄▄ █ ▄ █ ▄ █ █ +# █ █ ▄▄█ █ █ █ █▄▄▄▄█ █ █ █ █ █▄█ █ █ +# █ █ █ █ █▄█ █ ▄ █ ▄▄▄▄▄▄█ █▄█ █ ▄▄▄█ █ +# █ █▄▄█ █ █ █ █ █ █▄▄▄▄▄█ █ █ █ █ +# █▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄█ █▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█ + +# https://gonzopi.tarina.org + +import picamerax as picamera +import numpy as np +import string +import os +import time +import datetime +import multiprocessing as mp +from subprocess import call +from subprocess import Popen +from omxplayer import OMXPlayer +from multiprocessing import Process, Queue +import subprocess +import sys +import pickle +import RPi.GPIO as GPIO +from PIL import Image +import socket +import configparser +import shortuuid +import smbus +import ifaddr +import web + +#import shlex +from blessed import Terminal + +# bless the code! +term = Terminal() + +#DEBIAN VERSION +pipe = subprocess.check_output('lsb_release -c -s', shell=True) +debianversion = pipe.decode().strip() +print('running debian ' + debianversion) + +#I2CBUTTONS +probei2c = 0 +while probei2c < 3: + try: + if debianversion == "stretch": + os.system('sudo modprobe i2c-dev') + bus = smbus.SMBus(3) # Rev 2 Pi uses 1 + else: + os.system('sudo modprobe i2c-dev') + bus = smbus.SMBus(11) # Rev 2 Pi uses 1 + DEVICE = 0x20 # Device address (A0-A2) + IODIRB = 0x0d # Pin pullups B-side + IODIRA = 0x00 # Pin pullups A-side 0x0c + IODIRApullup = 0x0c # Pin pullups A-side 0x0c + GPIOB = 0x13 # Register B-side for inputs + GPIOA = 0x12 # Register A-side for inputs + OLATA = 0x14 # Register for outputs + bus.write_byte_data(DEVICE,IODIRB,0xFF) # set all gpiob to input + bus.write_byte_data(DEVICE,IODIRApullup,0xF3) # set two pullup inputs and two outputs + bus.write_byte_data(DEVICE,IODIRA,0xF3) # set two inputs and two outputs + bus.write_byte_data(DEVICE,OLATA,0x4) + print("yes, found em i2c buttons!") + i2cbuttons = True + break + except: + print("could not find i2c buttons!! running in keyboard only mode") + print("trying again...") + i2cbuttons = False + probei2c += 1 + time.sleep(1) + bus='' + +#MAIN +def main(): + global headphoneslevel, miclevel, gonzopifolder, screen, loadfilmsettings, plughw, channels, filmfolder, scene, showmenu, rendermenu, quality, profilelevel, i2cbuttons, menudone, soundrate, soundformat, process, serverstate, que, port, recording, onlysound, camera_model, fps_selection, fps_selected, fps, db, selected, cammode, newfilmname, camera_recording, abc + # Get path of the current dir, then use it as working directory: + rundir = os.path.dirname(__file__) + if rundir != '': + os.chdir(rundir) + #filmfolder = "/home/pi/Videos/" + #picfolder = "/home/pi/Pictures/" + gonzopifolder = os.getcwd() + + #MENUS + standardmenu = 'FILM:', 'SCENE:', 'SHOT:', 'TAKE:', '', 'SHUTTER:', 'ISO:', 'RED:', 'BLUE:', 'FPS:', 'Q:', 'BRIGHT:', 'CONT:', 'SAT:', 'FLIP:', 'BEEP:', 'LENGTH:', 'HW:', 'CH:', 'MIC:', 'PHONES:', 'COMP:', 'TIMELAPSE', 'MODE:', 'DSK:', 'SHUTDOWN', 'SRV:', 'SEARCH:', 'WIFI:', 'UPDATE', 'UPLOAD', 'BACKUP', 'LOAD', 'NEW', 'TITLE', 'LIVE:' + gonzopictrlmenu = 'FILM:', 'SCENE:', 'SHOT:', 'TAKE:', '', 'SHUTTER:', 'ISO:', 'RED:', 'BLUE:', 'FPS:', 'Q:', 'BRIGHT:', 'CONT:', 'SAT:', 'FLIP:', 'BEEP:', 'LENGTH:', 'HW:', 'CH:', 'MIC:', 'PHONES:', 'COMP:', 'TIMELAPSE', 'MODE:', 'DSK:', 'SHUTDOWN', 'SRV:', 'SEARCH:', 'WIFI:', 'CAMERA:', 'Add CAMERA', 'New FILM', 'New SCENE', 'Sync SCENE' + #gonzopictrlmenu = "BACK","CAMERA:", "Add CAMERA","New FILM","","New SCENE","Sync SCENE","Snapshot" + emptymenu='','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','' + menu = standardmenu + showgonzopictrl = False + recordwithports = False + pressagain = '' + #STANDARD VALUES (some of these may not be needed, should do some clean up) + abc = '_','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','1','2','3','4','5','6','7','8','9','0' + numbers_only = ' ','1','2','3','4','5','6','7','8','9','0' + keydelay = 0.0555 + selectedaction = 0 + selected = 0 + awb = 'auto', 'sunlight', 'cloudy', 'shade', 'tungsten', 'fluorescent', 'incandescent', 'flash', 'horizon' + awbx = 0 + awb_lock = 'no' + cammode = 'film' + camera_model='' + fps = 25 + fps_selected=2 + quality = 27 + profilelevel='4.2' + headphoneslevel = 40 + miclevel = 50 + soundformat = 'S16_LE' + soundrate = '48000' + recording = False + retake = False + lastmenu = '' + menudone = '' + rendermenu = True + showmenu = 1 + showmenu_settings = True + showhelp = False + overlay = None + overlay2 = None + underlay = None + reclenght = 0 + t = 0 + rectime = '' + scene = 1 + shot = 1 + take = 1 + pic = 1 + onlysound=False + filmname = 'onthefloor' + newfilmname = '' + beeps = 0 + beepcountdown = 0 + beeping = False + backlight = True + lastbeep = time.time() + flip = 'no' + between = 30 + duration = 0.2 + lenses = os.listdir('lenses/') + lens = lenses[0] + buttontime = time.time() + pressed = '' + buttonpressed = False + holdbutton = '' + updatethumb = False + loadfilmsettings = True + oldsettings = '' + comp = 0 + yanked = '' + copying = '' + moving = False + stream = '' + live = 'no' + peakshot = '' + peaktake = '' + plughw = 0 #default audio device + channels = 1 #default mono + #SAVE SETTINGS FREQUENCY IN SECS + pausetime = time.time() + savesettingsevery = 5 + #TARINA VERSION + f = open(gonzopifolder + '/VERSION') + gonzopiversion = f.readline() + gonzopivername = f.readline() + + #START INTERFACE + startinterface() + + db='' + #FIRE UP CAMERA + camera = startcamera(lens,fps) + #GET FILMFOLDER AND CAMERA VERSION + camera_model, camera_revision , filmfolder = getconfig(camera) + if os.path.isdir(filmfolder) == False: + os.makedirs(filmfolder) + + #SYSTEM CONFIGS (turn off hdmi) + #run_command('tvservice -o') + #Kernel page cache optimization for sd card + run_command('sudo ' + gonzopifolder + '/extras/sdcardhack.sh') + #Make screen shut off work and run full brightness + run_command('gpio -g mode 19 pwm ') + run_command('gpio -g pwm 19 1023') + #COUNT DISKSPACE + disk = os.statvfs(filmfolder) + diskleft = str(int(disk.f_bavail * disk.f_frsize / 1024 / 1024 / 1024)) + 'Gb' + + #LOAD FILM AND SCENE SETTINGS + try: + filmname = getfilms(filmfolder)[0][0] + except: + filmname = filmname + try: + filmname_back = getfilms(filmfolder)[0][1] + except: + filmname_back = filmname + + #THUMBNAILCHECKER + oldscene = scene + oldshot = shot + oldtake = take + #TURN ON WIFI AND TARINA SERVER + serverstate = 'on' + wifistate = 'on' + if os.path.isdir(gonzopifolder+'/srv/sessions') == False: + os.makedirs(gonzopifolder+'/srv/sessions') + os.system('chown www-data '+gonzopifolder+'/srv/sessions') + #serverstate = gonzopiserver(False) + #TO_BE_OR_NOT_TO_BE + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + recordable = not os.path.isfile(foldername + filename + '.mp4') and not os.path.isfile(foldername + filename + '.h264') + onthefloor_folder=filmfolder+'onthefloor' + if os.path.isdir(onthefloor_folder) == False: + os.makedirs(onthefloor_folder) + + #--------------Gonzopi Controller over socket ports --------# + + #TARINACTRL + camerasconnected='' + sleep=0.2 + cameras = [] + camerasoff =[] + camselected=0 + newselected=0 + mastersound=None + camera_recording=None + pingip=0 + searchforcameras='off' + #NETWORKS + networks=[] + adapters = ifaddr.get_adapters() + for adapter in adapters: + print("IPs of network adapter " + adapter.nice_name) + for ip in adapter.ips: + if ':' not in ip.ip[0] and '127.0.0.1' != ip.ip: + print(ip.ip) + networks=[ip.ip] + if networks != []: + network=networks[0] + if network not in cameras: + cameras=[] + cameras.append(network) + + port = 55555 + que = Queue() + process = Process(target=listenforclients, args=("0.0.0.0", port, que)) + process.start() + nextstatus = '' + + serverstate_old='off' + wifistate_old='off' + + camera_model, camera_revision, filmfolder= getconfig(camera) + + #--------------MAIN LOOP---------------# + while True: + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressagain != '': + pressed = pressagain + pressagain = '' + #event = screen.getch() + if wifistate != wifistate_old: + if wifistate == 'on': + run_command('sudo iwconfig wlan0 txpower auto') + elif wifistate == 'off': + run_command('sudo iwconfig wlan0 txpower off') + wifistate_old = wifistate + if serverstate != serverstate_old: + if serverstate == 'on': + gonzopiserver(True) + elif serverstate == 'off': + gonzopiserver(False) + serverstate_old=serverstate + if recording == False: + #SHUTDOWN + if pressed == 'middle' and menu[selected] == 'SHUTDOWN': + writemessage('Hold on shutting down...') + time.sleep(1) + run_command('sudo shutdown -h now') + #MODE + elif pressed == 'changemode': + if cammode == 'film': + cammode = 'picture' + vumetermessage('changing to picture mode') + elif cammode == 'picture': + cammode = 'film' + vumetermessage('changing to film mode') + camera.stop_preview() + camera.close() + camera = startcamera(lens,fps) + loadfilmsettings = True + #PICTURE + elif pressed == 'picture': + if os.path.isdir(foldername) == False: + os.makedirs(foldername) + picture = foldername +'picture' + str(take).zfill(3) + '.jpeg' + run_command('touch ' + foldername + '.placeholder') + print('taking picture') + camera.capture(picture,format="jpeg",use_video_port=True) + #PEAKING + elif pressed == 'peak' and recordable == True: + if shot > 1: + peakshot = shot - 1 + peaktake = counttakes(filmname, filmfolder, scene, peakshot) + p_imagename = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(peakshot).zfill(3) + '/take' + str(peaktake).zfill(3) + '.jpeg' + overlay = displayimage(camera, p_imagename, overlay, 3) + while holdbutton == 'peak': + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + vumetermessage('peaking ' + str(peakshot)) + time.sleep(0.03) + overlay = removeimage(camera, overlay) + #SHOWHELP + elif pressed == 'showhelp': + vumetermessage('Button layout') + if showhelp == False: + overlay2 = removeimage(camera, overlay2) + overlay2 = displayimage(camera, gonzopifolder+'/extras/buttons.png', overlay, 4) + showhelp = True + elif showhelp == True: + overlay2 = removeimage(camera, overlay2) + updatethumb = True + showhelp = False + #while holdbutton == 'showhelp' or pressed == 'H': + # pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + # vumetermessage('Button layout') + # time.sleep(0.03) + #TIMELAPSE + elif pressed == 'middle' and menu[selected] == 'TIMELAPSE': + overlay = removeimage(camera, overlay) + takes = counttakes(filmname, filmfolder, scene, shot) + if takes > 0: + shot = countshots(filmname, filmfolder, scene) + 1 + take = 1 + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + renderedfilename, between, duration = timelapse(beeps,camera,filmname,foldername,filename,between,duration,backlight) + if renderedfilename != '': + #render thumbnail + #writemessage('creating thumbnail') + #run_command('avconv -i ' + foldername + filename + '.mp4 -frames 1 -vf scale=800:460 ' + foldername + filename + '.jpeg') + updatethumb = True + #VIEW SCENE + elif pressed == 'view' and menu[selected] == 'SCENE:': + writemessage('Loading scene...') + organize(filmfolder, filmname) + filmfiles = shotfiles(filmfolder, filmname, scene) + if len(filmfiles) > 0: + #Check if rendered video exist + camera.stop_preview() + #renderfilename, newaudiomix = renderscene(filmfolder, filmname, scene) + renderfilename = renderfilm(filmfolder, filmname, comp, scene, True) + if renderfilename != '': + remove_shots = playdub(filmname,renderfilename, 'film') + if remove_shots != []: + for i in remove_shots: + remove(filmfolder, filmname, scene, i, take, 'shot') + organize(filmfolder, filmname) + updatethumb = True + #loadfilmsettings = True + time.sleep(0.5) + else: + print('nothing to remove') + camera.start_preview() + else: + vumetermessage("There's absolutely nothing in this scene! hit rec!") + rendermenu = True + #VIEW FILM + elif pressed == 'view' and menu[selected] == 'FILM:': + writemessage('Loading film...') + organize(filmfolder, filmname) + filmfiles = viewfilm(filmfolder, filmname) + if len(filmfiles) > 0: + camera.stop_preview() + #removeimage(camera, overlay) + renderfilename = renderfilm(filmfolder, filmname, comp, 0, True) + if renderfilename != '': + remove_shots = playdub(filmname,renderfilename, 'film') + #overlay = displayimage(camera, imagename, overlay, 3) + camera.start_preview() + else: + vumetermessage('wow, shoot first! there is zero, nada, zip footage to watch now... just hit rec!') + rendermenu = True + #VIEW SHOT OR TAKE + elif pressed == 'view': + writemessage('Loading clip...') + organize(filmfolder, filmname) + takes = counttakes(filmname, filmfolder, scene, shot) + if takes > 0: + removeimage(camera, overlay) + camera.stop_preview() + foldername = filmfolder + filmname + '/scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + #compileshot(foldername + filename,filmfolder,filmname) + renderfilename, newaudiomix = rendershot(filmfolder, filmname, foldername+filename, scene, shot) + if renderfilename != '': + trim = playdub(filmname,foldername + filename, 'shot') + if trim: + take = counttakes(filmname, filmfolder, scene, shot)+1 + trim_filename = foldername + 'take' + str(take).zfill(3) + videotrim(foldername + filename, trim_filename, trim[0], trim[1]) + if os.path.exists(foldername+'dub') == True: + print('trim dubs here') + imagename = foldername + filename + '.jpeg' + overlay = displayimage(camera, imagename, overlay, 3) + camera.start_preview() + else: + vumetermessage('nothing here! hit rec!') + rendermenu = True + #DUB SHOT + elif pressed == 'middle' and menu[selected] == 'SHOT:' and recordable == False: + newdub = clipsettings(filmfolder, filmname, scene, shot, take, plughw) + take = counttakes(filmname, filmfolder, scene, shot) + if newdub: + camera.stop_preview() + #save original sound + dubfolder = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/dub/' + saveoriginal = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take'+str(take).zfill(3)+'.wav' + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, shot) + foldername = filmfolder + filmname + '/scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + if dubfiles==[]: + print('no dubs, copying original sound to original') + os.system('cp '+saveoriginal+' '+dubfolder+'original.wav') + time.sleep(2) + renderfilename, newaudiomix = rendershot(filmfolder, filmname, foldername+filename, scene, shot) + playdub(filmname,renderfilename, 'dub') + #run_command('sox -V0 -G /dev/shm/dub.wav -c 2 ' + newdub) + #add audio/video start delay sync + run_command('sox -V0 -G /dev/shm/dub.wav -c 2 /dev/shm/temp.wav trim 0.013') + run_command('mv /dev/shm/temp.wav '+ newdub) + audiosync, videolenght, audiolenght = audiotrim(renderfilename, 'end', newdub) + vumetermessage('new shot dubbing made!') + camera.start_preview() + time.sleep(1) + else: + vumetermessage('see ya around!') + rendermenu = True + #DUB SCENE + elif pressed == 'middle' and menu[selected] == 'SCENE:': + newdub = clipsettings(filmfolder, filmname, scene, 0, take, plughw) + if newdub: + camera.stop_preview() + renderfilename, newaudiomix = renderscene(filmfolder, filmname, scene) + playdub(filmname,renderfilename, 'dub') + #run_command('sox -V0 -G /dev/shm/dub.wav -c 2 ' + newdub) + #add audio/video start delay sync + run_command('sox -V0 -G /dev/shm/dub.wav -c 2 /dev/shm/temp.wav trim 0.013') + run_command('mv /dev/shm/temp.wav '+ newdub) + audiosync, videolenght, audiolenght = audiotrim(renderfilename, 'end', newdub) + vumetermessage('new scene dubbing made!') + camera.start_preview() + time.sleep(1) + else: + vumetermessage('see ya around!') + rendermenu = True + #DUB FILM + elif pressed == 'middle' and menu[selected] == 'FILM:': + newdub = clipsettings(filmfolder, filmname, 0, 0, take, plughw) + if newdub: + camera.stop_preview() + renderfilename = renderfilm(filmfolder, filmname, comp, 0, False) + playdub(filmname,renderfilename, 'dub') + run_command('sox -V0 -G /dev/shm/dub.wav -c 2 ' + newdub) + vumetermessage('new film dubbing made!') + camera.start_preview() + time.sleep(1) + else: + vumetermessage('see ya around!') + rendermenu = True + #BACKUP + elif pressed == 'middle' and menu[selected] == 'BACKUP': + copytousb(filmfolder) + rendermenu = True + #UPLOAD + elif pressed == 'middle' and menu[selected] == 'UPLOAD': + if webz_on() == True: + filmfiles = viewfilm(filmfolder, filmname) + if len(filmfiles) > 0: + renderfilename = renderfilm(filmfolder, filmname, comp, 0, True) + cmd = uploadfilm(renderfilename, filmname) + if cmd != None: + stopinterface(camera) + try: + run_command(cmd) + except: + logger.warning('uploadfilm bugging') + startinterface() + camera = startcamera(lens,fps) + loadfilmsettings = True + selectedaction = 0 + rendermenu = True + #LOAD FILM + elif pressed == 'middle' and menu[selected] == 'LOAD': + filmname = loadfilm(filmname, filmfolder) + loadfilmsettings = True + #UPDATE + elif pressed == 'middle' and menu[selected] == 'UPDATE': + if webz_on() == True: + stopinterface(camera) + gonzopiversion, gonzopivername = update(gonzopiversion, gonzopivername) + startinterface() + camera = startcamera(lens,fps) + loadfilmsettings = True + selectedaction = 0 + rendermenu = True + #WIFI + elif pressed == 'middle' and menu[selected] == 'WIFI:': + stopinterface(camera) + run_command('wicd-curses') + startinterface() + camera = startcamera(lens,fps) + loadfilmsettings = True + rendermenu = True + #NEW FILM + elif pressed == 'middle' and menu[selected] == 'NEW' or filmname == '' or pressed == 'new_film': + filmname_exist=False + if newfilmname == '': + newfilmname = nameyourfilm(filmfolder, filmname, abc, True) + allfilm = getfilms(filmfolder) + for i in allfilm: + if i[0] == newfilmname: + filmname_exist=True + if filmname != newfilmname and filmname_exist==False: + filmname = newfilmname + os.makedirs(filmfolder + filmname) + vumetermessage('Good luck with your film ' + filmname + '!') + #make a filmhash + print('making filmhash...') + filmhash = shortuuid.uuid() + with open(filmfolder + filmname + '/.filmhash', 'w') as f: + f.write(filmhash) + updatethumb = True + rendermenu = True + scene = 1 + shot = 1 + take = 1 + #selectedaction = 0 + newfilmname = '' + else: + print(term.clear) + filmname = newfilmname + newfilmname = '' + vumetermessage('film already exist!') + logger.info('film already exist!') + updatethumb = True + loadfilmsettings = True + rendermenu = True + #EDIT FILM NAME + elif pressed == 'middle' and menu[selected] == 'TITLE' or filmname == '': + newfilmname = nameyourfilm(filmfolder, filmname, abc, False) + if filmname != newfilmname: + os.system('mv ' + filmfolder + filmname + ' ' + filmfolder + newfilmname) + filmname = newfilmname + db = get_film_files(filmname,filmfolder,db) + vumetermessage('Film title changed to ' + filmname + '!') + else: + vumetermessage('') + rendermenu = True + #(YANK) COPY TAKE + elif pressed == 'copy' and menu[selected] == 'TAKE:' and recordable == False: + copying = 'take' + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3)+'/take' + str(take).zfill(3) + vumetermessage('Take ' + str(take) + ' copied! (I)nsert button to place it...') + time.sleep(1) + #(YANK) COPY SHOT + elif pressed == 'copy' and menu[selected] == 'SHOT:': + copying = 'shot' + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + vumetermessage('Shot ' + str(shot) + ' copied! (I)nsert button to place it...') + time.sleep(1) + #(YANK) COPY SCENE + elif pressed == 'copy' and menu[selected] == 'SCENE:': + copying = 'scene' + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + vumetermessage('Scene ' + str(scene) + ' copied! (I)nsert button to place it...') + time.sleep(1) + #(CUT) MOVE TAKE + elif pressed == 'move' and menu[selected] == 'TAKE:' and recordable == False: + copying = 'take' + moving = True + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3)+'/take' + str(take).zfill(3) + vumetermessage('Moving shot ' + str(shot) + ' (I)nsert button to place it...') + time.sleep(1) + #(CUT) MOVE SHOT + elif pressed == 'move' and menu[selected] == 'SHOT:': + copying='shot' + moving = True + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + vumetermessage('Moving shot ' + str(shot) + ' (I)nsert button to place it...') + time.sleep(1) + #(CUT) MOVE SCENE + elif pressed == 'move' and menu[selected] == 'SCENE:': + copying='scene' + moving = True + yanked = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + vumetermessage('Moving scene ' + str(scene) + ' (I)nsert button to place it...') + time.sleep(1) + #PASTE SHOT and PASTE SCENE + elif pressed == 'insert' and yanked: + if copying == 'take': + take = counttakes(filmname, filmfolder, scene, shot) + take=take+1 + vumetermessage('Pasting take, please wait...') + paste = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/take' + str(take).zfill(3) + #try: + # os.makedirs(filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot'+ str(shot).zfill(3)) + #except: + # pass + os.system('cp ' + yanked + '.mp4 ' + paste + '.mp4') + os.system('cp ' + yanked + '.jpeg ' + paste + '.jpeg') + os.system('cp ' + yanked + '.h264 ' + paste + '.h264') + os.system('cp ' + yanked + '.wav ' + paste + '.wav') + paste = '' + if moving == True: + os.system('rm -r ' + yanked + '*') + elif copying == 'shot': + vumetermessage('Pasting shot, please wait...') + paste = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot-1).zfill(3) + '_yanked' + try: + os.makedirs(filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3)) + except: + pass + os.system('cp -r ' + yanked + ' ' + paste) + if moving == True: + os.system('rm -r ' + yanked + '/*') + #Remove hidden placeholder + os.system('rm ' + yanked + '/.placeholder') + elif copying == 'scene': + vumetermessage('Pasting scene, please wait...') + paste = filmfolder + filmname + '/' + 'scene' + str(scene-1).zfill(3) + '_yanked' + os.system('cp -r ' + yanked + ' ' + paste) + if moving == True: + os.system('rm -r ' + yanked + '/*') + #Remove hidden placeholder + #os.system('rm ' + yanked + '/.placeholder') + add_organize(filmfolder, filmname) + organize(filmfolder, filmname) + updatethumb = True + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + yanked = '' + copying = '' + moving = False + vumetermessage('Pasted!') + time.sleep(2) + #INSERT SHOT + elif pressed == 'insert' and menu[selected] != 'SCENE:' and yanked == '': + insertshot = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot-1).zfill(3) + '_insert' + try: + os.makedirs(insertshot) + except: + print('is there already prob') + add_organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + vumetermessage('Shot ' + str(shot) + ' inserted') + updatethumb = True + time.sleep(1) + #INSERT SHOT TO LAST SHOT + elif pressed == 'insert_shot': + logger.info('inserting shot') + shot = countshots(filmname, filmfolder, scene) + shot=shot+1 + insertshot = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot-1).zfill(3) + '_insert' + try: + os.makedirs(insertshot) + except: + print('is there already prob') + add_organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + vumetermessage('Shot ' + str(shot) + ' inserted') + updatethumb = True + #INSERT TAKE + elif pressed == 'insert_take': + logger.info('inserting take') + insertshot = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + try: + os.makedirs(insertshot) + run_command('touch ' + insertshot + '/.placeholder') + except: + print('is there already prob') + add_organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + vumetermessage('Take ' + str(shot) + ' inserted') + updatethumb = True + #time.sleep(1) + #INSERT SCENE + elif pressed == 'insert' and menu[selected] == 'SCENE:' and recordable == False and yanked == '': + insertscene = filmfolder + filmname + '/' + 'scene' + str(scene-1).zfill(3) + '_insert' + logger.info("inserting scene") + os.makedirs(insertscene) + add_organize(filmfolder, filmname) + take = 1 + shot = 1 + updatethumb = True + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + vumetermessage('Scene ' + str(scene) + ' inserted') + time.sleep(1) + #NEW SCENE + elif pressed == 'new_scene': + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + vumetermessage('got new scene') + scene=scenes+1 + shot=1 + take=1 + #DEVELOP + elif event == 'D': + try: + stopinterface(camera) + code.interact(local=locals()) + startinterface() + camera = startcamera(lens,fps) + loadfilmsetings = True + except: + writemessage('hmm.. couldnt enter developer mode') + #TURN OFF SCREEN + elif pressed == 'screen': + if backlight == False: + # requires wiringpi installed + run_command('gpio -g pwm 19 1023') + backlight = True + camera.start_preview() + elif backlight == True: + run_command('gpio -g pwm 19 0') + backlight = False + camera.stop_preview() + elif pressed == 'showmenu': + if showmenu == 1: + # requires wiringpi installed + showmenu = 0 + showmenu_settings = False + elif showmenu == 0: + showmenu = 1 + showmenu_settings = True + #DSK + elif pressed == 'middle' and menu[selected] == 'DSK:': + print("clean up film folder here") + #cleanupdisk(filmname,filmfolder) + #REMOVE DELETE + #take + elif pressed == 'remove' and menu[selected] == 'TAKE:': + remove(filmfolder, filmname, scene, shot, take, 'take') + organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + take = counttakes(filmname, filmfolder, scene, shot) + updatethumb = True + rendermenu = True + #loadfilmsettings = True + time.sleep(0.2) + #shot + elif pressed == 'remove' and menu[selected] == 'SHOT:': + remove(filmfolder, filmname, scene, shot, take, 'shot') + organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + take = counttakes(filmname, filmfolder, scene, shot) + updatethumb = True + rendermenu = True + #loadfilmsettings = True + time.sleep(0.2) + #scene + elif pressed == 'remove' and menu[selected] == 'SCENE:' or pressed=='remove_now': + remove(filmfolder, filmname, scene, shot, take, 'scene') + organize(filmfolder, filmname) + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + shot = countshots(filmname, filmfolder, scene) + take = counttakes(filmname, filmfolder, scene, shot) + updatethumb = True + rendermenu = True + #loadfilmsettings = True + time.sleep(0.2) + #film + elif pressed == 'remove' and menu[selected] == 'FILM:': + remove(filmfolder, filmname, scene, shot, take, 'film') + filmname = getfilms(filmfolder)[0][0] + if filmname == '': + filmname = nameyourfilm(filmfolder,filmname,abc, True) + else: + scene, shot, take = countlast(filmname, filmfolder) + loadfilmsettings = True + updatethumb = True + rendermenu = True + time.sleep(0.2) + elif pressed == 'remove' and menu[selected] == 'CAMERA:': + if camselected != 0: + cameras.pop(camselected) + newselected=0 + elif pressed == 'middle' and menu[selected] == 'Add CAMERA': + if networks != []: + newcamera = newcamera_ip(numbers_only, network) + if newcamera != '': + if newcamera not in cameras and newcamera not in networks: + sendtocamera(newcamera,port,'NEWFILM:'+filmname) + time.sleep(0.2) + sendtocamera(newcamera,port,'Q:'+str(quality)) + time.sleep(0.2) + sendtocamera(newcamera,port,'SHOT:'+str(shot)) + time.sleep(0.2) + sendtocamera(newcamera,port,'SCENE:'+str(scene)) + time.sleep(0.2) + sendtocamera(newcamera,port,'MAKEPLACEHOLDERS:'+str(scenes)+'|'+str(shots)) + cameras.append(newcamera) + rendermenu = True + #newselected=newselected+1 + camera_recording=None + vumetermessage("New camera! "+newcamera) + else: + vumetermessage('No network!') + elif 'SYNCIP:' in pressed: + ip = pressed.split(':')[1] + vumetermessage('SYNCING!') + stopinterface(camera) + video_files=shotfiles(filmfolder, filmname, scene) + for i in video_files: + compileshot(i,filmfolder,filmname) + logger.info('SYNCING:'+i) + organize(filmfolder, filmname) + if not os.path.isfile('/home/pi/.ssh/id_rsa'): + run_command('ssh-keygen') + run_command('ssh-copy-id pi@'+ip) + try: + run_command('rsync -avr --update --progress --files-from='+filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/.origin_videos / pi@'+ip+':/') + except: + logger.info('no origin videos') + #run_command('scp -r '+filmfolder+filmname+'/'+'scene'+str(scene).zfill(3)+' pi@'+ip+':'+filmfolder+filmname+'/') + sendtocamera(ip,port,'SYNCDONE:'+cameras[0]) + startinterface() + camera = startcamera(lens,fps) + loadfilmsettings = True + rendermenu = True + elif 'SYNCDONE:' in pressed: + stopinterface(camera) + ip = pressed.split(':')[1] + logger.info('SYNCING from ip:'+ip) + run_command('ssh-copy-id pi@'+ip) + try: + run_command('rsync -avr --update --progress pi@'+ip+':'+filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/ '+filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/') + except: + logger.info('no files') + with open(filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/.origin_videos', 'r') as f: + scene_origin_files = [line.rstrip() for line in f] + #a=0 + #for i in cameras: + # if a != 0: + # run_command('rsync -avr --update --progress '+filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/ pi@'+i+':'+filmfolder+filmname+'/scene'+str(scene).zfill(3)+'/') + # time.sleep(3) + # a=a+1 + startinterface() + camera = startcamera(lens,fps) + loadfilmsettings = True + rendermenu = True + vumetermessage('SYNC DONE!') + elif 'RETAKE:' in pressed: + shot=pressed.split(':')[1] + shot=int(shot) + retake = True + pressed="retake_now" + elif 'SCENE:' in pressed: + scene=pressed.split(':')[1] + scene=int(scene) + shot = countshots(filmname, filmfolder, scene) + take = counttakes(filmname, filmfolder, scene, shot) + elif 'SHOT:' in pressed: + shot=pressed.split(':')[1] + shot=int(shot) + take = counttakes(filmname, filmfolder, scene, shot) + elif 'REMOVE:' in pressed: + scene=pressed.split(':')[1] + scene=int(scene) + shot = countshots(filmname, filmfolder, scene) + take = counttakes(filmname, filmfolder, scene, shot) + pressagain='remove_now' + elif 'Q:' in pressed: + qual=pressed.split(':')[1] + quality=int(qual) + vumetermessage('Quality changed to '+str(quality)) + elif 'MAKEPLACEHOLDERS:' in pressed: + scenesshots=pressed.split(':')[1] + pscene=int(scenesshots.split('|')[0]) + pshots=int(scenesshots.split('|')[1]) + #to not throw away empty shots, make placeholders + for i in range(pshots): + placeholders=filmfolder + filmname + '/scene' + str(pscene).zfill(3) + '/shot' + str(i+1).zfill(3) + try: + os.makedirs(placeholders) + except: + logger.info('scene or shot already there!') + run_command('touch ' + placeholders + '/.placeholder') + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + rendermenu = True + vumetermessage('CONNECTED TO MASTER TARINA!') + #SHOWTARINACTRL + if recordwithports: + if pressed == 'middle' and menu[selected] == "New FILM": + newfilmname = nameyourfilm(filmfolder, filmname, abc, True) + a=0 + for i in cameras: + if i not in camerasoff: + sendtocamera(i,port,'NEWFILM:'+newfilmname) + a=a+1 + elif pressed == "retake": + a=0 + for i in cameras: + if i not in camerasoff: + if a == camselected: + if camera_recording == a: + if a==0: + if recording == True: + pressed="retake_now" + retake = True + camera_recording=None + else: + sendtocamera(i,port,'STOPRETAKE') + camera_recording=None + else: + if a==0: + if recording == False: + pressed="retake_now" + retake = True + camera_recording=0 + else: + sendtocamera(i,port,'RETAKE:'+str(shot)) + camera_recording=camselected + else: + if a==0: + pressagain='insert_take' + else: + sendtocamera(i,port,'TAKEPLACEHOLDER') + a=a+1 + elif pressed == "middle" and menu[selected]=="Sync SCENE": + for i in cameras: + if i != cameras[0]: + vumetermessage('Hold on syncing!') + sendtocamera(i,port,'SYNCIP:'+cameras[0]) + time.sleep(1) + elif pressed == "middle" and menu[selected]=='New SCENE': + a=0 + for i in cameras: + if i not in camerasoff: + if a==0: + pressagain="new_scene" + else: + sendtocamera(i,port,'NEWSCENE') + a=a+1 + elif pressed == "record" and camera_recording != None: + if camera_recording == 0: + if recording == True: + pressed='record_now' + else: + sendtocamera(cameras[camera_recording],port,'STOP') + camera_recording=None + elif pressed == "record" and camera_recording == None: + a=0 + for i in cameras: + if i not in camerasoff: + if a == camselected: + if camselected==0: + pressed='record_now' + else: + sendtocamera(i,port,'REC') + camera_recording=camselected + else: + if a==0: + pressagain='insert_shot' + else: + sendtocamera(i,port,'PLACEHOLDER') + a=a+1 + elif pressed == "remove" and menu[selected]=='SCENE:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'REMOVE:'+str(scene)) + a=a+1 + elif pressed == "up" and menu[selected]=='SCENE:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'SCENE:'+str(scene+1)) + a=a+1 + elif pressed == "down" and menu[selected]=='SCENE:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'SCENE:'+str(scene-1)) + a=a+1 + elif pressed == "up" and menu[selected]=='SHOT:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'SHOT:'+str(shot+1)) + a=a+1 + elif pressed == "down" and menu[selected]=='SHOT:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'SHOT:'+str(shot-1)) + a=a+1 + elif pressed == "up" and menu[selected]=='Q:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'Q:'+str(quality+1)) + a=a+1 + elif pressed == "down" and menu[selected]=='Q:': + a=0 + for i in cameras: + if a!=0: + sendtocamera(i,port,'Q:'+str(quality-1)) + a=a+1 + elif event == "0": + newselected = 0 + elif event == "1": + if len(cameras) > 1: + newselected = 1 + elif event == "2": + if len(cameras) > 2: + newselected = 2 + elif event == "3": + if len(cameras) > 3: + newselected = 3 + elif event == "4": + if len(cameras) > 4: + newselected = 4 + elif event == "5": + if len(cameras) > 5: + newselected = 5 + elif event == "6": + if len(cameras) > 6: + newselected = 6 + elif event == "7": + if len(cameras) > 7: + newselected = 7 + elif event == "8": + if len(cameras) > 8: + newselected = 8 + elif event == "9": + if len(cameras) > 9: + newselected = 9 + elif event == "-": + if cameras[camselected] not in camerasoff: + camerasoff.append(cameras[camselected]) + elif event == "+": + if cameras[camselected] in camerasoff: + camerasoff.remove(cameras[camselected]) + elif camselected != newselected: + if camera_recording != None: + #change camera + a=0 + for c in cameras: + if c not in camerasoff: + if a == camselected: + if a == 0: + #pressed='record_now' + #pressagain='insert_shot' + delayedstop=c + else: + #sendtocamera(c,port,'STOP') + #time.sleep(sleep) + #sendtocamera(c,port,'PLACEHOLDER') + delayedstop=c + elif a == newselected: + if a == 0: + if recording == False: + pressed='record_now' + else: + sendtocamera(c,port,'REC') + camera_recording=newselected + else: + if a == 0: + pressagain='insert_shot' + else: + sendtocamera(c,port,'PLACEHOLDER') + #time.sleep(2) + a=a+1 + if delayedstop: + time.sleep(0.05) + if delayedstop==cameras[0]: + if recording == True: + pressed='record_now' + pressagain='insert_shot' + else: + sendtocamera(delayedstop,port,'STOP') + time.sleep(sleep) + sendtocamera(delayedstop,port,'PLACEHOLDER') + camselected=newselected + rendermenu = True + #vumetermessage('filming with '+camera_model +' ip:'+ network + ' '+camerasconnected+' camselected:'+str(camselected)) + if len(cameras) > 1: + vumetermessage('filming with '+camera_model +' ip:'+ cameras[camselected] + ' '+camerasconnected+' camselected:'+str(camselected)+' rec:'+str(camera_recording)) + else: + vumetermessage('filming with '+camera_model +' ip:'+ network + ' '+camerasconnected+' camselected:'+str(camselected)+' rec:'+str(camera_recording)) + + + #RECORD AND PAUSE + if beepcountdown > 1: + if time.time() - lastbeep > 1: + beep(bus) + beepcountdown -= 1 + lastbeep = time.time() + logger.info('beepcountdown: ' + str(beepcountdown)) + vumetermessage('Filming in ' + str(beepcountdown) + ' seconds, press record again to cancel ') + elif beepcountdown > 0: + if time.time() - float(lastbeep) > 0.1: + beep(bus) + vumetermessage('Get ready!!') + if time.time() - lastbeep > 1: + longbeep(bus) + beepcountdown = 0 + if recordwithports == True: + if retake == True: + pressed = 'retake_now' + retake = False + else: + pressed = 'record_now' + else: + pressed = 'record' + print('exhausted from all beepings') + if pressed == 'record' and recordwithports==False or pressed == 'record_now' or pressed == 'retake_now' or pressed == 'retake' and recordwithports==False or reclenght != 0 and t > reclenght: + overlay = removeimage(camera, overlay) + if recording == False and recordable == True or recording == False and pressed == 'record_now' or recording == False and pressed == 'retake_now': + #camera_recording=0 + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + if pressed == "record": + #shot = shots+1 + take = takes+1 + elif pressed == "retake": + take = takes+1 + elif pressed == 'record_now': + shot=shots+1 + take=1 + elif pressed == 'retake_now': + takes = counttakes(filmname, filmfolder, scene, shot) + take = takes + 1 + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + if beeps > 0 and beeping == False: + beeping = True + beepcountdown = beeps + elif beepcountdown == 0: + beeping = False + if os.path.isdir(foldername) == False: + os.makedirs(foldername) + if cammode == 'film': + videos_totalt = db.query("SELECT COUNT(*) AS videos FROM videos")[0] + tot = int(videos_totalt.videos) + video_origins=datetime.datetime.now().strftime('%Y%d%m')+str(tot).zfill(5) + db.insert('videos', tid=datetime.datetime.now(), filename=filmfolder+'.videos/'+video_origins+'.mp4', foldername=foldername, filmname=filmname, scene=scene, shot=shot, take=take, audiolenght=0, videolenght=0) + os.system(gonzopifolder + '/alsa-utils-1.1.3/aplay/arecord -D plughw:' + str(plughw) + ' -f '+soundformat+' -c ' + str(channels) + ' -r '+soundrate+' -vv '+ foldername + filename + '.wav &') + sound_start = time.time() + if onlysound != True: + camera.start_recording(filmfolder+ '.videos/'+video_origins+'.h264', format='h264', quality=quality, level=profilelevel) + starttime = time.time() + os.system('ln -s '+filmfolder+'.videos/'+video_origins+'.h264 '+foldername+filename+'.h264') + recording = True + showmenu = 0 + if cammode == 'picture': + #picdate=datetime.datetime.now().strftime('%Y%d%m') + picture = foldername +'picture' + str(take).zfill(3) + '.jpeg' + print('taking picture') + camera.capture(picture,format="jpeg",use_video_port=True) + run_command('touch ' + foldername + 'take' + str(take).zfill(3) + '.mp4') + basewidth = 800 + img = Image.open(picture) + wpercent = (basewidth/float(img.size[0])) + hsize = int((float(img.size[1])*float(wpercent))) + img = img.resize((basewidth,hsize), Image.ANTIALIAS) + img.save(foldername+'take'+str(take).zfill(3) + '.jpeg') + vumetermessage('Great Pic taken!!') + updatethumb = True + elif beepcountdown > 0 and beeping == True: + beeping = False + beepcountdown = 0 + vumetermessage('Filming was canceled!!') + elif recording == True and float(time.time() - starttime) > 0.2: + #print(term.clear+term.home) + disk = os.statvfs(gonzopifolder + '/') + diskleft = str(int(disk.f_bavail * disk.f_frsize / 1024 / 1024 / 1024)) + 'Gb' + recording = False + if showmenu_settings == True: + showmenu = 1 + if onlysound != True: + camera.stop_recording() + os.system('pkill arecord') + soundlag=starttime-sound_start + db.update('videos', where='filename="'+filmfolder+'.videos/'+video_origins+'.mp4"', soundlag=soundlag) + #time.sleep(0.005) #get audio at least 0.1 longer + #camera.capture(foldername + filename + '.jpeg', resize=(800,341)) + if onlysound != True: + try: + #camera.capture(foldername + filename + '.jpeg', resize=(800,340), use_video_port=True) + camera.capture(foldername + filename + '.jpeg', resize=(800,450), use_video_port=True) + except: + logger.warning('something wrong with camera jpeg capture') + #delayerr = audiotrim(foldername,filename) + onlysound = False + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + if beeps > 0: + if bus: + buzz(300) + else: + run_command('aplay -D plughw:' + str(plughw) + ' '+ gonzopifolder + '/extras/beep.wav') + if round(fps) != 25: + compileshot(foldername + filename,filmfolder,filmname) + #os.system('cp /dev/shm/' + filename + '.wav ' + foldername + filename + '.wav') + if beeps > 0: + if bus: + buzz(150) + else: + run_command('aplay -D plughw:' + str(plughw) + ' '+ gonzopifolder + '/extras/beep.wav') + t = 0 + rectime = '' + vumetermessage('Gonzopi ' + gonzopiversion[:-1] + ' ' + gonzopivername[:-1]) + updatethumb = True + #camera_recording=0 + #if not in last shot or take then go to it + if pressed == 'record' and recordable == False: + scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + shot=shots+1 + take=1 + #take = takes + #takes = counttakes(filmname, filmfolder, scene, shot) + if pressed == 'retake' and recordable == False: + #scenes, shots, takes = browse(filmname,filmfolder,scene,shot,take) + takes = counttakes(filmname, filmfolder, scene, shot) + #take = takes + #takes = counttakes(filmname, filmfolder, scene, shot) + take = takes + 1 + #ENTER (auto shutter, iso, awb on/off) + elif pressed == 'middle' and menu[selected] == 'SHUTTER:': + if camera.shutter_speed == 0: + camera.shutter_speed = camera.exposure_speed + else: + camera.shutter_speed = 0 + elif pressed == 'middle' and menu[selected] == 'ISO:': + if camera.iso == 0: + camera.iso = 100 + else: + camera.iso = 0 + elif pressed == 'middle' and menu[selected] == 'RED:': + if camera.awb_mode == 'auto': + camera.awb_gains = (float(camera.awb_gains[0]), float(camera.awb_gains[1])) + camera.awb_mode = 'off' + else: + camera.awb_mode = 'auto' + elif pressed == 'middle' and menu[selected] == 'BLUE:': + if camera.awb_mode == 'auto': + camera.awb_gains = (float(camera.awb_gains[0]), float(camera.awb_gains[1])) + camera.awb_mode = 'off' + else: + camera.awb_mode = 'auto' + elif pressed == 'middle' and menu[selected] == 'BEEP:': + beeps = 0 + elif pressed == 'middle' and menu[selected] == 'LENGTH:': + reclenght = 0 + elif pressed == 'middle' and menu[selected] == 'LIVE:': + if stream == '': + stream = startstream(camera, stream, plughw, channels) + if stream == '': + vumetermessage('something wrong with streaming') + else: + live = 'yes' + else: + stream = stopstream(camera, stream) + live = 'no' + elif pressed == 'middle' and menu[selected] == 'BRIGHT:': + camera.brightness = 50 + elif pressed == 'middle' and menu[selected] == 'CONT:': + camera.contrast = 0 + elif pressed == 'middle' and menu[selected] == 'SAT:': + camera.saturation = 0 + elif pressed == 'middle' and menu[selected] == 'MIC:': + miclevel = 70 + elif pressed == 'middle' and menu[selected] == 'PHONES:': + headphoneslevel = 70 + elif pressed == 'middle' and menu[selected] == 'SRV:': + if showgonzopictrl == False: + menu=gonzopictrlmenu + #selected=0 + showgonzopictrl = True + else: + menu=standardmenu + showgonzopictrl=False + + #UP + elif pressed == 'up': + if menu[selected] == 'FILM:': + filmname = 'onthefloor' + filmname_back = filmname + filmname = loadfilm(filmname, filmfolder) + loadfilmsettings = True + if menu[selected] == 'BRIGHT:': + camera.brightness = min(camera.brightness + 1, 99) + elif menu[selected] == 'CONT:': + camera.contrast = min(camera.contrast + 1, 99) + elif menu[selected] == 'SAT:': + camera.saturation = min(camera.saturation + 1, 99) + elif menu[selected] == 'SHUTTER:': + if camera.shutter_speed == 0: + camera.shutter_speed = camera.exposure_speed + if camera.shutter_speed < 5000: + camera.shutter_speed = min(camera.shutter_speed + 50, 50000) + else: + camera.shutter_speed = min(camera.shutter_speed + 200, 50000) + elif menu[selected] == 'ISO:': + camera.iso = min(camera.iso + 100, 1600) + elif menu[selected] == 'BEEP:': + beeps = beeps + 1 + elif menu[selected] == 'FLIP:': + if flip == 'yes': + camera.hflip = False + camera.vflip = False + flip = 'no' + time.sleep(0.2) + else: + camera.hflip = True + camera.vflip = True + flip = 'yes' + time.sleep(0.2) + elif menu[selected] == 'LENGTH:': + reclenght = reclenght + 1 + time.sleep(0.1) + elif menu[selected] == 'MIC:': + if miclevel < 100: + miclevel = miclevel + 2 + run_command('amixer -c 0 sset Mic ' + str(miclevel) + '% unmute') + elif menu[selected] == 'PHONES:': + if headphoneslevel < 100: + headphoneslevel = headphoneslevel + 2 + run_command('amixer -c 0 sset Speaker ' + str(headphoneslevel) + '%') + elif menu[selected] == 'SCENE:' and recording == False: + if scene <= scenes: + scene += 1 + #shot = countshots(filmname, filmfolder, scene) + shot = 1 + take = counttakes(filmname, filmfolder, scene, shot) + #scene, shots, takes = browse2(filmname, filmfolder, scene, shot, take, 0, 1) + #shot = 1 + elif menu[selected] == 'SHOT:' and recording == False: + if shot <= shots: + shot += 1 + take = counttakes(filmname, filmfolder, scene, shot) + #scene, shot, take = browse2(filmname, filmfolder, scene, shot, take, 1, 1) + #takes = take + elif menu[selected] == 'TAKE:' and recording == False: + if take <= takes: + take += 1 + #scene, shot, take = browse2(filmname, filmfolder, scene, shot, take, 2, 1) + elif menu[selected] == 'RED:': + camera.awb_mode = 'off' + if float(camera.awb_gains[0]) < 7.98: + camera.awb_gains = (round(camera.awb_gains[0],2) + 0.02, round(camera.awb_gains[1],2)) + elif menu[selected] == 'BLUE:': + camera.awb_mode = 'off' + if float(camera.awb_gains[1]) < 7.98: + camera.awb_gains = (round(camera.awb_gains[0],2), round(camera.awb_gains[1],2) + 0.02) + elif menu[selected] == 'SRV:': + if serverstate == 'on': + try: + os.makedirs(gonzopifolder+'/srv/sessions') + os.system('chown www-data '+gonzopifolder+'/srv/sessions') + except: + print('srv folder exist') + serverstate = 'false' + serverstate = gonzopiserver(False) + elif serverstate == 'off': + serverstate = 'on' + serverstate = gonzopiserver(True) + elif menu[selected] == 'WIFI:': + if wifistate == 'on': + run_command('sudo iwconfig wlan0 txpower off') + wifistate = 'off' + elif wifistate == 'off': + run_command('sudo iwconfig wlan0 txpower auto') + wifistate = 'on' + elif menu[selected] == 'SEARCH:': + if searchforcameras == 'on': + searchforcameras = 'off' + elif searchforcameras == 'off': + searchforcameras = 'on' + elif menu[selected] == 'MODE:': + if cammode == 'film': + cammode = 'picture' + vumetermessage('changing to picture mode') + elif cammode == 'picture': + cammode = 'film' + vumetermessage('changing to film mode') + camera.stop_preview() + camera.close() + camera = startcamera(lens,fps) + loadfilmsettings = True + elif menu[selected] == 'LENS:': + s = 0 + for a in lenses: + if a == lens: + selectlens = s + s += 1 + if selectlens < len(lenses) - 1: + selectlens += 1 + lens = os.listdir('lenses/')[selectlens] + #npzfile = np.load('lenses/' + lens) + #lensshade = npzfile['lens_shading_table'] + table = read_table('lenses/' + lens) + camera.lens_shading_table = table + elif menu[selected] == 'COMP:': + if comp < 1: + comp += 1 + elif menu[selected] == 'HW:': + if plughw < len(getaudiocards())-1: + plughw += 1 + vumetermessage(getaudiocards()[plughw]) + elif menu[selected] == 'CH:': + if channels == 1: + channels = 2 + elif menu[selected] == 'FPS:': + if camera_model == 'imx477': + if fps_selected < len(fps_selection)-1: + fps_selected+=1 + fps_selection=[5,8,10,11,12,13,14,15,24.985,35,49] + fps=fps_selection[fps_selected] + camera.framerate = fps + elif menu[selected] == 'Q:': + if quality < 39: + quality += 1 + elif menu[selected] == 'CAMERA:': + if camselected < len(cameras)-1: + newselected = camselected+1 + logger.info('camera selected:'+str(camselected)) + + #LEFT + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + else: + selected = len(menu) - 1 + if selected == 4: + selected = 3 + #DOWN + elif pressed == 'down': + if menu[selected] == 'FILM:': + if filmname == 'onthefloor': + try: + filmname = getfilms(filmfolder)[1][0] + except: + filmname='onthefloor' + filename_back = 'onthefloor' + loadfilmsettings = True + else: + filmname = 'onthefloor' + loadfilmsettings = True + elif menu[selected] == 'BRIGHT:': + camera.brightness = max(camera.brightness - 1, 0) + elif menu[selected] == 'CONT:': + camera.contrast = max(camera.contrast - 1, -100) + elif menu[selected] == 'SAT:': + camera.saturation = max(camera.saturation - 1, -100) + elif menu[selected] == 'SHUTTER:': + if camera.shutter_speed == 0: + camera.shutter_speed = camera.exposure_speed + if camera.shutter_speed < 5000: + camera.shutter_speed = max(camera.shutter_speed - 50, 20) + else: + camera.shutter_speed = max(camera.shutter_speed - 200, 200) + elif menu[selected] == 'ISO:': + camera.iso = max(camera.iso - 100, 100) + elif menu[selected] == 'BEEP:': + if beeps > 0: + beeps = beeps - 1 + elif menu[selected] == 'FLIP:': + if flip == 'yes': + camera.hflip = False + camera.vflip = False + flip = 'no' + time.sleep(0.2) + else: + camera.hflip = True + camera.vflip = True + flip = 'yes' + time.sleep(0.2) + elif menu[selected] == 'LENGTH:': + if reclenght > 0: + reclenght = reclenght - 1 + time.sleep(0.1) + elif menu[selected] == 'MIC:': + if miclevel > 0: + miclevel = miclevel - 2 + run_command('amixer -c 0 sset Mic ' + str(miclevel) + '% unmute') + elif menu[selected] == 'PHONES:': + if headphoneslevel > 0: + headphoneslevel = headphoneslevel - 2 + run_command('amixer -c 0 sset Speaker ' + str(headphoneslevel) + '%') + elif menu[selected] == 'SCENE:' and recording == False: + if scene > 1: + scene -= 1 + #shot = countshots(filmname, filmfolder, scene) + shot=1 + take = counttakes(filmname, filmfolder, scene, shot) + #scene, shots, take = browse2(filmname, filmfolder, scene, shot, take, 0, -1) + #takes = take + #shot = 1 + elif menu[selected] == 'SHOT:' and recording == False: + if shot > 1: + shot -= 1 + take = counttakes(filmname, filmfolder, scene, shot) + #scene, shot, take = browse2(filmname, filmfolder, scene, shot, take, 1, -1) + #takes = take + elif menu[selected] == 'TAKE:' and recording == False: + if take > 1: + take -= 1 + #scene, shot, take = browse2(filmname, filmfolder, scene, shot, take, 2, -1) + elif menu[selected] == 'RED:': + camera.awb_mode = 'off' + if float(camera.awb_gains[0]) > 0.02: + camera.awb_gains = (round(camera.awb_gains[0],2) - 0.02, round(camera.awb_gains[1],2)) + elif menu[selected] == 'BLUE:': + camera.awb_mode = 'off' + if float(camera.awb_gains[1]) > 0.02: + camera.awb_gains = (round(camera.awb_gains[0],2), round(camera.awb_gains[1],2) - 0.02) + elif menu[selected] == 'SRV:': + if serverstate == 'on': + try: + os.makedirs(gonzopifolder+'/srv/sessions') + os.system('chown www-data '+gonzopifolder+'/srv/sessions') + except: + print('srv folder exist') + serverstate = gonzopiserver(False) + elif serverstate == 'off': + serverstate = gonzopiserver(True) + elif menu[selected] == 'WIFI:': + if wifistate == 'on': + run_command('sudo iwconfig wlan0 txpower off') + wifistate = 'off' + elif wifistate == 'off': + run_command('sudo iwconfig wlan0 txpower auto') + wifistate = 'on' + elif menu[selected] == 'SEARCH:': + if searchforcameras == 'on': + searchforcameras = 'off' + elif searchforcameras == 'off': + seaarchforcameras = 'on' + elif menu[selected] == 'MODE:': + if cammode == 'film': + cammode = 'picture' + vumetermessage('changing to picture mode') + elif cammode == 'picture': + cammode = 'film' + vumetermessage('changing to film mode') + camera.stop_preview() + camera.close() + camera = startcamera(lens,fps) + loadfilmsettings = True + elif menu[selected] == 'LENS:': + s = 0 + for a in lenses: + if a == lens: + selectlens = s + s += 1 + if selectlens > 0: + selectlens -= 1 + lens = os.listdir('lenses/')[selectlens] + #npzfile = np.load('lenses/' + lens) + #lensshade = npzfile['lens_shading_table'] + table = read_table('lenses/' + lens) + camera.lens_shading_table = table + elif menu[selected] == 'DUB:': + if round(dub[0],1) == 1.0 and round(dub[1],1) > 0.0: + dub[1] -= 0.1 + if round(dub[1],1) == 1.0 and round(dub[0],1) < 1.0: + dub[0] += 0.1 + elif menu[selected] == 'COMP:': + if comp > 0: + comp -= 1 + elif menu[selected] == 'HW:': + if plughw > 0: + plughw -= 1 + vumetermessage(getaudiocards()[plughw]) + elif menu[selected] == 'CH:': + if channels == 2: + channels = 1 + elif menu[selected] == 'FPS:': + if camera_model == 'imx477': + if fps_selected > 0: + fps_selected-=1 + fps_selection=[5,8,10,11,12,13,14,15,24.985,35,49] + fps=fps_selection[fps_selected] + camera.framerate = fps + elif menu[selected] == 'Q:': + if quality > 10: + quality -= 1 + elif menu[selected] == 'CAMERA:': + if camselected > 0: + newselected = camselected-1 + logger.info('camera selected:'+str(camselected)) + + #RIGHT + elif pressed == 'right': + if selected < len(menu) - 1: + selected = selected + 1 + else: + selected = 0 + if selected == 4: #jump over recording time + selected = 5 + #Start Recording Time + if recording == True: + t = time.time() - starttime + rectime = time.strftime("%H:%M:%S", time.gmtime(t)) + #Load settings + if loadfilmsettings == True: + db = get_film_files(filmname,filmfolder,db) + try: + filmsettings = loadsettings(filmfolder, filmname) + camera.brightness = filmsettings[2] + camera.contrast = filmsettings[3] + camera.saturation = filmsettings[4] + camera.shutter_speed = filmsettings[5] + camera.iso = filmsettings[6] + camera.awb_mode = filmsettings[7] + camera.awb_gains = filmsettings[8] + awb_lock = filmsettings[9] + miclevel = filmsettings[10] + headphoneslevel = filmsettings[11] + beeps = filmsettings[12] + flip = filmsettings[13] + comp = filmsettings[14] + between = filmsettings[15] + duration = filmsettings[16] + showmenu_settings = filmsettings[17] + quality = filmsettings[18] + #wifistate = filmsettings[19] + #serverstate=filmsettings[20] + plughw=filmsettings[21] + channels=filmsettings[22] + cammode=filmsettings[23] + scene=filmsettings[24] + shot=filmsettings[25] + take=filmsettings[26] + logger.info('film settings loaded & applied') + time.sleep(0.2) + except: + logger.warning('could not load film settings') + if flip == "yes": + camera.vflip = True + camera.hflip = True + run_command('amixer -c 0 sset Mic ' + str(miclevel) + '% unmute') + run_command('amixer -c 0 sset Speaker ' + str(headphoneslevel) + '%') + origin_videos=organize(filmfolder, filmname) + print('ORIGIN') + print(origin_videos) + print('total of videos: '+str(len(origin_videos))) + if not os.path.isdir(filmfolder+'.videos/'): + os.makedirs(filmfolder+'.videos/') + allfiles = os.listdir(filmfolder+'.videos/') + print(allfiles) + print('alll') + for origin in origin_videos: + if origin in allfiles: + try: + #os.remove(origin) + print('ORIGIN VIDEO FOLDER NOT IN SYNC' + origin) + time.sleep(5) + except: + print('not exist') + organize(filmfolder,'onthefloor') + scenes, shots, takes = countlast(filmname, filmfolder) + loadfilmsettings = False + rendermenu = True + updatethumb = True + if scene == 0: + scene = 1 + if take == 0: + take = 1 + if shot == 0: + shot = 1 + # If menu at SCENE show first shot thumbnail off that scene + if menu[selected] == 'FILM:' and lastmenu != menu[selected] and recordable == False: + updatethumb = True + if menu[selected] == 'SCENE:' and lastmenu != menu[selected] and recordable == False: + updatethumb = True + if menu[selected] == 'SHOT:' and lastmenu != menu[selected] and recordable == False: + updatethumb = True + if menu[selected] == 'TAKE:' and lastmenu != menu[selected] and recordable == False: + updatethumb = True + #Check if scene, shot, or take changed and update thumbnail + if oldscene != scene or oldshot != shot or oldtake != take or updatethumb == True: + if recording == False: + #logger.info('film:' + filmname + ' scene:' + str(scene) + '/' + str(scenes) + ' shot:' + str(shot) + '/' + str(shots) + ' take:' + str(take) + '/' + str(takes)) + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) +'/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + recordable = not os.path.isfile(foldername + filename + '.mp4') and not os.path.isfile(foldername + filename + '.h264') + overlay = removeimage(camera, overlay) + if menu[selected] == 'SCENE:' and recordable == False: # display first shot of scene if browsing scenes + p = counttakes(filmname, filmfolder, scene, 1) + imagename = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(1).zfill(3) + '/take' + str(p).zfill(3) + '.jpeg' + #elif menu[selected] == 'FILM:' and recordable == True: + # scene, shot, take = countlast(filmname,filmfolder) + # shot += 1 + elif menu[selected] == 'FILM:' and recordable == False: # display first shot of film + p = counttakes(filmname, filmfolder, 1, 1) + imagename = filmfolder + filmname + '/scene' + str(1).zfill(3) + '/shot' + str(1).zfill(3) + '/take' + str(p).zfill(3) + '.jpeg' + imagename = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(take).zfill(3) + '.jpeg' + overlay = displayimage(camera, imagename, overlay, 3) + oldscene = scene + oldshot = shot + oldtake = take + updatethumb = False + scenes = countscenes(filmfolder, filmname) + shots = countshots(filmname, filmfolder, scene) + takes = counttakes(filmname, filmfolder, scene, shot) + #If auto dont show value show auto (impovement here to show different colors in gui, yes!!?) + if camera.iso == 0: + cameraiso = 'auto' + else: + cameraiso = str(camera.iso) + if camera.shutter_speed == 0: + camerashutter = 'auto' + else: + camerashutter = str(camera.exposure_speed).zfill(5) + if camera.awb_mode == 'auto': + camerared = 'auto' + camerablue = 'auto' + else: + camerared = str(float(camera.awb_gains[0]))[:4] + camerablue = str(float(camera.awb_gains[1]))[:4] + + #Check if menu is changed and save settings / sec + if buttonpressed == True or recording == True or rendermenu == True: + lastmenu = menu[selected] + if showgonzopictrl == False: + menu = standardmenu + settings = filmname, str(scene) + '/' + str(scenes), str(shot) + '/' + str(shots), str(take) + '/' + str(takes), rectime, camerashutter, cameraiso, camerared, camerablue, str(round(camera.framerate)), str(quality), str(camera.brightness), str(camera.contrast), str(camera.saturation), str(flip), str(beeps), str(reclenght), str(plughw), str(channels), str(miclevel), str(headphoneslevel), str(comp), '', cammode, diskleft, '', serverstate, searchforcameras, wifistate, '', '', '', '', '', '', live + else: + #gonzopictrlmenu = 'FILM:', 'SCENE:', 'SHOT:', 'TAKE:', '', 'SHUTTER:', 'ISO:', 'RED:', 'BLUE:', 'FPS:', 'Q:', 'BRIGHT:', 'CONT:', 'SAT:', 'FLIP:', 'BEEP:', 'LENGTH:', 'HW:', 'CH:', 'MIC:', 'PHONES:', 'COMP:', 'TIMELAPSE', 'MODE:', 'DSK:', 'SHUTDOWN', 'SRV:', 'SEARCH:', 'WIFI:', 'CAMERA:', 'Add CAMERA', 'New FILM', 'Sync FILM', 'Sync SCENE' + menu = gonzopictrlmenu + #settings = '',str(camselected),'','',rectime,'','','','','','','','','','' + settings = filmname, str(scene) + '/' + str(scenes), str(shot) + '/' + str(shots), str(take) + '/' + str(takes), rectime, camerashutter, cameraiso, camerared, camerablue, str(round(camera.framerate)), str(quality), str(camera.brightness), str(camera.contrast), str(camera.saturation), str(flip), str(beeps), str(reclenght), str(plughw), str(channels), str(miclevel), str(headphoneslevel), str(comp), '', cammode, diskleft, '', serverstate, searchforcameras, wifistate, str(camselected), '', '', '', '', '', '' + #Rerender menu if picamera settings change + #if settings != oldsettings or selected != oldselected: + writemenu(menu,settings,selected,'',showmenu) + rendermenu = False + #save settings if menu has been updated and x seconds passed + if recording == False: + #if time.time() - pausetime > savesettingsevery: + if oldsettings != settings: + settings_to_save = [filmfolder, filmname, camera.brightness, camera.contrast, camera.saturation, camera.shutter_speed, camera.iso, camera.awb_mode, camera.awb_gains, awb_lock, miclevel, headphoneslevel, beeps, flip, comp, between, duration, showmenu_settings, quality,wifistate,serverstate,plughw,channels,cammode,scene,shot,take] + #print('saving settings') + savesettings(settings_to_save, filmname, filmfolder) + if time.time() - pausetime > savesettingsevery: + pausetime = time.time() + #NETWORKS + networks=[] + adapters = ifaddr.get_adapters() + for adapter in adapters: + print("IPs of network adapter " + adapter.nice_name) + for ip in adapter.ips: + if ':' not in ip.ip[0] and '127.0.0.1' != ip.ip: + print(ip.ip) + networks=[ip.ip] + if networks != []: + network=networks[0] + if network not in cameras: + cameras=[] + cameras.append(network) + else: + network='not connected' + if len(cameras) > 1: + camerasconnected='connected '+str(len(cameras)-1) + recordwithports=True + vumetermessage('filming with '+camera_model +' ip:'+ cameras[camselected] + ' '+camerasconnected+' camselected:'+str(camselected)+' rec:'+str(camera_recording)) + else: + camerasconnected='' + recordwithports=False + if searchforcameras == 'on': + camerasconnected='searching '+str(pingip) + vumetermessage('filming with '+camera_model +' ip:'+ network + ' '+camerasconnected) + disk = os.statvfs(gonzopifolder + '/') + diskleft = str(int(disk.f_bavail * disk.f_frsize / 1024 / 1024 / 1024)) + 'Gb' + #print(term.yellow+'filming with '+camera_model +' ip:'+ network + print(camselected,camera_recording,cameras) + #writemessage(pressed) + oldsettings = settings + oldselected = selected + #PING TARINAS + if searchforcameras == 'on': + if camera_recording == None: + if pingip < 256: + pingip+=1 + else: + pingip=0 + #searchforcameras='off' + newcamera=pingtocamera(network[:-3]+str(pingip),port,'PING') + if newcamera != '': + if newcamera not in cameras and newcamera not in networks: + cameras.append(newcamera) + vumetermessage("Found camera! "+newcamera) + print('-~-') + print('pinging ip: '+network[:-3]+str(pingip)) + else: + searchforcameras = 'off' + time.sleep(keydelay) + +#--------------Logger----------------------- + +class logger(): + def info(info): + print(term.yellow(info)) + def warning(warning): + print('Warning: ' + warning) + +#-------------get film db files--- + +def get_film_files(filmname,filmfolder,db): + if not os.path.isdir(filmfolder+'.videos/'): + os.makedirs(filmfolder+'.videos/') + filmdb = filmfolder+'.videos/gonzopi.db' + db = web.database(dbn='sqlite', db=filmdb) + try: + videodb=db.select('videos') + return db + except: + db.query("CREATE TABLE videos (id integer PRIMARY KEY, tid DATETIME, filename TEXT, foldername TEXT, filmname TEXT, scene INT, shot INT, take INT, audiolenght FLOAT, videolenght FLOAT,soundlag FLOAT, audiosync FLOAT);") + videodb=db.select('videos') + return db + +#--------------Save settings----------------- + +def savesettings(settings, filmname, filmfolder): + #db.insert('videos', tid=datetime.datetime.now()) + try: + with open(filmfolder + filmname + "/settings.p", "wb") as f: + pickle.dump(settings, f) + #logger.info("settings saved") + except: + logger.warning("could not save settings") + #logger.warning(e) + return + +#--------------Load film settings-------------- + +def loadsettings(filmfolder, filmname): + try: + settings = pickle.load(open(filmfolder + filmname + "/settings.p", "rb")) + logger.info("settings loaded") + return settings + except: + logger.info("couldnt load settings") + return '' + + +##---------------Connection---------------------------------------------- +def pingtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.05) + newcamera='' + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + newcamera=host + print("Sent to server..") + break + except: + print('did not connect') + s.close() + return newcamera + +##---------------Send to server---------------------------------------------- + +def sendtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(5) + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + print("Sent to server..") + break + except: + print('did not connect') + s.close() + +##---------------Send to server---------------------------------------------- + +def sendtoserver(host, port, data): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + while True: + print('sending data to '+host+':'+str(port)) + s.connect((host, port)) + s.send(str.encode(data)) + s.close() + break + except: + print('sometin rong') + +##--------------Listen for Clients----------------------- + +def listenforclients(host, port, q): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host,port)) + #s.settimeout(0.1) + try: + print("listening on port "+str(port)) + s.listen(5) + c, addr = s.accept() + while True: + data = c.recv(1024).decode() + if not data: + print("no data") + break + else: + if addr: + #print(addr[0],' sending back') + #sendtoserver(addr[0],port,'rebounce'+data) + nextstatus = data + print("got data:"+nextstatus) + c.close() + q.put(nextstatus+'*'+addr[0]) + break + except: + print("somthin wrong") + q.put('') + +#--------------Write the menu layer to dispmanx-------------- + +def writemenu(menu,settings,selected,header,showmenu): + global menudone, rendermenu + oldmenu=menudone + menudone = '' + menudoneprint = '' + menudone += str(selected) + '\n' + menudone += str(showmenu) + '\n' + menudone += header + '\n' + n = 0 + for i, s in zip(menu, settings): + menudone += i + s + '\n' + if n == selected: + menudoneprint += term.black_on_darkkhaki(i+s) + ' | ' + else: + menudoneprint += i + ' ' + s + ' | ' + n += 1 + spaces = len(menudone) - 500 + menudone += spaces * ' ' + if oldmenu != menudone or rendermenu == True: + print(term.clear+term.home) + if showmenu == 0: + print(term.red+menudoneprint) + else: + print(menudoneprint) + #menudone += 'EOF' + f = open('/dev/shm/interface', 'w') + f.write(menudone) + f.close() + return menudone + +#------------Write to screen---------------- + +def writemessage(message): + menudone = "" + menudone += '420' + '\n' + menudone += message + '\n' + #menudone += 'EOF' + #clear = 500 + #clear = clear - len(message) + f = open('/dev/shm/interface', 'w') + f.write(menudone) + f.close() + +#------------Write to vumeter (last line)----- + +def vumetermessage(message): + clear = 72 + clear = clear - len(message) + f = open('/dev/shm/vumeter', 'w') + f.write(message + clear * ' ') + f.close() + +#------------Count file size----- + +def countvideosize(filename): + size = 0 + if type(filename) is list: + size = 0 + for i in filename[:]: + size = size + os.stat(i + '.mp4').st_size + if type(filename) is str: + size = os.stat(filename + '.mp4').st_size + return size/1024 + +def countsize(filename): + size = 0 + if type(filename) is str: + size = os.stat(filename).st_size + return size/1024 + +#------------Count scenes, takes and shots----- + +def countlast(filmname, filmfolder): + scenes = 0 + shots = 0 + takes = 0 + try: + allfiles = os.listdir(filmfolder + filmname) + except: + allfiles = [] + scenes = 0 + for a in allfiles: + if 'scene' in a: + scenes = scenes + 1 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scenes).zfill(3)) + except: + allfiles = [] + shots = 0 + for a in allfiles: + if 'shot' in a: + shots = shots + 1 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scenes).zfill(3) + '/shot' + str(shots).zfill(3)) + except: + allfiles = [] + takes = 0 + for a in allfiles: + if '.mp4' in a or '.h264' in a: + takes = takes + 1 + return scenes, shots, takes + +#------------Count scenes-------- + +def countscenes(filmfolder, filmname): + scenes = 0 + try: + allfiles = os.listdir(filmfolder + filmname) + except: + allfiles = [] + scenes = 0 + for a in allfiles: + if 'scene' in a: + scenes = scenes + 1 + return scenes + +#------------Count shots-------- + +def countshots(filmname, filmfolder, scene): + shots = 0 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3)) + except: + allfiles = [] + shots = 0 + for a in allfiles: + if 'shot' in a: + shots = shots + 1 + return shots + +#------------Count takes-------- + +def counttakes(filmname, filmfolder, scene, shot): + takes = 0 + doubles = '' + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3)) + except: + allfiles = [] + return takes + for a in allfiles: + if '.mp4' in a or '.h264' in a: + if not doubles.replace('.h264', '.mp4') == a: + takes = takes + 1 + doubles = a + return takes + +#-----------Count videos on floor----- + +def countonfloor(filmname, filmfolder): + print('dsad') + + +#------------Run Command------------- + +def run_command(command_line): + #command_line_args = shlex.split(command_line) + logger.info('Running: "' + command_line + '"') + try: + p = subprocess.Popen(command_line, shell=True).wait() + # process_output is now a string, not a file, + # you may want to do: + except (OSError, CalledProcessError) as exception: + logger.warning('Exception occured: ' + str(exception)) + logger.warning('Process failed') + return False + else: + # no exception was raised + logger.info('Process finished') + return True + +#-------------Display bakg------------------- + +def displaybakg(camera, filename, underlay, layer): + # Load the arbitrarily sized image + img = Image.open(filename) + # Create an image padded to the required size with + # mode 'RGB' + pad = Image.new('RGB', ( + ((img.size[0] + 31) // 32) * 32, + ((img.size[1] + 15) // 16) * 16, + )) + # Paste the original image into the padded one + pad.paste(img, (0, 0)) + + # Add the overlay with the padded image as the source, + # but the original image's dimensions + underlay = camera.add_overlay(pad.tobytes(), size=img.size) + # By default, the overlay is in layer 0, beneath the + # preview (which defaults to layer 2). Here we make + # the new overlay semi-transparent, then move it above + # the preview + underlay.alpha = 255 + underlay.layer = layer + +#-------------Display jpeg------------------- + +def displayimage(camera, filename, overlay, layer): + # Load the arbitrarily sized image + try: + img = Image.open(filename) + except: + #writemessage('Seems like an empty shot. Hit record!') + overlay = removeimage(camera, overlay) + return overlay + camera.stop_preview() + # Create an image padded to the required size with + # mode 'RGB' + pad = Image.new('RGB', ( + ((img.size[0] + 31) // 32) * 32, + ((img.size[1] + 15) // 16) * 16, + )) + # Paste the original image into the padded one + pad.paste(img, (0, 0)) + + # Add the overlay with the padded image as the source, + # but the original image's dimensions + overlay = camera.add_overlay(pad.tobytes(), size=img.size) + # By default, the overlay is in layer 0, beneath the + # preview (which defaults to layer 2). Here we make + # the new overlay semi-transparent, then move it above + # the preview + overlay.alpha = 255 + overlay.layer = layer + return overlay + +def removeimage(camera, overlay): + if overlay: + try: + camera.remove_overlay(overlay) + overlay = None + camera.start_preview() + except: + pass + return overlay + + +#-------------Browse------------------ + +def browse(filmname, filmfolder, scene, shot, take): + scenes = countscenes(filmfolder, filmname) + shots = countshots(filmname, filmfolder, scene) + takes = counttakes(filmname, filmfolder, scene, shot) + return scenes, shots, takes + +#-------------Browse2.0------------------ + +def browse2(filmname, filmfolder, scene, shot, take, n, b): + scenes = countscenes(filmfolder, filmname) + shots = countshots(filmname, filmfolder, scene) + takes = counttakes(filmname, filmfolder, scene, shot) + #writemessage(str(scene) + ' < ' + str(scenes)) + #time.sleep(4) + selected = n + if selected == 0 and b == 1: + if scene < scenes + 1: #remove this if u want to select any scene + scene = scene + 1 + shot = countshots(filmname, filmfolder, scene) + take = counttakes(filmname, filmfolder, scene, shot) + #if take == 0: + #shot = shot - 1 + #take = counttakes(filmname, filmfolder, scene, shot - 1) + elif selected == 1 and b == 1: + if shot < shots + 1: #remove this if u want to select any shot + shot = shot + 1 + take = counttakes(filmname, filmfolder, scene, shot) + elif selected == 2 and b == 1: + if take < takes + 1: + take = take + 1 + elif selected == 0 and b == -1: + if scene > 1: + scene = scene - 1 + shot = countshots(filmname, filmfolder, scene) + take = counttakes(filmname, filmfolder, scene, shot) + #if take == 0: + # shot = shot - 1 + # take = counttakes(filmname, filmfolder, scene, shot - 1) + elif selected == 1 and b == -1: + if shot > 1: + shot = shot - 1 + take = counttakes(filmname, filmfolder, scene, shot) + elif selected == 2 and b == -1: + if take > 1: + take = take - 1 + return scene, shot, take + +#-------------Update------------------ + +def update(gonzopiversion, gonzopivername): + logger.info('Current version ' + gonzopiversion[:-1] + ' ' + gonzopivername[:-1]) + time.sleep(2) + logger.info('Checking for updates...') + try: + run_command('wget -N https://raw.githubusercontent.com/rbckman/gonzopi/master/VERSION -P /tmp/') + except: + logger.info('Sorry buddy, no internet connection') + time.sleep(2) + return gonzopiversion, gonzopivername + try: + f = open('/tmp/VERSION') + versionnumber = f.readline() + versionname = f.readline() + except: + logger.info('hmm.. something wrong with the update') + if round(float(gonzopiversion),3) < round(float(versionnumber),3): + logger.info('New version found ' + versionnumber[:-1] + ' ' + versionname[:-1]) + time.sleep(4) + logger.info('Updating...') + run_command('git -C ' + gonzopifolder + ' pull') + #run_command('sudo ' + gonzopifolder + '/install.sh') + logger.info('Update done, will now reboot Gonzopi') + waitforanykey() + logger.info('Hold on rebooting Gonzopi...') + run_command('sudo reboot') + logger.info('Version is up-to-date!') + return gonzopiversion, gonzopivername + +#-------------Get films--------------- + +def getfilms(filmfolder): + #get a list of films, in order of settings.p file last modified + films_sorted = [] + films = next(os.walk(filmfolder))[1] + for i in films: + if not '.videos' in i: + if os.path.isfile(filmfolder + i + '/' + 'settings.p') == True: + lastupdate = os.path.getmtime(filmfolder + i + '/' + 'settings.p') + films_sorted.append((i,lastupdate)) + else: + films_sorted.append((i,0)) + films_sorted = sorted(films_sorted, key=lambda tup: tup[1], reverse=True) + logger.info('*-- Films --*') + for p in films_sorted: + logger.info(p[0]) + return films_sorted + +#-------------Load gonzopi config--------------- + +def getconfig(camera): + filmfolder='' + version = camera.revision + home = os.path.expanduser('~') + configfile = home + '/.gonzopi/config.ini' + configdir = os.path.dirname(configfile) + if not os.path.isdir(configdir): + os.makedirs(configdir) + config = configparser.ConfigParser() + if config.read(configfile): + try: + camera_model = config['SENSOR']['model'] + except: + logger.info("couldnt read config") + try: + camera_revision = config['SENSOR']['revision'] + except: + logger.info("couldnt read config") + try: + filmfolder = config['USER']['filmfolder'] + return camera_model, camera_revision, filmfolder+'/' + except: + logger.info("couldnt read config") + if version == 'imx219': + config['SENSOR'] = {} + config['SENSOR']['model'] = version + config['SENSOR']['revision'] = 'standard' + with open(configfile, 'w') as f: + config.write(f) + camera_model = version + camera_revision = 'standard' + elif version == 'imx477': + config['SENSOR'] = {} + config['SENSOR']['model'] = version + config['SENSOR']['revision'] = 'hq-camera' + camera_model = version + camera_revision = 'hq-camera' + with open(configfile, 'w') as f: + config.write(f) + else: + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + selected = 0 + header = 'What revision of ' + version + ' sensor are you using?' + menu = 'rev.C', 'rev.D', 'hq-camera' + while True: + settings = '', '', '' + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle': + camera_model = version + camera_revision = menu[selected] + config['SENSOR'] = {} + config['SENSOR']['model'] = camera_model + config['SENSOR']['revision'] = camera_revision + with open(configfile, 'w') as f: + config.write(f) + time.sleep(0.02) + + if filmfolder != '': + return version, camera_revision, filmfolder+'/' + else: + filmfolder = namesomething('Your film folder: ', home+'/Videos') + config['USER'] = {} + config['USER']['filmfolder'] = filmfolder + with open(configfile, 'w') as f: + config.write(f) + return camera_model, camera_revision, filmfolder+'/' + +#-------------Calc folder size with du----------- + +def du(path): + """disk usage in human readable format (e.g. '2,1GB')""" + return subprocess.check_output(['du','-sh', path]).split()[0].decode('utf-8') + + +#------------Clean up---------------- + +def cleanupdisk(filmname, filmfolder): + alloriginfiles=[] + films = getfilms(filmfolder) + for f in films: + alloriginfiles.extend(organize(filmfolder,f[0])) + print(alloriginfiles) + filesinfolder = next(os.walk(filmfolder+'.videos/'))[2] + filesfolder=[] + for i in filesinfolder: + filesfolder.append(filmfolder+'.videos/'+i) + print(filesfolder) + for i in alloriginfiles: + if i in filesfolder: + print("YES, found link to origin") + else: + print("NOPE, no link to origin") + print(i) + time.sleep(2) + #os.system('rm ' + i) + #for i in filesfolder: + # if i in alloriginfiles: + # print("YES, found link to origin") + # else: + # print("NOPE, no link to origin") + # print(i) + # os.system('rm ' + i) + +#-------------Load film--------------- + +def loadfilm(filmname, filmfolder): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + films = getfilms(filmfolder) + filmsize=[] + for f in films: + filmsize.append(du(filmfolder+f[0])) + filmstotal = len(films[1:]) + selectedfilm = 0 + selected = 0 + header = 'Up and down to select and load film' + menu = 'FILM:', 'BACK' + while True: + settings = films[selectedfilm][0], '' + writemenu(menu,settings,selected,header,showmenu) + vumetermessage('filmsize: '+filmsize[selectedfilm]+' date: '+time.strftime('%Y-%m-%d %H:%M:%S',time.gmtime(films[selectedfilm][1]))) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'down': + if selectedfilm < filmstotal: + selectedfilm = selectedfilm + 1 + elif pressed == 'up': + if selectedfilm > 0: + selectedfilm = selectedfilm - 1 + elif pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle' and menu[selected] == 'FILM:': + filmname = films[selectedfilm][0] + return filmname + elif pressed == 'middle' and menu[selected] == 'BACK': + writemessage('Returning') + return filmname + time.sleep(0.02) + + +#---------Name anything really----------- + +def namesomething(what, readymadeinput): + global abc + anything = readymadeinput + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + abcx = 0 + helpmessage = 'Up, Down (select characters) Right (next). Middle (done)' + cursor = '_' + blinking = True + pausetime = time.time() + while True: + message = what + anything + print(term.clear+term.home) + print(message+cursor) + writemessage(message + cursor) + vumetermessage(helpmessage) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if event == ' ': + event = '_' + if pressed == 'down': + pausetime = time.time() + if abcx < (len(abc) - 1): + abcx = abcx + 1 + cursor = abc[abcx] + elif pressed == 'up': + pausetime = time.time() + if abcx > 0: + abcx = abcx - 1 + cursor = abc[abcx] + elif pressed == 'right': + pausetime = time.time() + if len(anything) < 30: + anything = anything + abc[abcx] + cursor = abc[abcx] + else: + helpmessage = 'Yo, maximum characters reached bro!' + elif pressed == 'left' or pressed == 'remove': + pausetime = time.time() + if len(anything) > 0: + anything = anything[:-1] + cursor = abc[abcx] + elif pressed == 'middle' or event == 10: + if len(anything) > 0: + if abc[abcx] != '_': + anything = anything + abc[abcx] + return anything + elif event in abc: + pausetime = time.time() + anything = anything + event + if time.time() - pausetime > 0.5: + if blinking == True: + cursor = abc[abcx] + if blinking == False: + cursor = ' ' + blinking = not blinking + pausetime = time.time() + time.sleep(keydelay) + + +#-------------New film---------------- + +def nameyourfilm(filmfolder, filmname, abc, newfilm): + oldfilmname = filmname + if newfilm == True: + filmname = '' + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + abcx = 0 + helpmessage = 'Left (remove), Up, Down (select characters) Right (next). Middle (done), Retake (Cancel)' + vumetermessage('Press enter if you want to leave it untitled') + cursor = '_' + blinking = True + pausetime = time.time() + while True: + if newfilm == True: + message = 'New film name: ' + filmname + else: + message = 'Edit film name: ' + filmname + print(term.clear+term.home) + print(message+cursor) + print(helpmessage) + writemessage(message + cursor) + vumetermessage(helpmessage) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if event == ' ': + event = '_' + if pressed == 'down': + pausetime = time.time() + if abcx < (len(abc) - 1): + abcx = abcx + 1 + cursor = abc[abcx] + elif pressed == 'up': + pausetime = time.time() + if abcx > 0: + abcx = abcx - 1 + cursor = abc[abcx] + elif pressed == 'right': + pausetime = time.time() + if len(filmname) < 30: + filmname = filmname + abc[abcx] + cursor = abc[abcx] + else: + helpmessage = 'Yo, maximum characters reached bro!' + elif pressed == 'left' or pressed == 'remove': + pausetime = time.time() + if len(filmname) > 0: + filmname = filmname[:-1] + cursor = abc[abcx] + elif pressed == 'middle' or event == 10: + if filmname == '': + filmname='untitledfilm' + if len(filmname) > 0: + if abc[abcx] != '_': + filmname = filmname + abc[abcx] + try: + if filmname == oldfilmname: + return oldfilmname + elif filmname in getfilms(filmfolder)[0]: + helpmessage = 'this filmname is already taken! make a sequel!' + filmname = filmname+'2' + elif filmname not in getfilms(filmfolder)[0]: + logger.info("New film " + filmname) + return(filmname) + except: + logger.info("New film " + filmname) + return(filmname) + elif pressed == 'retake': + return oldfilmname + elif event in abc: + pausetime = time.time() + filmname = filmname + event + if time.time() - pausetime > 0.5: + if blinking == True: + cursor = abc[abcx] + if blinking == False: + cursor = ' ' + blinking = not blinking + pausetime = time.time() + time.sleep(keydelay) + +#-------------New camera---------------- + +def newcamera_ip(abc, network): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + abcx = 0 + helpmessage = 'Up, Down (select characters) Right (next). Middle (done), Retake (Cancel)' + cursor = '_' + blinking = True + pausetime = time.time() + ip_network = network.split('.')[:-1] + ip_network = '.'.join(ip_network)+'.' + ip = '' + while True: + message = 'Camera ip: ' + ip_network + ip + print(term.clear+term.home) + print(message+cursor) + writemessage(message + cursor) + vumetermessage(helpmessage) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if event == ' ': + event = '_' + if pressed == 'down': + pausetime = time.time() + if abcx < (len(abc) - 1): + abcx = abcx + 1 + cursor = abc[abcx] + elif pressed == 'up': + pausetime = time.time() + if abcx > 0: + abcx = abcx - 1 + cursor = abc[abcx] + elif pressed == 'right': + pausetime = time.time() + if len(ip) < 2: + ip = ip + abc[abcx] + cursor = abc[abcx] + else: + helpmessage = 'Yo, maximum ip reached bro!' + elif pressed == 'left' or pressed == 'remove': + pausetime = time.time() + if len(ip) > 0: + ip = ip[:-1] + cursor = abc[abcx] + elif pressed == 'middle' or event == 10: + if abc[abcx] != ' ' or ip != '': + ip = ip + abc[abcx] + if int(ip) < 256: + logger.info("New camera " + ip_network+ip) + return (ip_network+ip).strip() + else: + helpmessage = 'in the range of ips 1-256' + elif pressed == 'retake': + return '' + elif event in abc: + pausetime = time.time() + ip = ip + event + if time.time() - pausetime > 0.5: + if blinking == True: + cursor = abc[abcx] + if blinking == False: + cursor = ' ' + blinking = not blinking + pausetime = time.time() + time.sleep(keydelay) + +#------------Timelapse-------------------------- + +def timelapse(beeps,camera,filmname,foldername,filename,between,duration,backlight): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + sound = False + selected = 0 + header = 'Adjust delay in seconds between videos' + menu = 'DELAY:', 'DURATION:', 'SOUND:', 'START', 'BACK' + while True: + settings = str(round(between,2)), str(round(duration,2)), str(sound), '', '' + writemenu(menu,settings,selected,header,showmenu) + seconds = (3600 / between) * duration + vumetermessage('1 h timelapse filming equals ' + str(round(seconds,2)) + ' second clip ') + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'up' and menu[selected] == 'DELAY:': + between = between + 1 + elif pressed == 'down' and menu[selected] == 'DELAY:': + if between > 1: + between = between - 1 + if pressed == 'up' and menu[selected] == 'SOUND:': + sound = True + elif pressed == 'down' and menu[selected] == 'SOUND:': + sound = False + elif pressed == 'up' and menu[selected] == 'DURATION:': + duration = duration + 0.1 + elif pressed == 'down' and menu[selected] == 'DURATION:': + if duration > 0.3: + duration = duration - 0.1 + elif pressed == 'up' or pressed == 'down' and menu[selected] == 'SOUND:': + if sound == False: + sound == True + if sound == True: + sound == False + elif pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle': + if menu[selected] == 'START': + if os.path.isdir(foldername+'timelapse') == False: + os.makedirs(foldername + 'timelapse') + time.sleep(0.02) + writemessage('Recording timelapse, middlebutton to stop') + n = 1 + recording = False + starttime = time.time() + t = 0 + files = [] + while True: + t = time.time() - starttime + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + vumetermessage('Timelapse lenght is now ' + str(round(n * duration,2)) + ' second clip ') + if recording == False and t > between: + if beeps > 0: + if bus: + buzz(150) + else: + run_command('aplay -D plughw:' + str(plughw) + ' '+ gonzopifolder + '/extras/beep.wav') + #camera.start_recording(foldername + 'timelapse/' + filename + '_' + str(n).zfill(3) + '.h264', format='h264', quality=26, bitrate=5000000) + camera.start_recording(foldername + 'timelapse/' + filename + '_' + str(n).zfill(3) + '.h264', format='h264', quality=quality, level=profilelevel) + if sound == True: + os.system(gonzopifolder+'/alsa-utils-1.1.3/aplay/arecord -D hw:'+str(plughw)+' -f '+soundformat+' -c '+str(channels)+' -r '+soundrate+' -vv '+foldername+'timelapse/'+filename+'_'+str(n).zfill(3)+'.wav &') + files.append(foldername + 'timelapse/' + filename + '_' + str(n).zfill(3)) + starttime = time.time() + recording = True + n = n + 1 + t = 0 + if recording == True: + writemessage('Recording timelapse ' + str(n) + ' ' + 'time:' + str(round(t,2))) + if recording == False: + writemessage('Between timelapse ' + str(n) + ' ' + 'time:' + str(round(t,2))) + if t > duration and recording == True: + if sound == True: + os.system('pkill arecord') + camera.stop_recording() + recording = False + starttime = time.time() + t = 0 + if pressed == 'screen': + if backlight == False: + # requires wiringpi installed + run_command('gpio -g pwm 19 1023') + backlight = True + elif backlight == True: + run_command('gpio -g pwm 19 0') + backlight = False + elif pressed == 'middle' and n > 1: + if recording == True: + os.system('pkill arecord') + camera.stop_recording() + #create thumbnail + try: + camera.capture(foldername + filename + '.jpeg', resize=(800,450), use_video_port=True) + except: + logger.warning('something wrong with camera jpeg capture') + writemessage('Compiling timelapse') + logger.info('Hold on, rendering ' + str(len(files)) + ' scenes') + #RENDER VIDEO + renderfilename = foldername + filename + n = 1 + videomerge = ['MP4Box'] + videomerge.append('-force-cat') + for f in files: + if sound == True: + compileshot(f,filmfolder,filmname) + audiotrim(foldername + 'timelapse/' + filename + '_' + str(n).zfill(3), 'end', '') + videomerge.append('-cat') + videomerge.append(f + '.mp4') + else: + videomerge.append('-cat') + videomerge.append(f + '.h264') + n = n + 1 + videomerge.append('-new') + videomerge.append(renderfilename + '.mp4') + call(videomerge, shell=False) #how to insert somekind of estimated time while it does this? + ##RENDER AUDIO + if sound == True: + writemessage('Rendering sound') + audiomerge = ['sox'] + #if render > 2: + # audiomerge.append(filename + '.wav') + for f in files: + audiomerge.append(f + '.wav') + audiomerge.append(renderfilename + '.wav') + call(audiomerge, shell=False) + ##MAKE AUDIO SILENCE + if sound == False: + audiosilence(foldername,filename) + #cleanup + #os.system('rm -r ' + foldername + 'timelapse') + vumetermessage('timelapse done! ;)') + return renderfilename, between, duration + time.sleep(keydelay) + if menu[selected] == 'BACK': + vumetermessage('ok!') + return '', between, duration + time.sleep(keydelay) + +#------------Remove----------------------- + +def remove(filmfolder, filmname, scene, shot, take, sceneshotortake): + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(take).zfill(3) + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + time.sleep(0.1) + header = 'Are you sure you want to remove ' + sceneshotortake + '?' + menu = '', '' + settings = 'NO', 'YES' + selected = 0 + otf_scene = countscenes(filmfolder, 'onthefloor') + otf_scene += 1 + otf_shot = countshots('onthefloor', filmfolder, otf_scene) + otf_shot += 1 + otf_take = counttakes('onthefloor', filmfolder, otf_scene, otf_shot) + otf_take += 1 + while True: + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle': + if selected == 1: + if filmname == 'onthefloor': + if sceneshotortake == 'take': + os.system('rm ' + foldername + filename + '.h264') + os.system('rm ' + foldername + filename + '.mp4') + os.system('rm ' + foldername + filename + '.wav') + os.system('rm ' + foldername + filename + '.jpeg') + elif sceneshotortake == 'shot' and shot > 0: + os.system('rm -r ' + foldername) + elif sceneshotortake == 'scene': + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + os.system('rm -r ' + foldername) + scene = countscenes(filmfolder, filmname) + shot=1 + take=1 + elif sceneshotortake == 'film': + foldername = filmfolder + filmname + os.system('rm -r ' + foldername) + os.makedirs(filmfolder+'onthefloor') + return + else: + if sceneshotortake == 'take': + writemessage('Throwing take on the floor' + str(take)) + onthefloor = filmfolder + 'onthefloor/' + 'scene' + str(otf_scene).zfill(3) + '/shot' + str(otf_shot).zfill(3) + '/take' + str(otf_take).zfill(3) + onthefloor_folder = filmfolder + 'onthefloor/' + 'scene' + str(otf_scene).zfill(3) + '/shot' + str(otf_shot).zfill(3) + '/' + if os.path.isdir(onthefloor_folder) == False: + os.makedirs(onthefloor) + os.system('mv ' + foldername + filename + '.h264 ' + onthefloor + '.h264') + os.system('mv ' + foldername + filename + '.mp4 ' + onthefloor + '.mp4') + os.system('mv ' + foldername + filename + '.wav ' + onthefloor + '.wav') + os.system('mv ' + foldername + filename + '.jpeg ' + onthefloor + '.jpeg') + take = take - 1 + if take == 0: + take = 1 + elif sceneshotortake == 'shot' and shot > 0: + writemessage('Throwing shot on the floor' + str(shot)) + onthefloor = filmfolder + 'onthefloor/' + 'scene' + str(otf_scene).zfill(3) + '/shot' + str(otf_shot).zfill(3)+'/' + os.makedirs(onthefloor) + os.system('cp -r '+foldername+'* '+onthefloor) + os.system('rm -r '+foldername) + take = counttakes(filmname, filmfolder, scene, shot) + elif sceneshotortake == 'scene': + onthefloor = filmfolder + 'onthefloor/' + 'scene' + str(otf_scene).zfill(3) + os.makedirs(onthefloor) + writemessage('Throwing clips on the floor ' + str(scene)) + foldername = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + os.system('mv ' + foldername + '/* ' + onthefloor+'/' ) + scene = countscenes(filmfolder, filmname) + shot = 1 + take = 1 + elif sceneshotortake == 'film': + foldername = filmfolder + filmname + os.system('rm -r ' + foldername) + organize(filmfolder, 'onthefloor') + return + elif selected == 0: + return + time.sleep(0.02) + +#------------Remove and Organize---------------- + +def organize(filmfolder, filmname): + global fps, db + origin_files=[] + scenes = next(os.walk(filmfolder + filmname))[1] + for i in scenes: + if 'scene' not in i: + scenes.remove(i) + # Takes + for i in sorted(scenes): + origin_scene_files=[] + shots = next(os.walk(filmfolder + filmname + '/' + i))[1] + for p in sorted(shots): + takes = next(os.walk(filmfolder + filmname + '/' + i + '/' + p))[2] + if len(takes) == 0: + logger.info('no takes in this shot, removing shot if no placeholder') + if not os.path.isfile(filmfolder + filmname + '/' + i + '/' + p + '/.placeholder'): + os.system('rm -r ' + filmfolder + filmname + '/' + i + '/' + p) + organized_nr = 1 + print(i) + print(p) + print(sorted(takes)) + #time.sleep(2) + for s in sorted(takes): + if '.mp4' in s or '.h264' in s: + unorganized_nr = int(s[4:7]) + takename = filmfolder + filmname + '/' + i + '/' + p + '/take' + str(unorganized_nr).zfill(3) + if '.mp4' in s: + origin=os.path.realpath(takename+'.mp4') + if origin != os.path.abspath(takename+'.mp4'): + print('appending: '+origin) + origin_files.append(origin) + origin_scene_files.append(origin) + if os.path.isfile(takename+'.h264'): + print('oh no boubles found!') + if '.h264' in s: + origin=os.path.realpath(takename+'.h264') + if origin != os.path.abspath(takename+'.h264'): + origin_files.append(origin) + origin_scene_files.append(origin) + if organized_nr == unorganized_nr: + #print('correct') + pass + if organized_nr != unorganized_nr: + print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + print(s) + #time.sleep(3) + mv = 'mv ' + filmfolder + filmname + '/' + i + '/' + p + '/take' + str(unorganized_nr).zfill(3) + run_command(mv + '.mp4 ' + filmfolder + filmname + '/' + i + '/' + p + '/take' + str(organized_nr).zfill(3) + '.mp4') + run_command(mv + '.h264 ' + filmfolder + filmname + '/' + i + '/' + p + '/take' + str(organized_nr).zfill(3) + '.h264') + run_command(mv + '.wav ' + filmfolder + filmname + '/' + i + '/' + p + '/take' + str(organized_nr).zfill(3) + '.wav') + run_command(mv + '.jpeg ' + filmfolder + filmname + '/' + i + '/' + p + '/take' + str(organized_nr).zfill(3) + '.jpeg') + #check if same video has both h246 and mp4 and render and remove h264 + for t in sorted(takes): + if t.replace('.mp4','') == s.replace('.h264','') or s.replace('.mp4','') == t.replace('.h264',''): + logger.info('Found both mp4 and h264 of same video!') + logger.info(t) + logger.info(s) + #time.sleep(5) + compileshot(takename,filmfolder,filmname) + organized_nr -= 1 + organized_nr += 1 + origin_files.extend(origin_scene_files) + with open(filmfolder+filmname+'/'+i+'/.origin_videos', 'w') as outfile: + outfile.write('\n'.join(str(i) for i in origin_scene_files)) + + # Shots + for i in sorted(scenes): + shots = next(os.walk(filmfolder + filmname + '/' + i))[1] + if len(shots) == 0: + logger.info('no shots in this scene, removing scene..') + os.system('rm -r ' + filmfolder + filmname + '/' + i) + organized_nr = 1 + for p in sorted(shots): + if 'insert' in p: + #add_organize(filmfolder, filmname) + pass + elif 'shot' in p: + #print(p) + unorganized_nr = int(p[-3:]) + if organized_nr == unorganized_nr: + #print('correct') + pass + if organized_nr != unorganized_nr: + #print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + os.system('mv ' + filmfolder + filmname + '/' + i + '/shot' + str(unorganized_nr).zfill(3) + ' ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr).zfill(3)) + organized_nr += 1 + + # Scenes + organized_nr = 1 + for i in sorted(scenes): + if 'insert' in i: + #add_organize(filmfolder, filmname) + pass + elif 'scene' in i: + #print(i) + unorganized_nr = int(i[-3:]) + if organized_nr == unorganized_nr: + #print('correct') + pass + if organized_nr != unorganized_nr: + #print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + os.system('mv ' + filmfolder + filmname + '/scene' + str(unorganized_nr).zfill(3) + ' ' + filmfolder + filmname + '/scene' + str(organized_nr).zfill(3)) + organized_nr += 1 + + logger.info('Organizer done! Everything is tidy') + return origin_files + + +#------------Add and Organize---------------- + +def add_organize(filmfolder, filmname): + scenes = next(os.walk(filmfolder + filmname))[1] + for i in scenes: + if 'scene' not in i: + scenes.remove(i) + # Shots + for i in sorted(scenes): + shots = next(os.walk(filmfolder + filmname + '/' + i))[1] + for c in shots: + if 'shot' not in c: + shots.remove(c) + organized_nr = len(shots) + for p in sorted(shots, reverse=True): + if 'yanked' in p: + #print(p) + os.system('mv -n ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr - 1).zfill(3) + '_yanked ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr).zfill(3)) + elif 'insert' in p: + os.system('mv -n ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr - 1).zfill(3) + '_insert ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr).zfill(3)) + run_command('touch ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr).zfill(3) + '/.placeholder') + elif 'shot' in p: + #print(p) + unorganized_nr = int(p[-3:]) + if organized_nr == unorganized_nr: + #print('correct') + pass + if organized_nr != unorganized_nr: + #print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + os.system('mv -n ' + filmfolder + filmname + '/' + i + '/shot' + str(unorganized_nr).zfill(3) + ' ' + filmfolder + filmname + '/' + i + '/shot' + str(organized_nr).zfill(3)) + organized_nr -= 1 + + # Scenes + organized_nr = len(scenes) + for i in sorted(scenes, reverse=True): + #print(i) + if 'yanked' in i: + os.system('mv -n ' + filmfolder + filmname + '/scene' + str(organized_nr - 1).zfill(3) + '_yanked ' + filmfolder + filmname + '/scene' + str(organized_nr).zfill(3)) + elif 'insert' in i: + #print(p) + os.system('mv -n ' + filmfolder + filmname + '/scene' + str(organized_nr - 1).zfill(3) + '_insert ' + filmfolder + filmname + '/scene' + str(organized_nr).zfill(3)) + run_command('touch ' + filmfolder + filmname + '/scene' + str(organized_nr).zfill(3) + '/.placeholder') + elif 'scene' in i: + #print(i) + unorganized_nr = int(i[-3:]) + if organized_nr == unorganized_nr: + #print('correct') + pass + if organized_nr != unorganized_nr: + #print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + os.system('mv -n ' + filmfolder + filmname + '/scene' + str(unorganized_nr).zfill(3) + ' ' + filmfolder + filmname + '/scene' + str(organized_nr).zfill(3)) + organized_nr -= 1 + return + + +#-------------Stretch Audio-------------- + +def stretchaudio(filename,fps): + fps_rounded=round(fps) + if int(fps_rounded) != 25: + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + filename + '.mp4', shell=True) + videolenght = pipe.decode().strip() + try: + pipe = subprocess.check_output('mediainfo --Inform="Audio;%Duration%" ' + filename + '.wav', shell=True) + audiolenght = pipe.decode().strip() + except: + audiosilence('',filename) + audiolenght=videolenght + #if there is no audio lenght + logger.info('audio is:' + audiolenght) + if not audiolenght.strip(): + audiolenght = 0 + ratio = int(audiolenght)/int(videolenght) + print(str(ratio)) + run_command('cp '+filename+'.wav '+filename+'_temp.wav') + run_command('ffmpeg -y -i ' + filename + '_temp.wav -filter:a atempo="'+str(ratio) + '" ' + filename + '.wav') + os.remove(filename + '_temp.wav') + #time.sleep(5) + return + +#-------------Compile Shot-------------- + +def compileshot(filename,filmfolder,filmname): + global fps, soundrate, channels + videolenght=0 + audiolenght=0 + #Check if file already converted + if '.h264' in filename: + filename=filename.replace('.h264','') + if '.mp4' in filename: + filename=filename.replace('.mp4','') + if os.path.isfile(filename + '.h264'): + logger.info('Video not converted!') + writemessage('Converting to playable video') + #remove old mp4 if corrupted like if an unpredicted shutdown in middle of converting + video_origins = (os.path.realpath(filename+'.h264'))[:-5] + os.system('rm ' + filename + '.mp4') + os.system('rm ' + video_origins + '.mp4') + print(filename+'.mp4 removed!') + run_command('MP4Box -fps 25 -add ' + video_origins + '.h264 ' + video_origins + '.mp4') + os.system('ln -sf '+video_origins+'.mp4 '+filename+'.mp4') + if not os.path.isfile(filename + '.wav'): + audiosilence('',filename) + #add audio/video start delay sync + run_command('sox -V0 '+filename+'.wav -c 2 /dev/shm/temp.wav trim 0.013') + run_command('mv /dev/shm/temp.wav '+ filename + '.wav') + stretchaudio(filename,fps) + audiosync, videolenght, audiolenght = audiotrim(filename, 'end','') + muxing = False + if muxing == True: + #muxing mp3 layer to mp4 file + #count estimated audio filesize with a bitrate of 320 kb/s + audiosize = countsize(filename + '.wav') * 0.453 + p = Popen(['ffmpeg', '-y', '-i', filename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', filename + '.mp3']) + while p.poll() is None: + time.sleep(0.2) + try: + rendersize = countsize(filename + '.mp3') + except: + continue + writemessage('audio rendering ' + str(int(rendersize)) + ' of ' + str(int(audiosize)) + ' kb done') + ##MERGE AUDIO & VIDEO + writemessage('Merging audio & video') + #os.remove(renderfilename + '.mp4') + call(['MP4Box', '-rem', '2', video_origins + '.mp4'], shell=False) + call(['MP4Box', '-fps', '25', '-add', video_origins + '.mp4', '-add', filename + '.mp3', '-new', video_origins + '_tmp.mp4'], shell=False) + os.system('cp -f ' + video_origins + '_tmp.mp4 ' + video_origins + '.mp4') + os.remove(video_origins + '_tmp.mp4') + os.remove(filename + '.mp3') + origin=os.path.realpath(filename+'.mp4') + db.update('videos', where='filename="'+origin+'"', videolenght=videolenght/1000, audiolenght=audiolenght/1000, audiosync=audiosync) + os.system('rm ' + video_origins + '.h264') + os.system('rm ' + filename + '.h264') + os.system('rm /dev/shm/temp.wav') + os.system('ln -sf '+video_origins+'.mp4 '+filename+'.mp4') + logger.info('compile done!') + #run_command('omxplayer --layer 3 ' + filmfolder + '/.rendered/' + filename + '.mp4 &') + #time.sleep(0.8) + #run_command('aplay ' + foldername + filename + '.wav') + return + +#-------------Get shot files-------------- + +def shotfiles(filmfolder, filmname, scene): + files = [] + shots = countshots(filmname,filmfolder,scene) + print("shots"+str(shots)) + shot = 1 + for i in range(shots): + takes = counttakes(filmname,filmfolder,scene,shot) + if takes > 0: + folder = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/' + filename = 'take' + str(takes).zfill(3) + files.append(folder + filename) + print(folder+filename) + shot = shot + 1 + #writemessage(str(len(shotfiles))) + #time.sleep(2) + return files + +#--------Show JPEG as progress when rendering + +#---------------Render Video------------------ + +def rendervideo(filmfiles, filename, renderinfo): + if len(filmfiles) < 1: + writemessage('Nothing here!') + time.sleep(2) + return None + print('Rendering videofiles') + writemessage('Hold on, rendering ' + renderinfo + ' with ' + str(len(filmfiles)) + ' files') + videosize = 0 + rendersize = 0 + videomerge = ['MP4Box'] + videomerge.append('-force-cat') + for f in filmfiles[:]: + videosize = videosize + countsize(f + '.mp4') + videomerge.append('-cat') + videomerge.append(f + '.mp4#video') + videomerge.append('-new') + videomerge.append(filename + '.mp4') + #videomerge.append(filename + '.h264') + #call(videomerge, shell=True) #how to insert somekind of estimated time while it does this? + p = Popen(videomerge) + #show progress + while p.poll() is None: + time.sleep(0.1) + try: + rendersize = countsize(filename + '.mp4') + except: + continue + writemessage('video rendering ' + str(int(rendersize)) + ' of ' + str(int(videosize)) + ' kb done') + print('Video rendered!') + return + +#---------------Render Audio---------------- + +def renderaudio(audiofiles, filename, dubfiles, dubmix): + #if len(audiofiles) < 1: + # writemessage('Nothing here!') + # time.sleep(2) + # return None + print('Rendering audiofiles') + ##PASTE AUDIO TOGETHER + writemessage('Hold on, rendering audio...') + audiomerge = ['sox'] + #if render > 2: + # audiomerge.append(filename + '.wav') + if isinstance(audiofiles, list): + for f in audiofiles: + audiomerge.append(f + '.wav') + audiomerge.append(filename + '.wav') + call(audiomerge, shell=False) + else: + #if rendering scene with one shot + if audiofiles[0] != filename: + os.system('cp '+audiofiles[0]+'.wav '+filename+'.wav') + #DUBBING + p = 1 + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + filename + '.mp4', shell=True) + videolenght = pipe.decode().strip() + audiolenght=videolenght + for i, d in zip(dubmix, dubfiles): + writemessage('Dub ' + str(p) + ' audio found lets mix...') + #first trimit! + audiotrim(filename, 'end', d) + try: + pipe = subprocess.check_output('soxi -D ' + d, shell=True) + dubaudiolenght = pipe.decode() + if dubaudiolengt != videolenght: + print('dub wrong lenght!') + time.sleep(5) + except: + pass + os.system('cp ' + filename + '.wav ' + filename + '_tmp.wav') + #Fade and make stereo + run_command('sox -V0 -G ' + d + ' /dev/shm/fade.wav fade ' + str(round(i[2],1)) + ' 0 ' + str(round(i[3],1))) + run_command('sox -V0 -G -m -v ' + str(round(i[0],1)) + ' /dev/shm/fade.wav -v ' + str(round(i[1],1)) + ' ' + filename + '_tmp.wav ' + filename + '.wav trim 0 ' + audiolenght) + os.remove(filename + '_tmp.wav') + os.remove('/dev/shm/fade.wav') + print('Dub mix ' + str(p) + ' done!') + p += 1 + return + +#-------------Fast Edit----------------- +def fastedit(filmfolder, filmname, filmfiles, scene): + scenedir = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/' + totlenght = 0 + try: + os.remove(scenedir + '.fastedit') + except: + print('no fastedit file') + for f in filmfiles: + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + f + '.mp4', shell=True) + videolenght = pipe.decode().strip() + totlenght = int(videolenght) + totlenght + print('writing shot lenghts for fastedit mode') + with open(scenedir + '.fastedit', 'a') as f: + f.write(str(totlenght)+'\n') + + +#-------------Get scene files-------------- + +def scenefiles(filmfolder, filmname): + files = [] + scenes = countscenes(filmfolder,filmname) + scene = 1 + while scene <= scenes: + folder = filmfolder + filmname + '/' + 'scene' + str(scene).zfill(3) + '/' + filename = 'scene' + files.append(folder + filename) + scene = scene + 1 + #writemessage(str(len(shotfiles))) + #time.sleep(2) + return files + +#-------------Render Shot------------- + +def rendershot(filmfolder, filmname, renderfilename, scene, shot): + global fps + #This function checks and calls rendervideo & renderaudio if something has changed in the film + #Video + videohash = '' + oldvideohash = '' + #take = counttakes(filmname, filmfolder, scene, shot) + #renderfilename = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(take).zfill(3) + #return if no file + # Video Hash + #if something shutdown in middle of process + if os.path.isfile(renderfilename + '_tmp.mp4') == True: + os.system('cp ' + renderfilename + '_tmp.mp4 ' + renderfilename + '.mp4') + if os.path.isfile(renderfilename + '.h264') == True: + compileshot(renderfilename,filmfolder,filmname) + if os.path.isfile(renderfilename + '.mp4') == True: + videohash = videohash + str(int(countsize(renderfilename + '.mp4'))) + print('Videohash of shot is: ' + videohash) + else: + vumetermessage('Nothing here to play hit record') + return '', '' + #if os.path.isfile(renderfilename + '.h264') and os.path.isfile(renderfilename + '.mp4'): + # os.system('rm ' + renderfilename + '.h264 ') + scenedir = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/' + # Check if video corrupt + renderfix = False + if os.path.isfile(renderfilename + '.jpeg') == False: + run_command('ffmpeg -sseof -1 -i ' + renderfilename + '.mp4 -update 1 -q:v 1 -vf scale=800:450 ' + renderfilename + '.jpeg') + try: + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + renderfilename + '.mp4', shell=True) + videolenght = pipe.decode().strip() + except: + videolenght = '' + print('Shot lenght ' + videolenght) + if videolenght == '': + print('Okey, shot file not found or is corrupted') + # For backwards compatibility remove old rendered scene files + # run_command('rm ' + renderfilename + '*') + renderfix = True + try: + with open(scenedir + '.videohash', 'r') as f: + oldvideohash = f.readline().strip() + print('oldvideohash is: ' + oldvideohash) + except: + print('no videohash found, making one...') + with open(scenedir + '.videohash', 'w') as f: + f.write(videohash) + #Audio + audiohash = '' + oldaudiohash = '' + newaudiomix = False + audiohash += str(int(countsize(renderfilename + '.wav'))) + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, shot) + for p in dubfiles: + audiohash += str(int(countsize(p))) + print('Audiohash of shot is: ' + audiohash) + try: + with open(scenedir + '.audiohash', 'r') as f: + oldaudiohash = f.readline().strip() + print('oldaudiohash is: ' + oldaudiohash) + except: + print('no audiohash found, making one...') + with open(scenedir + '.audiohash', 'w') as f: + f.write(audiohash) + if audiohash != oldaudiohash or newmix == True or renderfix == True: + #make scene rerender + os.system('touch '+filmfolder + filmname + '/scene' + str(scene).zfill(3)+'/.rerender') + #copy original sound + if os.path.exists(scenedir+'dub') == True: + os.system('cp '+scenedir+'dub/original.wav '+renderfilename+'.wav') + #os.system('cp '+dubfolder+'original.wav '+renderfilename+'.wav') + renderaudio(renderfilename, renderfilename, dubfiles, dubmix) + print('updating audiohash...') + with open(scenedir + '.audiohash', 'w') as f: + f.write(audiohash) + for i in range(len(dubfiles)): + os.system('cp ' + scenedir + '/dub/.settings' + str(i + 1).zfill(3) + ' ' + scenedir + '/dub/.rendered' + str(i + 1).zfill(3)) + print('Audio rendered!') + newaudiomix = True + muxing = True + if muxing == True: + #muxing mp3 layer to mp4 file + #count estimated audio filesize with a bitrate of 320 kb/s + audiosize = countsize(renderfilename + '.wav') * 0.453 + os.system('mv ' + renderfilename + '.mp4 ' + renderfilename + '_tmp.mp4') + if debianversion == 'stretch': + p = Popen(['avconv', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + else: + p = Popen(['ffmpeg', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + while p.poll() is None: + time.sleep(0.02) + try: + rendersize = countsize(renderfilename + '.mp3') + except: + continue + writemessage('audio rendering ' + str(int(rendersize)) + ' of ' + str(int(audiosize)) + ' kb done') + ##MERGE AUDIO & VIDEO + writemessage('Merging audio & video') + #os.remove(renderfilename + '.mp4') + call(['MP4Box', '-rem', '2', renderfilename + '_tmp.mp4'], shell=False) + call(['MP4Box', '-add', renderfilename + '_tmp.mp4', '-add', renderfilename + '.mp3', '-new', renderfilename + '.mp4'], shell=False) + try: + os.remove(renderfilename + '_tmp.mp4') + os.remove(renderfilename + '.mp3') + except: + print('nothin to remove') + else: + print('Already rendered!') + return renderfilename, newaudiomix + + +#-------------Render Scene------------- + +def renderscene(filmfolder, filmname, scene): + global fps + #This function checks and calls rendervideo & renderaudio if something has changed in the film + #Video + videohash = '' + oldvideohash = '' + filmfiles = shotfiles(filmfolder, filmname, scene) + renderfilename = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/scene' + scenedir = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/' + # Check if video corrupt + renderfixscene = False + try: + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + renderfilename + '.mp4', shell=True) + videolenght = pipe.decode().strip() + except: + videolenght = '' + renderfixscene = True + print('Scene lenght ' + videolenght) + if videolenght == '': + print('Okey, hold your horses, rendering!') + # For backwards compatibility remove old rendered scene files + #run_command('rm ' + renderfilename + '.mp4') + #run_command('rm ' + renderfilename + '.wav') + #vumetermessage('corrupted scene file! removing, please render again') + renderfixscene = True + #return '', '' + # Video Hash + for p in filmfiles: + #compileshot(p,filmfolder,filmname) + #print(p) + #time.sleep(5) + scene = int(p.rsplit('scene',1)[1][:3]) + shot = int(p.rsplit('shot',1)[1][:3]) + rendershotname, renderfix = rendershot(filmfolder, filmname, p, scene, shot) + if renderfix == True: + renderfixscene = True + if rendershotname: + try: + videohash = videohash + str(int(countsize(p + '.mp4'))) + except: + print('no file? ') + print('Videohash of scene is: ' + videohash) + try: + with open(scenedir + '.videohash', 'r') as f: + oldvideohash = f.readline().strip() + print('oldvideohash is: ' + oldvideohash) + except: + print('no videohash found, making one...') + with open(scenedir + '.videohash', 'w') as f: + f.write(videohash) + + print('renderfix is:'+str(renderfixscene)) + # Render if needed + if videohash != oldvideohash or renderfixscene == True or renderfix == True: + rendervideo(filmfiles, renderfilename, 'scene ' + str(scene)) + fastedit(filmfolder, filmname, filmfiles, scene) + print('updating videohash...') + with open(scenedir + '.videohash', 'w') as f: + f.write(videohash) + #time.sleep(3) + + #Audio + audiohash = '' + oldaudiohash = '' + newaudiomix = False + for p in filmfiles: + try: + audiohash += str(int(countsize(p + '.wav'))) + except: + audiohash=0 + renderfix=True + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, 0) + for p in dubfiles: + try: + audiohash += str(int(countsize(p))) + except: + audiohash=0 + print('Audiohash of scene is: ' + audiohash) + try: + with open(scenedir + '.audiohash', 'r') as f: + oldaudiohash = f.readline().strip() + print('oldaudiohash is: ' + oldaudiohash) + except: + print('no audiohash found, making one...') + with open(scenedir + '.audiohash', 'w') as f: + f.write(audiohash) + renderfixscene=True + if os.path.isfile(scenedir+'/.rerender') == True: + renderfixscene=True + os.system('rm '+scenedir+'/.rerender') + if audiohash != oldaudiohash or newmix == True or renderfix == True or renderfixscene == True: + renderaudio(filmfiles, renderfilename, dubfiles, dubmix) + print('updating audiohash...') + with open(scenedir + '.audiohash', 'w') as f: + f.write(audiohash) + for i in range(len(dubfiles)): + os.system('cp ' + scenedir + '/dub/.settings' + str(i + 1).zfill(3) + ' ' + scenedir + '/dub/.rendered' + str(i + 1).zfill(3)) + print('Audio rendered!') + newaudiomix = True + muxing = True + if muxing == True: + #muxing mp3 layer to mp4 file + #count estimated audio filesize with a bitrate of 320 kb/s + try: + audiosize = countsize(renderfilename + '.wav') * 0.453 + except: + print('noothing here') + os.system('mv ' + renderfilename + '.mp4 ' + renderfilename + '_tmp.mp4') + if debianversion == 'stretch': + p = Popen(['avconv', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + else: + p = Popen(['ffmpeg', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + while p.poll() is None: + time.sleep(0.02) + try: + rendersize = countsize(renderfilename + '.mp3') + except: + continue + writemessage('audio rendering ' + str(int(rendersize)) + ' of ' + str(int(audiosize)) + ' kb done') + ##MERGE AUDIO & VIDEO + writemessage('Merging audio & video') + #os.remove(renderfilename + '.mp4') + call(['MP4Box', '-rem', '2', renderfilename + '_tmp.mp4'], shell=False) + call(['MP4Box', '-add', renderfilename + '_tmp.mp4', '-add', renderfilename + '.mp3', '-new', renderfilename + '.mp4'], shell=False) + os.remove(renderfilename + '_tmp.mp4') + os.remove(renderfilename + '.mp3') + else: + print('Already rendered!') + return renderfilename, newaudiomix + +#-------------Render film------------ + +def renderfilm(filmfolder, filmname, comp, scene, muxing): + global fps + def render(q, filmfolder, filmname, comp, scene): + newaudiomix = False + #if comp == 1: + # newaudiomix = True + #This function checks and calls renderscene first then rendervideo & renderaudio if something has changed in the film + if scene > 0: + scenefilename, audiomix = renderscene(filmfolder, filmname, scene) + q.put(scenefilename) + return + scenes = countscenes(filmfolder, filmname) + for i in range(scenes): + scenefilename, audiomix = renderscene(filmfolder, filmname, i + 1) + #Check if a scene has a new audiomix + print('audiomix of scene ' + str(i + 1) + ' is ' + str(audiomix)) + if audiomix == True: + newaudiomix = True + filmfiles = scenefiles(filmfolder, filmname) + #Video + videohash = '' + oldvideohash = '' + renderfilename = filmfolder + filmname + '/' + filmname + filmdir = filmfolder + filmname + '/' + scenedir = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/' + for p in filmfiles: + print(p) + #compileshot(p,filmfolder,filmname) + videohash += str(int(countsize(p + '.mp4'))) + print('Videohash of film is: ' + videohash) + try: + with open(filmdir + '.videohash', 'r') as f: + oldvideohash = f.readline().strip() + print('oldvideohash is: ' + oldvideohash) + except: + print('no videohash found, making one...') + with open(filmdir + '.videohash', 'w') as f: + f.write(videohash) + if videohash != oldvideohash: + rendervideo(filmfiles, renderfilename, filmname) + print('updating video hash') + with open(filmdir + '.videohash', 'w') as f: + f.write(videohash) + #Audio + audiohash = '' + oldaudiohash = '' + for p in filmfiles: + print(p) + audiohash += str(int(countsize(p + '.wav'))) + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, 0, 0) + for p in dubfiles: + audiohash += str(int(countsize(p))) + print('Audiohash of film is: ' + audiohash) + try: + with open(filmdir + '.audiohash', 'r') as f: + oldaudiohash = f.readline().strip() + print('oldaudiohash is: ' + oldaudiohash) + except: + print('no audiohash found, making one...') + with open(filmdir+ '.audiohash', 'w') as f: + f.write(audiohash) + #This is if the scene has a new audiomix + if newaudiomix == True: + newmix = True + if audiohash != oldaudiohash or newmix == True: + renderaudio(filmfiles, renderfilename, dubfiles, dubmix) + print('updating audiohash...') + with open(filmdir+ '.audiohash', 'w') as f: + f.write(audiohash) + for i in range(len(dubfiles)): + os.system('cp ' + filmdir + '/dub/.settings' + str(i + 1).zfill(3) + ' ' + filmdir + '/dub/.rendered' + str(i + 1).zfill(3)) + print('Audio rendered!') + #compressing + if comp > 0: + writemessage('compressing audio') + os.system('mv ' + renderfilename + '.wav ' + renderfilename + '_tmp.wav') + #run_command('sox ' + renderfilename + '_tmp.wav ' + renderfilename + '.wav compand 0.3,1 6:-70,-60,-20 -5 -90 0.2') + run_command('sox ' + renderfilename + '_tmp.wav ' + renderfilename + '.wav compand 0.0,1 6:-70,-43,-20 -6 -90 0.1') + os.remove(renderfilename + '_tmp.wav') + if muxing == True: + #muxing mp3 layer to mp4 file + #count estimated audio filesize with a bitrate of 320 kb/s + audiosize = countsize(renderfilename + '.wav') * 0.453 + os.system('mv ' + renderfilename + '.mp4 ' + renderfilename + '_tmp.mp4') + if debianversion == 'stretch': + p = Popen(['avconv', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + else: + p = Popen(['ffmpeg', '-y', '-i', renderfilename + '.wav', '-acodec', 'libmp3lame', '-ac', '2', '-b:a', '320k', renderfilename + '.mp3']) + while p.poll() is None: + time.sleep(0.02) + try: + rendersize = countsize(renderfilename + '.mp3') + except: + continue + writemessage('audio rendering ' + str(int(rendersize)) + ' of ' + str(int(audiosize)) + ' kb done') + ##MERGE AUDIO & VIDEO + writemessage('Merging audio & video') + #os.remove(renderfilename + '.mp4') + call(['MP4Box', '-rem', '2', renderfilename + '_tmp.mp4'], shell=False) + call(['MP4Box', '-add', renderfilename + '_tmp.mp4', '-add', renderfilename + '.mp3', '-new', renderfilename + '.mp4'], shell=False) + os.remove(renderfilename + '_tmp.mp4') + os.remove(renderfilename + '.mp3') + else: + print('Already rendered!') + q.put(renderfilename) + q = mp.Queue() + proc = mp.Process(target=render, args=(q,filmfolder,filmname,comp,scene)) + proc.start() + procdone = False + status = '' + vumetermessage('press middlebutton to cancel') + while True: + if proc.is_alive() == False and procdone == False: + status = q.get() + print(status) + procdone = True + proc.join() + renderfilename = status + vumetermessage('') + break + if middlebutton() == True: + proc.terminate() + proc.join() + procdone = True + q='' + os.system('pkill MP4Box') + vumetermessage('canceled for now, maybe u want to render later ;)') + writemessage('press any button to continue') + print('canceling videorender') + renderfilename = '' + break + return renderfilename + +#-------------Get dub files----------- + +def getdubs(filmfolder, filmname, scene, shot): + #search for dub files + print('getting scene dubs') + dubfiles = [] + dubmix = [] + rerender = False + if scene > 0 and shot == 0: + filefolder = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/dub/' + elif scene > 0 and shot > 0: + filefolder = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/dub/' + else: + filefolder = filmfolder + filmname + '/dub/' + try: + allfiles = os.listdir(filefolder) + except: + print('no dubs') + return dubfiles, dubmix, rerender + for a in allfiles: + if 'dub' in a: + print('Dub audio found! ' + filefolder + a) + dubfiles.append(filefolder + a) + #check if dub mix has changed + dubnr = 1 + for i in dubfiles: + dub = [] + rendered_dub = [] + try: + with open(filefolder + '.settings' + str(dubnr).zfill(3), 'r') as f: + dubstr = f.read().splitlines() + for i in dubstr: + dub.append(float(i)) + print('dub ' + str(dubnr).zfill(3) + ' loaded!') + print(dub) + except: + print('cant find settings file') + dub = [1.0, 1.0, 0.0, 0.0] + with open(filefolder + ".settings" + str(dubnr).zfill(3), "w") as f: + for i in dub: + f.write(str(i) + '\n') + try: + with open(filefolder + '.rendered' + str(dubnr).zfill(3), 'r') as f: + dubstr = f.read().splitlines() + for i in dubstr: + rendered_dub.append(float(i)) + print('rendered dub loaded') + print(rendered_dub) + except: + print('no rendered dubmix found!') + if rendered_dub != dub: + rerender = True + dubmix.append(dub) + dubnr += 1 + return dubfiles, dubmix, rerender + +#------------Remove Dubs---------------- + +def removedub(dubfolder, dubnr): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + selected = 0 + header = 'Are you sure you want to remove dub ' + str(dubnr) + '?' + menu = 'NO', 'YES' + settings = '', '' + while True: + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'right': + if selected < (len(menu) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle' and selected == 0: + logger.info('dont remove dub') + time.sleep(0.3) + break + elif pressed == 'middle' and selected == 1: + os.system('rm ' + dubfolder + 'dub' + str(dubnr).zfill(3) + '.wav') + os.system('rm ' + dubfolder + '.settings' + str(dubnr).zfill(3)) + os.system('rm ' + dubfolder + '.rendered' + str(dubnr).zfill(3)) + time.sleep(0.5) + print(dubfolder) + dubs = next(os.walk(dubfolder))[2] + print(dubs) + for i in dubs: + if 'dub' not in i: + dubs.remove(i) + organized_nr = 1 + for s in sorted(dubs): + if '.wav' in s and 'dub' in s: + print(s) + unorganized_nr = int(s[3:-4]) + if organized_nr == unorganized_nr: + print('correct') + pass + if organized_nr != unorganized_nr: + print('false, correcting from ' + str(unorganized_nr) + ' to ' + str(organized_nr)) + run_command('mv ' + dubfolder + 'dub' + str(unorganized_nr).zfill(3) + '.wav ' + dubfolder + 'dub' + str(organized_nr).zfill(3) + '.wav') + run_command('mv ' + dubfolder + '.settings' + str(unorganized_nr).zfill(3) + ' ' + dubfolder + '.settings' + str(organized_nr).zfill(3)) + run_command('mv ' + dubfolder + '.rendered' + str(unorganized_nr).zfill(3) + ' ' + dubfolder + '.rendered' + str(organized_nr).zfill(3)) + organized_nr += 1 + logger.info('removed dub file!') + vumetermessage('dub removed!') + break + time.sleep(0.05) + +#-------------Clip settings--------------- + +def clipsettings(filmfolder, filmname, scene, shot, take, plughw): + vumetermessage('press record, view or retake to be dubbing') + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + selected = 0 + dubfiles = [] + dubmix = [] + dubmix_old = [] + if scene > 0 and shot == 0: + header = 'Scene ' + str(scene) + ' dubbing settings' + filefolder = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/dub/' + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, 0) + elif scene > 0 and shot > 0: + header = 'Scene ' + str(scene) + ' shot ' + str(shot) + ' dubbing settings' + filefolder = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/dub/' + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, shot) + else: + header = 'Film ' + filmname + ' dubbing settings' + filefolder = filmfolder + filmname + '/dub/' + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, 0, 0) + newdub = [1.0, 1.0, 0.1, 0.1] + dubselected = len(dubfiles) - 1 + dubrecord = '' + while True: + nmix = round(newdub[0],1) + ndub = round(newdub[1],1) + nfadein = round(newdub[2],1) + nfadeout = round(newdub[3],1) + if dubfiles: + mix = round(dubmix[dubselected][0],1) + dub = round(dubmix[dubselected][1],1) + fadein = round(dubmix[dubselected][2],1) + fadeout = round(dubmix[dubselected][3],1) + menu = 'BACK', 'ADD:', '', '', 'DUB' + str(dubselected + 1) + ':', '', '', '' + settings = '', 'd:' + str(nmix) + '/o:' + str(ndub), 'in:' + str(nfadein), 'out:' + str(nfadeout), '', 'd:' + str(mix) + '/o' + str(dub), 'in:' + str(fadein), 'out:' + str(fadeout) + else: + menu = 'BACK', 'ADD:', '', '' + settings = '', 'd:' + str(nmix) + '/o:' + str(ndub), 'in:' + str(nfadein), 'out:' + str(nfadeout) + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + + #NEW DUB SETTINGS + if pressed == 'up' and selected == 1: + if newdub[0] > 0.99 and newdub[1] > 0.01: + newdub[1] -= 0.1 + if newdub[1] > 0.99 and newdub[0] < 0.99: + newdub[0] += 0.1 + elif pressed == 'down' and selected == 1: + if newdub[1] > 0.99 and newdub[0] > 0.01: + newdub[0] -= 0.1 + if newdub[0] > 0.99 and newdub[1] < 0.99: + newdub[1] += 0.1 + elif pressed == 'up' and selected == 2: + newdub[2] += 0.1 + elif pressed == 'down' and selected == 2: + if newdub[2] > 0.01: + newdub[2] -= 0.1 + elif pressed == 'up' and selected == 3: + newdub[3] += 0.1 + elif pressed == 'down' and selected == 3: + if newdub[3] > 0.01: + newdub[3] -= 0.1 + elif pressed == 'record' or pressed == 'middle' and selected == 1: + dubmix.append(newdub) + dubrecord = filefolder + 'dub' + str(len(dubfiles)+1).zfill(3) + '.wav' + break + elif pressed == 'retake' and selected == 4: + dubrecord = filefolder + 'dub' + str(dubselected + 1).zfill(3) + '.wav' + break + #DUB SETTINGS + elif pressed == 'up' and selected == 4: + if dubselected + 1 < len(dubfiles): + dubselected = dubselected + 1 + elif pressed == 'down' and selected == 4: + if dubselected > 0: + dubselected = dubselected - 1 + elif pressed == 'remove' and selected == 4: + removedub(filefolder, dubselected + 1) + dubfiles, dubmix, newmix = getdubs(filmfolder, filmname, scene, shot) + dubselected = len(dubfiles) - 1 + if len(dubfiles) == 0: + #save original sound + saveoriginal = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take'+str(take).zfill(3)+'.wav' + print('no dubs, copying original sound to original') + os.system('cp '+filefolder+'original.wav '+saveoriginal) + #removedub folder + os.system('rm -r ' + filefolder) + time.sleep(1) + selected = 0 + elif pressed == 'up' and selected == 5: + if dubmix[dubselected][0] >= 0.99 and dubmix[dubselected][1] > 0.01: + dubmix[dubselected][1] -= 0.1 + if dubmix[dubselected][1] >= 0.99 and dubmix[dubselected][0] < 0.99: + dubmix[dubselected][0] += 0.1 + elif pressed == 'down' and selected == 5: + if dubmix[dubselected][1] >= 0.99 and dubmix[dubselected][0] > 0.01: + dubmix[dubselected][0] -= 0.1 + if dubmix[dubselected][0] >= 0.99 and dubmix[dubselected][1] < 0.99: + dubmix[dubselected][1] += 0.1 + elif pressed == 'up' and selected == 6: + dubmix[dubselected][2] += 0.1 + elif pressed == 'down' and selected == 6: + if dubmix[dubselected][2] > 0.01: + dubmix[dubselected][2] -= 0.1 + elif pressed == 'up' and selected == 7: + dubmix[dubselected][3] += 0.1 + elif pressed == 'down' and selected == 7: + if dubmix[dubselected][3] > 0.01: + dubmix[dubselected][3] -= 0.1 + elif pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle' and menu[selected] == 'BACK': + os.system('pkill aplay') + break + elif pressed == 'view': # mix dub and listen + run_command('pkill aplay') + dubfiles, dubmix, rerender = getdubs(filmfolder, filmname, scene, shot) + if scene: + filename = filmfolder + filmname + '/scene' + str(scene).zfill(3) +'/scene' + else: + filename = filmfolder + filmname + '/' + filmname + renderfilename = renderfilm(filmfolder, filmname, 0, scene, False) + playdub(filmname,renderfilename, 'scene') + time.sleep(0.05) + #Save dubmix before returning + if dubmix != dubmix_old: + if os.path.isdir(filefolder) == False: + os.makedirs(filefolder) + c = 1 + for i in dubmix: + with open(filefolder + ".settings" + str(c).zfill(3), "w") as f: + for p in i: + f.write(str(round(p,1)) + '\n') + print(str(round(p,1))) + c += 1 + dubmix_old = dubmix + return dubrecord + +#---------------Play & DUB-------------------- + +def playdub(filmname, filename, player_menu): + global headphoneslevel, miclevel, plughw, channels, filmfolder, scene, soundrate, soundformat + #read fastedit file + if player_menu == 'scene': + scenedir = filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/' + try: + with open(scenedir + '.fastedit', 'r') as f: + fastedit = f.read().splitlines() + print(fastedit) + except: + print('no fastedit file found') + fastedit = 9999999 + #omxplayer hack + os.system('rm /tmp/omxplayer*') + video = True + if player_menu == 'dub': + dub = True + else: + dub = False + if not os.path.isfile(filename + '.mp4'): + #should probably check if its not a corrupted video file + logger.info("no file to play") + if dub == True: + video = False + else: + return + t = 0 + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + playing = False + pause = False + trim = False + videolag = 0 + remove_shots = [] + if video == True: + if player_menu == 'dubbb': + try: + player = OMXPlayer(filename + '.mp4', args=['-n', '-1', '--fps', '25', '--layer', '3', '--no-osd', '--win', '0,15,800,475','--no-keys'], dbus_name='org.mpris.MediaPlayer2.omxplayer1', pause=True) + except: + writemessage('Something wrong with omxplayer') + time.sleep(0.5) + return + else: + try: + player = OMXPlayer(filename + '.mp4', args=['--adev', 'alsa:hw:'+str(plughw), '--fps', '25', '--layer', '3', '--no-osd', '--win', '0,15,800,475','--no-keys'], dbus_name='org.mpris.MediaPlayer2.omxplayer1', pause=True) + except: + writemessage('Something wrong with omxplayer') + time.sleep(0.5) + return + #player = OMXPlayer(filename + '.mp4', args=['--fps', '25', '--layer', '3', '--win', '0,70,800,410', '--no-osd', '--no-keys'], dbus_name='org.mpris.MediaPlayer2.omxplayer1', pause=True) + writemessage('Loading...') + clipduration = player.duration() + #sound + #if player_menu != 'film': + # try: + # playerAudio = OMXPlayer(filename + '.wav', args=['--adev','alsa:hw:'+str(plughw)], dbus_name='org.mpris.MediaPlayer2.omxplayer2', pause=True) + # time.sleep(0.2) + # except: + # writemessage('something wrong with audio player') + # time.sleep(2) + # return + #omxplayer hack to play really short videos. + if clipduration < 4: + logger.info("clip duration shorter than 4 sec") + player.previous() + if dub == True: + p = 0 + while p < 3: + writemessage('Dubbing in ' + str(3 - p) + 's') + time.sleep(1) + p+=1 + if video == True: + player.play() + #run_command('aplay -D plughw:0 ' + filename + '.wav &') + #run_command('mplayer ' + filename + '.wav &') + if player_menu == 'dub': + run_command(gonzopifolder + '/alsa-utils-1.1.3/aplay/arecord -D plughw:'+str(plughw)+' -f '+soundformat+' -c '+str(channels)+' -r '+soundrate+' -vv /dev/shm/dub.wav &') + time.sleep(0.5) + #try: + # playerAudio.play() + #except: + # logger.info('something wrong with omxplayer audio or playing film mp4 audio') + #logger.warning(e) + starttime = time.time() + selected = 1 + while True: + if player_menu == 'scene': + fastedit_shot = 1 + for i in fastedit: + if int(t) > float(int(i)/1000): + fastedit_shot = fastedit_shot + 1 + if not remove_shots: + vumetermessage('shot ' + str(fastedit_shot)) + else: + p = '' + for i in remove_shots: + p = p + str(i) + ',' + vumetermessage('shot ' + str(fastedit_shot) + ' remove:' + p) + if trim == True: + menu = 'CANCEL', 'FROM BEGINNING', 'FROM END' + settings = '','','' + elif pause == True: + if player_menu == 'shot': + menu = 'BACK', 'PLAY', 'REPLAY', 'TRIM' + settings = '','','','' + else: + menu = 'BACK', 'PLAY', 'REPLAY' + settings = '','','' + elif player_menu == 'dub': + menu = 'BACK', 'REDUB', 'PHONES:', 'MIC:' + settings = '', '', str(headphoneslevel), str(miclevel) + else: + menu = 'BACK', 'PAUSE', 'REPLAY', 'PHONES:' + settings = '', '', '', str(headphoneslevel) + if dub == True: + header = 'Dubbing ' + str(round(t,1)) + else: + header = 'Playing ' + str(round(t,1)) + ' of ' + str(clipduration) + ' s' + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if buttonpressed == True: + flushbutton() + if pressed == 'remove': + if fastedit_shot in remove_shots: + remove_shots.remove(fastedit_shot) + else: + remove_shots.append(fastedit_shot) + time.sleep(0.2) + elif pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'up': + if menu[selected] == 'PHONES:': + if headphoneslevel < 100: + headphoneslevel = headphoneslevel + 2 + run_command('amixer -c 0 sset Speaker ' + str(headphoneslevel) + '%') + elif menu[selected] == 'MIC:': + if miclevel < 100: + miclevel = miclevel + 2 + run_command('amixer -c 0 sset Mic ' + str(miclevel) + '% unmute') + else: + try: + player.set_position(t+2) + #playerAudio.set_position(player.position()) + except: + print('couldnt set position of player') + elif pressed == 'down': + if menu[selected] == 'PHONES:': + if headphoneslevel > 0: + headphoneslevel = headphoneslevel - 2 + run_command('amixer -c 0 sset Speaker ' + str(headphoneslevel) + '%') + elif menu[selected] == 'MIC:': + if miclevel > 0: + miclevel = miclevel - 2 + run_command('amixer -c 0 sset Mic ' + str(miclevel) + '% unmute') + else: + if t > 1: + try: + player.set_position(t-2) + #playerAudio.set_position(player.position()) + except: + print('couldnt set position of player') + elif pressed == 'middle' or pressed == 'record': + time.sleep(0.2) + if menu[selected] == 'BACK' or player.playback_status() == "Stopped" or pressed == 'record': + try: + if video == True: + #player.stop() + #playerAudio.stop() + player.quit() + #playerAudio.quit() + #os.system('pkill -9 aplay') + except: + #kill it if it dont stop + print('OMG! kill dbus-daemon') + if dub == True: + os.system('pkill arecord') + time.sleep(0.2) + os.system('pkill -9 omxplayer') + #os.system('pkill -9 dbus-daemon') + return remove_shots + elif menu[selected] == 'REPLAY' or menu[selected] == 'REDUB': + pause = False + try: + os.system('pkill aplay') + if dub == True: + os.system('pkill arecord') + if video == True: + player.pause() + player.set_position(0) + #if player_menu != 'film': + #playerAudio.pause() + #playerAudio.set_position(0) + if dub == True: + p = 0 + while p < 3: + writemessage('Dubbing in ' + str(3 - p) + 's') + time.sleep(1) + p+=1 + player.play() + #if player_menu != 'film': + # playerAudio.play() + #run_command('aplay -D plughw:0 ' + filename + '.wav &') + if dub == True: + run_command(gonzopifolder + '/alsa-utils-1.1.3/aplay/arecord -D plughw:'+str(plughw)+' -f '+soundformat+' -c '+str(channels)+' -r '+soundrate+' -vv /dev/shm/dub.wav &') + except: + pass + starttime = time.time() + elif menu[selected] == 'PAUSE': + player.pause() + #try: + # playerAudio.pause() + #except: + # pass + pause = True + elif menu[selected] == 'PLAY': + player.play() + #try: + # playerAudio.play() + #except: + # pass + pause = False + elif menu[selected] == 'TRIM': + selected = 1 + trim = True + elif menu[selected] == 'CANCEL': + selected = 1 + trim = False + elif menu[selected] == 'FROM BEGINNING': + trim = ['beginning', player.position()] + player.quit() + #playerAudio.quit() + return trim + elif menu[selected] == 'FROM END': + trim = ['end', player.position()] + player.quit() + #playerAudio.quit() + return trim + time.sleep(0.02) + if pause == False: + try: + t = player.position() + except: + os.system('pkill aplay') + if dub == True: + os.system('pkill arecord') + return remove_shots + player.quit() + #playerAudio.quit() + #os.system('pkill dbus-daemon') + +#---------------View Film-------------------- + +def viewfilm(filmfolder, filmname): + scenes, shots, takes = countlast(filmname, filmfolder) + scene = 1 + filmfiles = [] + while scene <= scenes: + shots = countshots(filmname, filmfolder, scene) + if shots > 0: + filmfiles.extend(shotfiles(filmfolder, filmname, scene)) + scene = scene + 1 + return filmfiles + +#---------------Video Trim-------------------- + +def videotrim(filename, trim_filename, where, s): + #theres two different ways of non-rerendering mp4 cut techniques that i know MP4Box and ffmpeg + if where == 'beginning': + logger.info('trimming clip from beginning') + #run_command('ffmpeg -ss ' + str(s) + ' -i ' + filename + '.mp4 -c copy ' + trim_filename + '.mp4') + run_command('MP4Box ' + filename + '.mp4 -splitx ' + str(s) + ':end -out ' + trim_filename + '.mp4') + run_command('cp ' + filename + '.wav ' + trim_filename + '.wav') + audiotrim(trim_filename, 'beginning','') + if where == 'end': + logger.info('trimming clip from end') + #run_command('ffmpeg -to ' + str(s) + ' -i ' + filename + '.mp4 -c copy ' + trim_filename + '.mp4') + run_command('MP4Box ' + filename + '.mp4 -splitx 0:' + str(s) + ' -out ' + trim_filename + '.mp4') + run_command('cp ' + filename + '.wav ' + trim_filename + '.wav') + audiotrim(trim_filename, 'end','') + #take last frame + run_command('ffmpeg -sseof -1 -i ' + trim_filename + '.mp4 -update 1 -q:v 1 -vf scale=800:450 ' + trim_filename + '.jpeg') + return + +#--------------Get Audio cards-------------- +def getaudiocards(): + with open("/proc/asound/cards") as fp: + cards = fp.readlines() + audiocards = [] + for i in cards: + if i[1] in ['0','1','2','3']: + print('audio card 0: ' + i[22:].rstrip('\n')) + audiocards.append(i[22:].rstrip('\n')) + return audiocards + +#--------------Audio Trim-------------------- +# make audio file same lenght as video file +def audiotrim(filename, where, dub): + global channels, fps + videofile=filename + audiosync=0 + print("chaaaaaaaaaaaaaaaanel8: " +str(channels)) + writemessage('Audio syncing..') + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + filename + '.mp4', shell=True) + videolenght = pipe.decode().strip() + print('videolenght:'+str(videolenght)) + if dub: + filename=dub[:-4] + try: + pipe = subprocess.check_output('mediainfo --Inform="Audio;%Duration%" ' + filename + '.wav', shell=True) + audiolenght = pipe.decode().strip() + except: + audiosilence('',filename) + audiolenght=videolenght + #if there is no audio lenght + logger.info('audio is:' + audiolenght) + if not audiolenght.strip(): + audiolenght = 0 + #separate seconds and milliseconds + #videoms = int(videolenght) % 1000 + #audioms = int(audiolenght) % 1000 + #videos = int(videolenght) / 1000 + #audios = int(audiolenght) / 1000 + elif int(audiolenght) > int(videolenght): + #calculate difference + audiosync = int(audiolenght) - int(videolenght) + newaudiolenght = int(audiolenght) - audiosync + logger.info('Audiofile is: ' + str(audiosync) + 'ms longer') + #trim from end or beginning and put a 0.01 in- and outfade + if where == 'end': + run_command('sox -V0 ' + filename + '.wav ' + filename + '_temp.wav trim 0 -' + str(int(audiosync)/1000)) + if where == 'beginning': + logger.info('trimming from beginning at: '+str(int(audiosync)/1000)) + run_command('sox -V0 ' + filename + '.wav ' + filename + '_temp.wav trim ' + str(int(audiosync)/1000)) + run_command('sox -V0 -G ' + filename + '_temp.wav ' + filename + '.wav fade 0.01 0 0.01') + os.remove(filename + '_temp.wav') + #if int(audiosync) > 400: + # writemessage('WARNING!!! VIDEO FRAMES DROPPED!') + # vumetermessage('Consider changing to a faster microsd card.') + # time.sleep(10) + delayerr = 'A' + str(audiosync) + print(delayerr) + elif int(audiolenght) < int(videolenght): + audiosync = int(videolenght) - int(audiolenght) + #calculate difference + #audiosyncs = videos - audios + #audiosyncms = videoms - audioms + #if audiosyncms < 0: + # if audiosyncs > 0: + # audiosyncs = audiosyncs - 1 + # audiosyncms = 1000 + audiosyncms + logger.info('Videofile is: ' + str(audiosync) + 'ms longer') + logger.info('Videofile is: ' + str(int(audiosync)/1000) + 's longer') + #time.sleep(2) + #make fade + #make delay file + print(str(int(audiosync)/1000)) + run_command('sox -V0 -r '+soundrate+' -c 2 '+filename+'.wav '+filename+'_temp.wav trim 0.0 pad 0 ' + str(int(audiosync)/1000)) + run_command('sox -V0 -G ' + filename + '_temp.wav ' + filename + '.wav fade 0.01 0 0.01') + #add silence to end + #run_command('sox -V0 /dev/shm/silence.wav ' + filename + '_temp.wav') + #run_command('cp '+filename+'.wav '+filename+'_temp.wav') + #run_command('sox -V0 -G ' + filename + '_temp.wav /dev/shm/silence.wav ' + filename + '.wav') + os.remove(filename + '_temp.wav') + #os.remove('/dev/shm/silence.wav') + delayerr = 'V' + str(audiosync) + print(delayerr) + print('the results:') + pipe = subprocess.check_output('mediainfo --Inform="Audio;%Duration%" ' + filename + '.wav', shell=True) + audiolenght = pipe.decode().strip() + print('aftersyncvideo: '+str(videolenght) + ' audio:'+str(audiolenght)) + if int(audiolenght) != int(videolenght): + vumetermessage('SYNCING FAILED!') + time.sleep(10) + #os.remove('/dev/shm/' + filename + '.wav') + return float(audiosync)/1000, int(videolenght), int(audiolenght) + #os.system('mv audiosynced.wav ' + filename + '.wav') + #os.system('rm silence.wav') + +#--------------Audiosilence-------------------- +# make an empty audio file as long as a video file + +def audiosilence(foldername,filename): + global channels + writemessage('Creating audiosilence..') + pipe = subprocess.check_output('mediainfo --Inform="Video;%Duration%" ' + foldername + filename + '.mp4', shell=True) + videolenght = pipe.decode() + logger.info('Video lenght is ' + videolenght) + #separate seconds and milliseconds + videoms = int(videolenght) % 1000 + videos = int(videolenght) / 1000 + logger.info('Videofile is: ' + str(videos) + 's ' + str(videoms)) + run_command('sox -V0 -n -r '+soundrate+' -c 2 /dev/shm/silence.wav trim 0.0 ' + str(videos)) + os.system('cp /dev/shm/silence.wav ' + foldername + filename + '.wav') + os.system('rm /dev/shm/silence.wav') + +#--------------Copy to USB------------------- + +def copytousb(filmfolder): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + writemessage('Searching for usb storage device, middlebutton to cancel') + films = getfilms(filmfolder) + while True: + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + usbconnected = os.path.ismount('/media/usb0') + if pressed == 'middle': + writemessage('canceling..') + time.sleep(2) + break + time.sleep(0.02) + if usbconnected == True: + #Copy new files to usb device + try: + os.makedirs('/media/usb0/gonzopifilms/') + except: + pass + try: + p = subprocess.check_output('stat -f -c %T /media/usb0', shell=True) + filesystem = p.decode() + print('filesystem info: ' + filesystem) + except: + writemessage('Oh-no! dont know your filesystem') + waitforanykey() + return + for filmname in films: + #check filmhash + filmname = filmname[0] + usbpath = '/media/usb0/gonzopifilms/'+filmname + usbfilmhash = '' + filmhash = '' + while True: + if os.path.exists(usbpath) == False: + break + try: + with open(filmfolder + filmname + '/.filmhash', 'r') as f: + filmhash = f.readline().strip() + print('filmhash is: ' + filmhash) + except: + print('no filmhash found!') + try: + with open(usbpath + '/.filmhash', 'r') as f: + usbfilmhash = f.readline().strip() + print('usbfilmhash is: ' + usbfilmhash) + except: + print('no usbfilmhash found!') + if usbfilmhash == filmhash: + print('same moviefilm found, updating clips...') + break + else: + writemessage('Found a subsequent moviefilm...') + print('same film exist with different filmhashes, copying to subsequent film folder') + time.sleep(2) + usbpath += '_new' + try: + os.makedirs(usbpath) + writemessage('Copying film ' + filmname + '...') + except: + writemessage('Found existing ' + filmname + ', copying new files... ') + try: + run_command('rsync -avr -P ' + filmfolder + filmname + ' ' + usbpath) + except: + writemessage('couldnt copy film ' + filmname) + waitforanykey() + return + run_command('sync') + run_command('pumount /media/usb0') + writemessage('all files copied successfully!') + waitforanykey() + writemessage('You can safely unplug the usb device now') + time.sleep(2) + return + +#-----------Check for the webz--------- + +def webz_on(): + try: + # connect to the host -- tells us if the host is actually + # reachable + socket.create_connection(("google.com", 80)) + return True + except OSError: + pass + writemessage('No internet connection!') + time.sleep(2) + return False + +#-------------Upload film------------ + +def uploadfilm(filename, filmname): + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + mods = ['Back'] + settings = [''] + writemessage('Searching for upload mods') + with open(gonzopifolder + '/mods/upload-mods-enabled') as m: + mods.extend(m.read().splitlines()) + for m in mods: + settings.append('') + menu = mods + selected = 0 + while True: + header = 'Where do you want to upload?' + writemenu(menu,settings,selected,header,showmenu) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'right': + if selected < (len(menu) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + elif pressed == 'middle' and menu[selected] == 'Back': + return None + elif pressed == 'middle' and menu[selected] in mods: + cmd = gonzopifolder + '/mods/' + menu[selected] + '.sh ' + filmname + ' ' + filename + '.mp4' + return cmd + time.sleep(0.02) + + +#-------------Streaming--------------- + +def startstream(camera, stream, plughw, channels): + #youtube + #youtube="rtmp://a.rtmp.youtube.com/live2/" + #with open("/home/pi/.youtube-live") as fp: + # key = fp.readlines() + #print('using key: ' + key[0]) + #stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac '+str(channels)+' -i hw:'+str(plughw)+' -ar 44100 -vcodec copy -acodec libmp3lame -b:a 128k -ar 44100 -map 0:0 -map 1:0 -strict experimental -f flv ' + youtube + key[0] + # + #stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac '+str(channels)+' -i hw:'+str(plughw)+' -ar 44100 -vcodec copy -acodec libmp3lame -b:a 128k -ar 44100 -map 0:0 -map 1:0 -strict experimental -f mpegts tcp://0.0.0.0:3333\?listen' + #stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac '+str(channels)+' -i hw:'+str(plughw)+' -ar '+soundrate+' -acodec mp2 -b:a 128k -ar '+soundrate+' -vcodec copy -map 0:0 -map 1:0 -g 0 -f mpegts udp://10.42.0.169:5002' + stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac '+str(channels)+' -i hw:'+str(plughw)+' -ar '+soundrate+' -acodec mp2 -b:a 128k -ar '+soundrate+' -vcodec copy -f mpegts udp://10.42.0.169:5002' + try: + stream = subprocess.Popen(stream_cmd, shell=True, stdin=subprocess.PIPE) + camera.start_recording(stream.stdin, splitter_port=2, format='h264', bitrate = 55555) + except: + stream = '' + #now = time.strftime("%Y-%m-%d-%H:%M:%S") + return stream + +def stopstream(camera, stream): + camera.stop_recording(splitter_port=2) + os.system('pkill -9 ffmpeg') + print("Camera safely shut down") + print("Good bye") + stream = '' + return stream + +#-------------Beeps------------------- + +def beep(bus): + global gonzopifolder, plughw + if bus: + buzzerrepetitions = 100 + buzzerdelay = 0.00001 + for _ in range(buzzerrepetitions): + for value in [0xC, 0x4]: + #GPIO.output(1, value) + bus.write_byte_data(DEVICE,OLATA,value) + time.sleep(buzzerdelay) + else: + run_command('aplay -D plughw:' + str(plughw) + ' '+ gonzopifolder + '/extras/beep.wav') + return + +def longbeep(bus): + global gonzopifolder, plughw + if bus: + buzzerrepetitions = 100 + buzzerdelay = 0.0001 + for _ in range(buzzerrepetitions * 5): + for value in [0xC, 0x4]: + #GPIO.output(1, value) + bus.write_byte_data(DEVICE,OLATA,value) + buzzerdelay = buzzerdelay - 0.00000004 + time.sleep(buzzerdelay) + bus.write_byte_data(DEVICE,OLATA,0x4) + else: + run_command('aplay -D plughw:' + str(plughw) + ' '+ gonzopifolder + '/extras/beep_long.wav') + return + +def buzz(buzzerlenght): + buzzerdelay = 0.0001 + for _ in range(buzzerlenght): + for value in [0xC, 0x4]: + #GPIO.output(1, value) + bus.write_byte_data(DEVICE,OLATA,value) + time.sleep(buzzerdelay) + return + +#---------reading in a lens shading table---------- + +def read_table(inFile): + # q&d-way to read in ls_table.h + ls_table = [] + channel = [] + with open(inFile) as file: + for line in file: + # we skip the unimportant stuff + if not ( line.startswith("uint") \ + or line.startswith("}")): + # the comments separate the color planes + if line.startswith("//"): + channel = [] + ls_table.append(channel) + else: + # scan in a single line + line = line.replace(',','') + lineData = [int(x) for x in line.split()] + channel.append(lineData) + return np.array(ls_table,dtype=np.uint8) + +#-------------Check if file empty---------- + +def empty(filename): + if os.path.isfile(filename + '.mp4') == False: + return False + if os.path.isfile(filename + '.mp4') == True: + writemessage('Take already exists') + time.sleep(1) + return True + +#--------------BUTTONS------------- + +def waitforanykey(): + vumetermessage("press any key to continue..") + time.sleep(1) + while True: + with term.cbreak(): + val = term.inkey(timeout=0) + if not val: + event = '' + elif val.is_sequence: + event = val.name + elif val: + event = val + if i2cbuttons == True: + readbus = bus.read_byte_data(DEVICE,GPIOB) + readbus2 = bus.read_byte_data(DEVICE,GPIOA) + else: + readbus = 255 + readbus2 = 247 + if readbus != 255 or readbus2 != 247 or event != '': + time.sleep(0.05) + vumetermessage(' ') + return + +def middlebutton(): + with term.cbreak(): + val = term.inkey(timeout=0) + if val.is_sequence: + event = val.name + #print(event) + elif val: + event = val + #print(event) + else: + event = '' + if i2cbuttons == True: + readbus = bus.read_byte_data(DEVICE,GPIOB) + readbus2 = bus.read_byte_data(DEVICE,GPIOA) + if readbus != 255: + print('i2cbutton pressed: ' + str(readbus)) + if readbus2 != 247: + print('i2cbutton pressed: ' + str(readbus2)) + else: + readbus = 255 + readbus2 = 247 + pressed = '' + if event == 'KEY_ENTER' or event == 10 or event == 13 or (readbus == 247 and readbus2 == 247): + pressed = 'middle' + return True + return False + +def flushbutton(): + with term.cbreak(): + while True: + inp = term.inkey(timeout=0) + #print('flushing ' + repr(inp)) + if inp == '': + break + +def getbutton(lastbutton, buttonpressed, buttontime, holdbutton): + global i2cbuttons, serverstate, nextstatus, process, que, gonzopictrl_ip, recording, onlysound, filmname, filmfolder, scene, shot, take, selected, camera, loadfilmsettings, selected, newfilmname + #Check controller + pressed = '' + nextstatus = '' + try: + if process.is_alive() == False and serverstate == 'on': + nextstatus = que.get() + if "*" in nextstatus: + gonzopictrl_ip = nextstatus.split('*')[1] + nextstatus = nextstatus.split('*')[0] + print('gonzopictrl ip:' + gonzopictrl_ip) + process = Process(target=listenforclients, args=("0.0.0.0", port, que)) + process.start() + if 'SELECTED' in nextstatus: + try: + selected=int(nextstatus.split(':')[1]) + except: + print('wtf?') + if nextstatus=="PICTURE": + pressed="picture" + elif nextstatus=="UP": + pressed="up" + elif nextstatus=="DOWN": + pressed="down" + elif nextstatus=="LEFT": + pressed="left" + elif nextstatus=="RIGHT": + pressed="right" + elif nextstatus=="VIEW": + pressed="view" + elif nextstatus=="MIDDLE": + pressed="middle" + elif nextstatus=="DELETE": + pressed="remove" + elif nextstatus=="REC": + pressed="record_now" + elif nextstatus=="STOP": + if recording == True: + pressed="record" + elif nextstatus=="STOPRETAKE": + if recording == True: + pressed="retake" + elif nextstatus=="RECSOUND": + if recording==False: + pressed="record" + onlysound=True + elif nextstatus=="PLACEHOLDER": + pressed="insert_shot" + elif nextstatus=="TAKEPLACEHOLDER": + pressed="insert_take" + elif nextstatus=="NEWSCENE": + pressed="new_scene" + elif "NEWFILM:" in nextstatus: + newfilmname = nextstatus.split(':')[1] + pressed="new_film" + elif "SYNCIP:" in nextstatus: + pressed=nextstatus + elif "SYNCDONE" in nextstatus: + pressed=nextstatus + elif "RETAKE:" in nextstatus: + pressed=nextstatus + elif "SCENE:" in nextstatus: + pressed=nextstatus + elif "SHOT:" in nextstatus: + pressed=nextstatus + elif "REMOVE:" in nextstatus: + pressed=nextstatus + elif "Q:" in nextstatus: + pressed=nextstatus + elif "MAKEPLACEHOLDERS:" in nextstatus: + pressed=nextstatus + #print(nextstatus) + except: + #print('process not found') + pass + + with term.cbreak(): + val = term.inkey(timeout=0) + if val.is_sequence: + event = val.name + #print(event) + flushbutton() + elif val: + event = val + #print(event) + flushbutton() + else: + event = '' + keydelay = 0.08 + if i2cbuttons == True: + readbus = bus.read_byte_data(DEVICE,GPIOB) + readbus2 = bus.read_byte_data(DEVICE,GPIOA) + if readbus == 0: + readbus = 255 + if readbus2 == 0: + readbus2 = 247 + if readbus != 255: + print('i2cbutton readbus pressed: ' + str(readbus)) + if readbus2 != 247: + print('i2cbutton readbus2 pressed: ' + str(readbus2)) + else: + readbus = 255 + readbus2 = 247 + if buttonpressed == False: + #if event != '': + # print(term.clear+term.home) + if event == 27: + pressed = 'quit' + elif event == 'KEY_ENTER' or event == 10 or event == 13 or (readbus == 247 and readbus2 == 247): + pressed = 'middle' + elif event == 'KEY_UP' or (readbus == 191 and readbus2 == 247): + pressed = 'up' + elif event == 'KEY_DOWN' or (readbus == 254 and readbus2 == 247): + pressed = 'down' + elif event == 'KEY_LEFT' or (readbus == 239 and readbus2 == 247): + pressed = 'left' + elif event == 'KEY_RIGHT' or (readbus == 251 and readbus2 == 247): + pressed = 'right' + elif event == 'KEY_PGUP' or event == ' ' or (readbus == 127 and readbus2 == 247): + pressed = 'record' + elif event == 'KEY_PGDOWN' or (readbus == 253 and readbus2 == 247): + pressed = 'retake' + elif event == 'KEY_TAB' or readbus2 == 246: + pressed = 'view' + elif event == 'KEY_DELETE' or (readbus == 223 and readbus2 == 247): + pressed = 'remove' + elif event == 'KEY_BACKSPACE': + pressed = 'remove' + elif event == 'P' or (readbus2 == 245 and readbus == 191): + pressed = 'peak' + elif event == 'S' or (readbus2 == 245 and readbus == 223): + pressed = 'screen' + elif event == 'A' or (readbus2 == 245 and readbus == 127): + pressed = 'showmenu' + elif event == (readbus2 == 245 and readbus == 239): + pressed = 'changemode' + elif event == 'H' or (readbus2 == 245 and readbus == 247): + pressed = 'showhelp' + elif event == 'I' or (readbus2 == 245 and readbus == 253): + pressed = 'insert' + elif event == 'C' or (readbus2 == 244): + pressed = 'copy' + elif event == 'M' or (readbus2 == 245 and readbus == 254): + pressed = 'move' + #elif readbus2 == 247: + # pressed = 'shutdown' + #if pressed != '': + #print(pressed) + buttontime = time.time() + holdbutton = pressed + buttonpressed = True + if readbus == 255 and event == '' and nextstatus == '' : + buttonpressed = False + if float(time.time() - buttontime) > 0.2 and buttonpressed == True: + if holdbutton == 'up' or holdbutton == 'down' or holdbutton == 'right' or holdbutton == 'left' or holdbutton == 'shutdown' or holdbutton == 'remove': + pressed = holdbutton + keydelay = 0.02 + if time.time() - buttontime > 2 and buttonpressed == True: + keydelay = 0.02 + if time.time() - buttontime > 4 and buttonpressed == True: + keydelay = 0.01 + return pressed, buttonpressed, buttontime, holdbutton, event, keydelay + +def startinterface(): + call(['./startinterface.sh &'], shell = True) + +def stopinterface(camera): + try: + camera.stop_preview() + camera.close() + except: + print('no camera to close') + os.system('pkill arecord') + os.system('pkill startinterface') + os.system('pkill gonzopigui') + #run_command('sudo systemctl stop apache2') + return camera + +def startcamera(lens, fps): + global camera_model, fps_selection, fps_selected, cammode + camera = picamera.PiCamera() + if cammode == 'film': + reso=(1920,1080) + elif cammode == 'picture': + reso=(4056,3040) + camera.resolution = reso #tested modes 1920x816, 1296x552/578, v2 1640x698, 1640x1232, hqbinned 2028x1080, full 4056x3040 + #Background image + underlay = None + bakgimg = gonzopifolder + '/extras/bakg.jpg' + displaybakg(camera, bakgimg, underlay, 2) + #lensshade = '' + #npzfile = np.load('lenses/' + lens) + #lensshade = npzfile['lens_shading_table'] + # + #camera frame rate sync to audio clock + # + camera_model, camera_revision , filmfolder = getconfig(camera) + if os.path.isdir(filmfolder) == False: + os.makedirs(filmfolder) + # v1 = 'ov5647' + # v2 = ? + logger.info("picamera version is: " + camera_model + ' ' + camera_revision) + if camera_model == 'imx219': + table = read_table('lenses/' + lens) + camera.lens_shading_table = table + camera.framerate = 24.999 + elif camera_model == 'ov5647': + table = read_table('lenses/' + lens) + camera.lens_shading_table = table + # Different versions of ov5647 with different clock speeds, need to make a config file + # if there's more frames then the video will be longer when converting it to 25 fps, + # I try to get it as perfect as possible with trial and error. + # ov5647 Rev C + if camera_revision == 'rev.C': + camera.framerate = 26.03 + # ov5647 Rev D" + if camera_revision == 'rev.D': + camera.framerate = 23.15 + elif camera_model == 'imx477': + fps_selection=[5,15,24.985,35,49] + fps=fps_selection[fps_selected] + camera.framerate = fps + else: + camera.framerate = fps + camera.crop = (0, 0, 1.0, 1.0) + #stabilization does not work in firmware + #camera.video_stabilization = True + camera.led = False + #lens_shading_table = np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32 + #camera.lens_shading_table = lens_shading_table + camera.start_preview() + camera.awb_mode = 'auto' + time.sleep(1) + return camera + +def gonzopiserver(state): + #Gonzopi server + if state == True: + #Try to run apache + try: + run_command('sudo systemctl start apache2') + return 'on' + except: + writemessage("could not run gonzopi server") + time.sleep(2) + return 'off' + if state == False: + run_command('sudo systemctl stop apache2') + return 'off' + +if __name__ == '__main__': + import sys + try: + main() + except: + os.system('pkill arecord') + os.system('pkill startinterface') + os.system('pkill gonzopigui') + print('Unexpected error : ', sys.exc_info()[0], sys.exc_info()[1]) diff --git a/gui/Makefile.include b/gui/Makefile.include @@ -0,0 +1,28 @@ + +CFLAGS+=-DSTANDALONE -D__STDC_CONSTANT_MACROS -D__STDC_LIMIT_MACROS -DTARGET_POSIX -D_LINUX -fPIC -DPIC -D_REENTRANT -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -U_FORTIFY_SOURCE -Wall -g -DHAVE_LIBOPENMAX=2 -DOMX -DOMX_SKIP64BIT -ftree-vectorize -pipe -DUSE_EXTERNAL_OMX -DHAVE_LIBBCM_HOST -DUSE_EXTERNAL_LIBBCM_HOST -DUSE_VCHIQ_ARM -Wno-psabi + +LDFLAGS+=-L$(SDKSTAGE)/opt/vc/lib/ -lbrcmGLESv2 -lbrcmEGL -lopenmaxil -lbcm_host -lvcos -lvchiq_arm -lpthread -lrt -lm -L$(SDKSTAGE)/home/pi/tarinaretake/gui/libs/ilclient -L$(SDKSTAGE)/home/pi/tarinaretake/gui/libs/vgfont + +INCLUDES+=-I$(SDKSTAGE)/opt/vc/include/ -I$(SDKSTAGE)/opt/vc/include/interface/vcos/pthreads -I$(SDKSTAGE)/opt/vc/include/interface/vmcs_host/linux -I./ -I$(SDKSTAGE)/home/pi/tarinaretake/gui/libs/ilclient -I$(SDKSTAGE)/home/pi/tarinaretake/gui/libs/vgfont + +all: $(BIN) $(LIB) + +%.o: %.c + @rm -f $@ + $(CC) $(CFLAGS) $(INCLUDES) -g -c $< -o $@ -Wno-deprecated-declarations + +%.o: %.cpp + @rm -f $@ + $(CXX) $(CFLAGS) $(INCLUDES) -g -c $< -o $@ -Wno-deprecated-declarations + +%.bin: $(OBJS) + $(CC) -o $@ -Wl,--whole-archive $(OBJS) $(LDFLAGS) -Wl,--no-whole-archive -rdynamic + +%.a: $(OBJS) + $(AR) r $@ $^ + +clean: + for i in $(OBJS); do (if test -e "$$i"; then ( rm $$i ); fi ); done + @rm -f $(BIN) $(LIB) + + diff --git a/gui/VeraMono.ttf b/gui/VeraMono.ttf Binary files differ. diff --git a/gui/libs/ilclient/Makefile b/gui/libs/ilclient/Makefile @@ -0,0 +1,5 @@ +OBJS=ilclient.o ilcore.o +LIB=libilclient.a + +include ../../Makefile.include + diff --git a/gui/libs/ilclient/ilclient.c b/gui/libs/ilclient/ilclient.c @@ -0,0 +1,1836 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + * \file + * + * \brief This API defines helper functions for writing IL clients. + * + * This file defines an IL client side library. This is useful when + * writing IL clients, since there tends to be much repeated and + * common code across both single and multiple clients. This library + * seeks to remove that common code and abstract some of the + * interactions with components. There is a wrapper around a + * component and tunnel, and some operations can be done on lists of + * these. The callbacks from components are handled, and specific + * events can be checked or waited for. +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <ctype.h> +#include <assert.h> + +#include "interface/vcos/vcos.h" +#include "interface/vcos/vcos_logging.h" +#include "interface/vmcs_host/vchost.h" + +#include "IL/OMX_Broadcom.h" +#include "ilclient.h" + +#define VCOS_LOG_CATEGORY (&ilclient_log_category) + +#ifndef ILCLIENT_THREAD_DEFAULT_STACK_SIZE +#define ILCLIENT_THREAD_DEFAULT_STACK_SIZE (6<<10) +#endif + +static VCOS_LOG_CAT_T ilclient_log_category; + +/****************************************************************************** +Static data and types used only in this file. +******************************************************************************/ + +struct _ILEVENT_T { + OMX_EVENTTYPE eEvent; + OMX_U32 nData1; + OMX_U32 nData2; + OMX_PTR pEventData; + struct _ILEVENT_T *next; +}; + +#define NUM_EVENTS 100 +struct _ILCLIENT_T { + ILEVENT_T *event_list; + VCOS_SEMAPHORE_T event_sema; + ILEVENT_T event_rep[NUM_EVENTS]; + + ILCLIENT_CALLBACK_T port_settings_callback; + void *port_settings_callback_data; + ILCLIENT_CALLBACK_T eos_callback; + void *eos_callback_data; + ILCLIENT_CALLBACK_T error_callback; + void *error_callback_data; + ILCLIENT_BUFFER_CALLBACK_T fill_buffer_done_callback; + void *fill_buffer_done_callback_data; + ILCLIENT_BUFFER_CALLBACK_T empty_buffer_done_callback; + void *empty_buffer_done_callback_data; + ILCLIENT_CALLBACK_T configchanged_callback; + void *configchanged_callback_data; +}; + +struct _COMPONENT_T { + OMX_HANDLETYPE comp; + ILCLIENT_CREATE_FLAGS_T flags; + VCOS_SEMAPHORE_T sema; + VCOS_EVENT_FLAGS_T event; + struct _COMPONENT_T *related; + OMX_BUFFERHEADERTYPE *out_list; + OMX_BUFFERHEADERTYPE *in_list; + char name[32]; + char bufname[32]; + unsigned int error_mask; + unsigned int private; + ILEVENT_T *list; + ILCLIENT_T *client; +}; + +#define random_wait() +static char *states[] = {"Invalid", "Loaded", "Idle", "Executing", "Pause", "WaitingForResources"}; + +typedef enum { + ILCLIENT_ERROR_UNPOPULATED = 0x1, + ILCLIENT_ERROR_SAMESTATE = 0x2, + ILCLIENT_ERROR_BADPARAMETER = 0x4 +} ILERROR_MASK_T; + +/****************************************************************************** +Static functions. +******************************************************************************/ + +static OMX_ERRORTYPE ilclient_empty_buffer_done(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_BUFFERHEADERTYPE* pBuffer); +static OMX_ERRORTYPE ilclient_empty_buffer_done_error(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_BUFFERHEADERTYPE* pBuffer); +static OMX_ERRORTYPE ilclient_fill_buffer_done(OMX_OUT OMX_HANDLETYPE hComponent, + OMX_OUT OMX_PTR pAppData, + OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer); +static OMX_ERRORTYPE ilclient_fill_buffer_done_error(OMX_OUT OMX_HANDLETYPE hComponent, + OMX_OUT OMX_PTR pAppData, + OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer); +static OMX_ERRORTYPE ilclient_event_handler(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_EVENTTYPE eEvent, + OMX_IN OMX_U32 nData1, + OMX_IN OMX_U32 nData2, + OMX_IN OMX_PTR pEventData); +static void ilclient_lock_events(ILCLIENT_T *st); +static void ilclient_unlock_events(ILCLIENT_T *st); + +/****************************************************************************** +Global functions +******************************************************************************/ + +/*********************************************************** + * Name: ilclient_init + * + * Description: Creates ilclient pointer + * + * Returns: pointer to client structure + ***********************************************************/ +ILCLIENT_T *ilclient_init() +{ + ILCLIENT_T *st = vcos_malloc(sizeof(ILCLIENT_T), "ilclient"); + int i; + + if (!st) + return NULL; + + vcos_log_set_level(VCOS_LOG_CATEGORY, VCOS_LOG_WARN); + vcos_log_register("ilclient", VCOS_LOG_CATEGORY); + + memset(st, 0, sizeof(ILCLIENT_T)); + + i = vcos_semaphore_create(&st->event_sema, "il:event", 1); + vc_assert(i == VCOS_SUCCESS); + + ilclient_lock_events(st); + st->event_list = NULL; + for (i=0; i<NUM_EVENTS; i++) + { + st->event_rep[i].eEvent = -1; // mark as unused + st->event_rep[i].next = st->event_list; + st->event_list = st->event_rep+i; + } + ilclient_unlock_events(st); + return st; +} + +/*********************************************************** + * Name: ilclient_destroy + * + * Description: frees client state + * + * Returns: void + ***********************************************************/ +void ilclient_destroy(ILCLIENT_T *st) +{ + vcos_semaphore_delete(&st->event_sema); + vcos_free(st); + vcos_log_unregister(VCOS_LOG_CATEGORY); +} + +/*********************************************************** + * Name: ilclient_set_port_settings_callback + * + * Description: sets the callback used when receiving port settings + * changed messages. The data field in the callback function will be + * the port index reporting the message. + * + * Returns: void + ***********************************************************/ +void ilclient_set_port_settings_callback(ILCLIENT_T *st, ILCLIENT_CALLBACK_T func, void *userdata) +{ + st->port_settings_callback = func; + st->port_settings_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_set_eos_callback + * + * Description: sets the callback used when receiving eos flags. The + * data parameter in the callback function will be the port index + * reporting an eos flag. + * + * Returns: void + ***********************************************************/ +void ilclient_set_eos_callback(ILCLIENT_T *st, ILCLIENT_CALLBACK_T func, void *userdata) +{ + st->eos_callback = func; + st->eos_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_set_error_callback + * + * Description: sets the callback used when receiving error events. + * The data parameter in the callback function will be the error code + * being reported. + * + * Returns: void + ***********************************************************/ +void ilclient_set_error_callback(ILCLIENT_T *st, ILCLIENT_CALLBACK_T func, void *userdata) +{ + st->error_callback = func; + st->error_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_set_fill_buffer_done_callback + * + * Description: sets the callback used when receiving + * fill_buffer_done event + * + * Returns: void + ***********************************************************/ +void ilclient_set_fill_buffer_done_callback(ILCLIENT_T *st, ILCLIENT_BUFFER_CALLBACK_T func, void *userdata) +{ + st->fill_buffer_done_callback = func; + st->fill_buffer_done_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_set_empty_buffer_done_callback + * + * Description: sets the callback used when receiving + * empty_buffer_done event + * + * Returns: void + ***********************************************************/ +void ilclient_set_empty_buffer_done_callback(ILCLIENT_T *st, ILCLIENT_BUFFER_CALLBACK_T func, void *userdata) +{ + st->empty_buffer_done_callback = func; + st->empty_buffer_done_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_set_configchanged_callback + * + * Description: sets the callback used when a config changed + * event is received + * + * Returns: void + ***********************************************************/ +void ilclient_set_configchanged_callback(ILCLIENT_T *st, ILCLIENT_CALLBACK_T func, void *userdata) +{ + st->configchanged_callback = func; + st->configchanged_callback_data = userdata; +} + +/*********************************************************** + * Name: ilclient_create_component + * + * Description: initialises a component state structure and creates + * the IL component. + * + * Returns: 0 on success, -1 on failure + ***********************************************************/ +int ilclient_create_component(ILCLIENT_T *client, COMPONENT_T **comp, char *name, + ILCLIENT_CREATE_FLAGS_T flags) +{ + OMX_CALLBACKTYPE callbacks; + OMX_ERRORTYPE error; + char component_name[128]; + int32_t status; + + *comp = vcos_malloc(sizeof(COMPONENT_T), "il:comp"); + if(!*comp) + return -1; + + memset(*comp, 0, sizeof(COMPONENT_T)); + +#define COMP_PREFIX "OMX.broadcom." + + status = vcos_event_flags_create(&(*comp)->event,"il:comp"); + vc_assert(status == VCOS_SUCCESS); + status = vcos_semaphore_create(&(*comp)->sema, "il:comp", 1); + vc_assert(status == VCOS_SUCCESS); + (*comp)->client = client; + + vcos_snprintf((*comp)->name, sizeof((*comp)->name), "cl:%s", name); + vcos_snprintf((*comp)->bufname, sizeof((*comp)->bufname), "cl:%s buffer", name); + vcos_snprintf(component_name, sizeof(component_name), "%s%s", COMP_PREFIX, name); + + (*comp)->flags = flags; + + callbacks.EventHandler = ilclient_event_handler; + callbacks.EmptyBufferDone = flags & ILCLIENT_ENABLE_INPUT_BUFFERS ? ilclient_empty_buffer_done : ilclient_empty_buffer_done_error; + callbacks.FillBufferDone = flags & ILCLIENT_ENABLE_OUTPUT_BUFFERS ? ilclient_fill_buffer_done : ilclient_fill_buffer_done_error; + + error = OMX_GetHandle(&(*comp)->comp, component_name, *comp, &callbacks); + + if (error == OMX_ErrorNone) + { + OMX_UUIDTYPE uid; + char name[128]; + OMX_VERSIONTYPE compVersion, specVersion; + + if(OMX_GetComponentVersion((*comp)->comp, name, &compVersion, &specVersion, &uid) == OMX_ErrorNone) + { + char *p = (char *) uid + strlen(COMP_PREFIX); + + vcos_snprintf((*comp)->name, sizeof((*comp)->name), "cl:%s", p); + (*comp)->name[sizeof((*comp)->name)-1] = 0; + vcos_snprintf((*comp)->bufname, sizeof((*comp)->bufname), "cl:%s buffer", p); + (*comp)->bufname[sizeof((*comp)->bufname)-1] = 0; + } + + if(flags & (ILCLIENT_DISABLE_ALL_PORTS | ILCLIENT_OUTPUT_ZERO_BUFFERS)) + { + OMX_PORT_PARAM_TYPE ports; + OMX_INDEXTYPE types[] = {OMX_IndexParamAudioInit, OMX_IndexParamVideoInit, OMX_IndexParamImageInit, OMX_IndexParamOtherInit}; + int i; + + ports.nSize = sizeof(OMX_PORT_PARAM_TYPE); + ports.nVersion.nVersion = OMX_VERSION; + + for(i=0; i<4; i++) + { + OMX_ERRORTYPE error = OMX_GetParameter((*comp)->comp, types[i], &ports); + if(error == OMX_ErrorNone) + { + uint32_t j; + for(j=0; j<ports.nPorts; j++) + { + if(flags & ILCLIENT_DISABLE_ALL_PORTS) + ilclient_disable_port(*comp, ports.nStartPortNumber+j); + + if(flags & ILCLIENT_OUTPUT_ZERO_BUFFERS) + { + OMX_PARAM_PORTDEFINITIONTYPE portdef; + portdef.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); + portdef.nVersion.nVersion = OMX_VERSION; + portdef.nPortIndex = ports.nStartPortNumber+j; + if(OMX_GetParameter((*comp)->comp, OMX_IndexParamPortDefinition, &portdef) == OMX_ErrorNone) + { + if(portdef.eDir == OMX_DirOutput && portdef.nBufferCountActual > 0) + { + portdef.nBufferCountActual = 0; + OMX_SetParameter((*comp)->comp, OMX_IndexParamPortDefinition, &portdef); + } + } + } + } + } + } + } + return 0; + } + else + { + vcos_event_flags_delete(&(*comp)->event); + vcos_semaphore_delete(&(*comp)->sema); + vcos_free(*comp); + *comp = NULL; + return -1; + } +} + +/*********************************************************** + * Name: ilclient_remove_event + * + * Description: Removes an event from a component event list. ignore1 + * and ignore2 are flags indicating whether to not match on nData1 and + * nData2 respectively. + * + * Returns: 0 if the event was removed. -1 if no matching event was + * found. + ***********************************************************/ +int ilclient_remove_event(COMPONENT_T *st, OMX_EVENTTYPE eEvent, + OMX_U32 nData1, int ignore1, OMX_IN OMX_U32 nData2, int ignore2) +{ + ILEVENT_T *cur, *prev; + uint32_t set; + ilclient_lock_events(st->client); + + cur = st->list; + prev = NULL; + + while (cur && !(cur->eEvent == eEvent && (ignore1 || cur->nData1 == nData1) && (ignore2 || cur->nData2 == nData2))) + { + prev = cur; + cur = cur->next; + } + + if (cur == NULL) + { + ilclient_unlock_events(st->client); + return -1; + } + + if (prev == NULL) + st->list = cur->next; + else + prev->next = cur->next; + + // add back into spare list + cur->next = st->client->event_list; + st->client->event_list = cur; + cur->eEvent = -1; // mark as unused + + // if we're removing an OMX_EventError or OMX_EventParamOrConfigChanged event, then clear the error bit from the eventgroup, + // since the user might have been notified through the error callback, and then + // can't clear the event bit - this will then cause problems the next time they + // wait for an error. + if(eEvent == OMX_EventError) + vcos_event_flags_get(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR_CONSUME, 0, &set); + else if(eEvent == OMX_EventParamOrConfigChanged) + vcos_event_flags_get(&st->event, ILCLIENT_CONFIG_CHANGED, VCOS_OR_CONSUME, 0, &set); + + ilclient_unlock_events(st->client); + return 0; +} + +/*********************************************************** + * Name: ilclient_state_transition + * + * Description: Transitions a null terminated list of IL components to + * a given state. All components are told to transition in a random + * order before any are checked for transition completion. + * + * Returns: void + ***********************************************************/ +void ilclient_state_transition(COMPONENT_T *list[], OMX_STATETYPE state) +{ + OMX_ERRORTYPE error; + int i, num; + uint32_t set; + + num=0; + while (list[num]) + num++; + + // if we transition the supplier port first, it will call freebuffer on the non + // supplier, which will correctly signal a port unpopulated error. We want to + // ignore these errors. + if (state == OMX_StateLoaded) + for (i=0; i<num; i++) + list[i]->error_mask |= ILCLIENT_ERROR_UNPOPULATED; + for (i=0; i<num; i++) + list[i]->private = ((rand() >> 13) & 0xff)+1; + + for (i=0; i<num; i++) + { + // transition the components in a random order + int j, min = -1; + for (j=0; j<num; j++) + if (list[j]->private && (min == -1 || list[min]->private > list[j]->private)) + min = j; + + list[min]->private = 0; + + random_wait(); + //Clear error event for this component + vcos_event_flags_get(&list[min]->event, ILCLIENT_EVENT_ERROR, VCOS_OR_CONSUME, 0, &set); + + error = OMX_SendCommand(list[min]->comp, OMX_CommandStateSet, state, NULL); + vc_assert(error == OMX_ErrorNone); + } + + random_wait(); + + for (i=0; i<num; i++) + if(ilclient_wait_for_command_complete(list[i], OMX_CommandStateSet, state) < 0) + vc_assert(0); + + if (state == OMX_StateLoaded) + for (i=0; i<num; i++) + list[i]->error_mask &= ~ILCLIENT_ERROR_UNPOPULATED; +} + +/*********************************************************** + * Name: ilclient_teardown_tunnels + * + * Description: tears down a null terminated list of tunnels. + * + * Returns: void + ***********************************************************/ +void ilclient_teardown_tunnels(TUNNEL_T *tunnel) +{ + int i; + OMX_ERRORTYPE error; + + i=0;; + while (tunnel[i].source) + { + error = OMX_SetupTunnel(tunnel[i].source->comp, tunnel[i].source_port, NULL, 0); + vc_assert(error == OMX_ErrorNone); + + error = OMX_SetupTunnel(tunnel[i].sink->comp, tunnel[i].sink_port, NULL, 0); + vc_assert(error == OMX_ErrorNone); + i++; + } +} + +/*********************************************************** + * Name: ilclient_disable_tunnel + * + * Description: disables a tunnel by disabling the ports. Allows + * ports to signal same state error if they were already disabled. + * + * Returns: void + ***********************************************************/ +void ilclient_disable_tunnel(TUNNEL_T *tunnel) +{ + OMX_ERRORTYPE error; + + if(tunnel->source == 0 || tunnel->sink == 0) + return; + + tunnel->source->error_mask |= ILCLIENT_ERROR_UNPOPULATED; + tunnel->sink->error_mask |= ILCLIENT_ERROR_UNPOPULATED; + + error = OMX_SendCommand(tunnel->source->comp, OMX_CommandPortDisable, tunnel->source_port, NULL); + vc_assert(error == OMX_ErrorNone); + + error = OMX_SendCommand(tunnel->sink->comp, OMX_CommandPortDisable, tunnel->sink_port, NULL); + vc_assert(error == OMX_ErrorNone); + + if(ilclient_wait_for_command_complete(tunnel->source, OMX_CommandPortDisable, tunnel->source_port) < 0) + vc_assert(0); + + if(ilclient_wait_for_command_complete(tunnel->sink, OMX_CommandPortDisable, tunnel->sink_port) < 0) + vc_assert(0); + + tunnel->source->error_mask &= ~ILCLIENT_ERROR_UNPOPULATED; + tunnel->sink->error_mask &= ~ILCLIENT_ERROR_UNPOPULATED; +} + +/*********************************************************** + * Name: ilclient_enable_tunnel + * + * Description: enables a tunnel by enabling the ports + * + * Returns: 0 on success, -1 on failure + ***********************************************************/ +int ilclient_enable_tunnel(TUNNEL_T *tunnel) +{ + OMX_STATETYPE state; + OMX_ERRORTYPE error; + + ilclient_debug_output("ilclient: enable tunnel from %x/%d to %x/%d", + tunnel->source, tunnel->source_port, + tunnel->sink, tunnel->sink_port); + + error = OMX_SendCommand(tunnel->source->comp, OMX_CommandPortEnable, tunnel->source_port, NULL); + vc_assert(error == OMX_ErrorNone); + + error = OMX_SendCommand(tunnel->sink->comp, OMX_CommandPortEnable, tunnel->sink_port, NULL); + vc_assert(error == OMX_ErrorNone); + + // to complete, the sink component can't be in loaded state + error = OMX_GetState(tunnel->sink->comp, &state); + vc_assert(error == OMX_ErrorNone); + if (state == OMX_StateLoaded) + { + int ret = 0; + + if(ilclient_wait_for_command_complete(tunnel->sink, OMX_CommandPortEnable, tunnel->sink_port) != 0 || + OMX_SendCommand(tunnel->sink->comp, OMX_CommandStateSet, OMX_StateIdle, NULL) != OMX_ErrorNone || + (ret = ilclient_wait_for_command_complete_dual(tunnel->sink, OMX_CommandStateSet, OMX_StateIdle, tunnel->source)) < 0) + { + if(ret == -2) + { + // the error was reported fom the source component: clear this error and disable the sink component + ilclient_wait_for_command_complete(tunnel->source, OMX_CommandPortEnable, tunnel->source_port); + ilclient_disable_port(tunnel->sink, tunnel->sink_port); + } + + ilclient_debug_output("ilclient: could not change component state to IDLE"); + ilclient_disable_port(tunnel->source, tunnel->source_port); + return -1; + } + } + else + { + if (ilclient_wait_for_command_complete(tunnel->sink, OMX_CommandPortEnable, tunnel->sink_port) != 0) + { + ilclient_debug_output("ilclient: could not change sink port %d to enabled", tunnel->sink_port); + + //Oops failed to enable the sink port + ilclient_disable_port(tunnel->source, tunnel->source_port); + //Clean up the port enable event from the source port. + ilclient_wait_for_event(tunnel->source, OMX_EventCmdComplete, + OMX_CommandPortEnable, 0, tunnel->source_port, 0, + ILCLIENT_PORT_ENABLED | ILCLIENT_EVENT_ERROR, VCOS_EVENT_FLAGS_SUSPEND); + return -1; + } + } + + if(ilclient_wait_for_command_complete(tunnel->source, OMX_CommandPortEnable, tunnel->source_port) != 0) + { + ilclient_debug_output("ilclient: could not change source port %d to enabled", tunnel->source_port); + + //Failed to enable the source port + ilclient_disable_port(tunnel->sink, tunnel->sink_port); + return -1; + } + + return 0; +} + + +/*********************************************************** + * Name: ilclient_flush_tunnels + * + * Description: flushes all ports used in a null terminated list of + * tunnels. max specifies the maximum number of tunnels to flush from + * the list, where max=0 means all tunnels. + * + * Returns: void + ***********************************************************/ +void ilclient_flush_tunnels(TUNNEL_T *tunnel, int max) +{ + OMX_ERRORTYPE error; + int i; + + i=0; + while (tunnel[i].source && (max == 0 || i < max)) + { + error = OMX_SendCommand(tunnel[i].source->comp, OMX_CommandFlush, tunnel[i].source_port, NULL); + vc_assert(error == OMX_ErrorNone); + + error = OMX_SendCommand(tunnel[i].sink->comp, OMX_CommandFlush, tunnel[i].sink_port, NULL); + vc_assert(error == OMX_ErrorNone); + + ilclient_wait_for_event(tunnel[i].source, OMX_EventCmdComplete, + OMX_CommandFlush, 0, tunnel[i].source_port, 0, + ILCLIENT_PORT_FLUSH, VCOS_EVENT_FLAGS_SUSPEND); + ilclient_wait_for_event(tunnel[i].sink, OMX_EventCmdComplete, + OMX_CommandFlush, 0, tunnel[i].sink_port, 0, + ILCLIENT_PORT_FLUSH, VCOS_EVENT_FLAGS_SUSPEND); + i++; + } +} + + +/*********************************************************** + * Name: ilclient_return_events + * + * Description: Returns all events from a component event list to the + * list of unused event structures. + * + * Returns: void + ***********************************************************/ +void ilclient_return_events(COMPONENT_T *comp) +{ + ilclient_lock_events(comp->client); + while (comp->list) + { + ILEVENT_T *next = comp->list->next; + comp->list->next = comp->client->event_list; + comp->client->event_list = comp->list; + comp->list = next; + } + ilclient_unlock_events(comp->client); +} + +/*********************************************************** + * Name: ilclient_cleanup_components + * + * Description: frees all components from a null terminated list and + * deletes resources used in component state structure. + * + * Returns: void + ***********************************************************/ +void ilclient_cleanup_components(COMPONENT_T *list[]) +{ + int i; + OMX_ERRORTYPE error; + + i=0; + while (list[i]) + { + ilclient_return_events(list[i]); + if (list[i]->comp) + { + error = OMX_FreeHandle(list[i]->comp); + + vc_assert(error == OMX_ErrorNone); + } + i++; + } + + i=0; + while (list[i]) + { + vcos_event_flags_delete(&list[i]->event); + vcos_semaphore_delete(&list[i]->sema); + vcos_free(list[i]); + list[i] = NULL; + i++; + } +} + +/*********************************************************** + * Name: ilclient_change_component_state + * + * Description: changes the state of a single component. Note: this + * may not be suitable if the component is tunnelled and requires + * connected components to also change state. + * + * Returns: 0 on success, -1 on failure (note - trying to change to + * the same state which causes a OMX_ErrorSameState is treated as + * success) + ***********************************************************/ +int ilclient_change_component_state(COMPONENT_T *comp, OMX_STATETYPE state) +{ + OMX_ERRORTYPE error; + error = OMX_SendCommand(comp->comp, OMX_CommandStateSet, state, NULL); + vc_assert(error == OMX_ErrorNone); + if(ilclient_wait_for_command_complete(comp, OMX_CommandStateSet, state) < 0) + { + ilclient_debug_output("ilclient: could not change component state to %d", state); + ilclient_remove_event(comp, OMX_EventError, 0, 1, 0, 1); + return -1; + } + return 0; +} + +/*********************************************************** + * Name: ilclient_disable_port + * + * Description: disables a port on a given component. + * + * Returns: void + ***********************************************************/ +void ilclient_disable_port(COMPONENT_T *comp, int portIndex) +{ + OMX_ERRORTYPE error; + error = OMX_SendCommand(comp->comp, OMX_CommandPortDisable, portIndex, NULL); + vc_assert(error == OMX_ErrorNone); + if(ilclient_wait_for_command_complete(comp, OMX_CommandPortDisable, portIndex) < 0) + vc_assert(0); +} + +/*********************************************************** + * Name: ilclient_enabled_port + * + * Description: enables a port on a given component. + * + * Returns: void + ***********************************************************/ +void ilclient_enable_port(COMPONENT_T *comp, int portIndex) +{ + OMX_ERRORTYPE error; + error = OMX_SendCommand(comp->comp, OMX_CommandPortEnable, portIndex, NULL); + vc_assert(error == OMX_ErrorNone); + if(ilclient_wait_for_command_complete(comp, OMX_CommandPortEnable, portIndex) < 0) + vc_assert(0); +} + + +/*********************************************************** + * Name: ilclient_enable_port_buffers + * + * Description: enables a port on a given component which requires + * buffers to be supplied by the client. + * + * Returns: void + ***********************************************************/ +int ilclient_enable_port_buffers(COMPONENT_T *comp, int portIndex, + ILCLIENT_MALLOC_T ilclient_malloc, + ILCLIENT_FREE_T ilclient_free, + void *private) +{ + OMX_ERRORTYPE error; + OMX_PARAM_PORTDEFINITIONTYPE portdef; + OMX_BUFFERHEADERTYPE *list = NULL, **end = &list; + OMX_STATETYPE state; + int i; + + memset(&portdef, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); + portdef.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); + portdef.nVersion.nVersion = OMX_VERSION; + portdef.nPortIndex = portIndex; + + // work out buffer requirements, check port is in the right state + error = OMX_GetParameter(comp->comp, OMX_IndexParamPortDefinition, &portdef); + if(error != OMX_ErrorNone || portdef.bEnabled != OMX_FALSE || portdef.nBufferCountActual == 0 || portdef.nBufferSize == 0) + return -1; + + // check component is in the right state to accept buffers + error = OMX_GetState(comp->comp, &state); + if (error != OMX_ErrorNone || !(state == OMX_StateIdle || state == OMX_StateExecuting || state == OMX_StatePause)) + return -1; + + // send the command + error = OMX_SendCommand(comp->comp, OMX_CommandPortEnable, portIndex, NULL); + vc_assert(error == OMX_ErrorNone); + + for (i=0; i != portdef.nBufferCountActual; i++) + { + unsigned char *buf; + if(ilclient_malloc) + buf = ilclient_malloc(private, portdef.nBufferSize, portdef.nBufferAlignment, comp->bufname); + else + buf = vcos_malloc_aligned(portdef.nBufferSize, portdef.nBufferAlignment, comp->bufname); + + if(!buf) + break; + + error = OMX_UseBuffer(comp->comp, end, portIndex, NULL, portdef.nBufferSize, buf); + if(error != OMX_ErrorNone) + { + if(ilclient_free) + ilclient_free(private, buf); + else + vcos_free(buf); + + break; + } + end = (OMX_BUFFERHEADERTYPE **) &((*end)->pAppPrivate); + } + + // queue these buffers + vcos_semaphore_wait(&comp->sema); + + if(portdef.eDir == OMX_DirInput) + { + *end = comp->in_list; + comp->in_list = list; + } + else + { + *end = comp->out_list; + comp->out_list = list; + } + + vcos_semaphore_post(&comp->sema); + + if(i != portdef.nBufferCountActual || + ilclient_wait_for_command_complete(comp, OMX_CommandPortEnable, portIndex) < 0) + { + ilclient_disable_port_buffers(comp, portIndex, NULL, ilclient_free, private); + + // at this point the first command might have terminated with an error, which means that + // the port is disabled before the disable_port_buffers function is called, so we're left + // with the error bit set and an error event in the queue. Clear these now if they exist. + ilclient_remove_event(comp, OMX_EventError, 0, 1, 1, 0); + + return -1; + } + + // success + return 0; +} + + +/*********************************************************** + * Name: ilclient_disable_port_buffers + * + * Description: disables a port on a given component which has + * buffers supplied by the client. + * + * Returns: void + ***********************************************************/ +void ilclient_disable_port_buffers(COMPONENT_T *comp, int portIndex, + OMX_BUFFERHEADERTYPE *bufferList, + ILCLIENT_FREE_T ilclient_free, + void *private) +{ + OMX_ERRORTYPE error; + OMX_BUFFERHEADERTYPE *list = bufferList; + OMX_BUFFERHEADERTYPE **head, *clist, *prev; + OMX_PARAM_PORTDEFINITIONTYPE portdef; + int num; + + // get the buffers off the relevant queue + memset(&portdef, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); + portdef.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); + portdef.nVersion.nVersion = OMX_VERSION; + portdef.nPortIndex = portIndex; + + // work out buffer requirements, check port is in the right state + error = OMX_GetParameter(comp->comp, OMX_IndexParamPortDefinition, &portdef); + if(error != OMX_ErrorNone || portdef.bEnabled != OMX_TRUE || portdef.nBufferCountActual == 0 || portdef.nBufferSize == 0) + return; + + num = portdef.nBufferCountActual; + + error = OMX_SendCommand(comp->comp, OMX_CommandPortDisable, portIndex, NULL); + vc_assert(error == OMX_ErrorNone); + + while(num > 0) + { + VCOS_UNSIGNED set; + + if(list == NULL) + { + vcos_semaphore_wait(&comp->sema); + + // take buffers for this port off the relevant queue + head = portdef.eDir == OMX_DirInput ? &comp->in_list : &comp->out_list; + clist = *head; + prev = NULL; + + while(clist) + { + if((portdef.eDir == OMX_DirInput ? clist->nInputPortIndex : clist->nOutputPortIndex) == portIndex) + { + OMX_BUFFERHEADERTYPE *pBuffer = clist; + + if(!prev) + clist = *head = (OMX_BUFFERHEADERTYPE *) pBuffer->pAppPrivate; + else + clist = prev->pAppPrivate = (OMX_BUFFERHEADERTYPE *) pBuffer->pAppPrivate; + + pBuffer->pAppPrivate = list; + list = pBuffer; + } + else + { + prev = clist; + clist = (OMX_BUFFERHEADERTYPE *) &(clist->pAppPrivate); + } + } + + vcos_semaphore_post(&comp->sema); + } + + while(list) + { + void *buf = list->pBuffer; + OMX_BUFFERHEADERTYPE *next = list->pAppPrivate; + + error = OMX_FreeBuffer(comp->comp, portIndex, list); + vc_assert(error == OMX_ErrorNone); + + if(ilclient_free) + ilclient_free(private, buf); + else + vcos_free(buf); + + num--; + list = next; + } + + if(num) + { + OMX_U32 mask = ILCLIENT_PORT_DISABLED | ILCLIENT_EVENT_ERROR; + mask |= (portdef.eDir == OMX_DirInput ? ILCLIENT_EMPTY_BUFFER_DONE : ILCLIENT_FILL_BUFFER_DONE); + + // also wait for command complete/error in case we didn't have all the buffers allocated + vcos_event_flags_get(&comp->event, mask, VCOS_OR_CONSUME, -1, &set); + + if((set & ILCLIENT_EVENT_ERROR) && ilclient_remove_event(comp, OMX_EventError, 0, 1, 1, 0) >= 0) + return; + + if((set & ILCLIENT_PORT_DISABLED) && ilclient_remove_event(comp, OMX_EventCmdComplete, OMX_CommandPortDisable, 0, portIndex, 0) >= 0) + return; + } + } + + if(ilclient_wait_for_command_complete(comp, OMX_CommandPortDisable, portIndex) < 0) + vc_assert(0); +} + + +/*********************************************************** + * Name: ilclient_setup_tunnel + * + * Description: creates a tunnel between components that require that + * ports be inititially disabled, then enabled after tunnel setup. If + * timeout is non-zero, it will initially wait until a port settings + * changes message has been received by the output port. If port + * streams are supported by the output port, the requested port stream + * will be selected. + * + * Returns: 0 indicates success, negative indicates failure. + * -1: a timeout waiting for the parameter changed + * -2: an error was returned instead of parameter changed + * -3: no streams are available from this port + * -4: requested stream is not available from this port + * -5: the data format was not acceptable to the sink + ***********************************************************/ +int ilclient_setup_tunnel(TUNNEL_T *tunnel, unsigned int portStream, int timeout) +{ + OMX_ERRORTYPE error; + OMX_PARAM_U32TYPE param; + OMX_STATETYPE state; + int32_t status; + int enable_error; + + // source component must at least be idle, not loaded + error = OMX_GetState(tunnel->source->comp, &state); + vc_assert(error == OMX_ErrorNone); + if (state == OMX_StateLoaded && ilclient_change_component_state(tunnel->source, OMX_StateIdle) < 0) + return -2; + + // wait for the port parameter changed from the source port + if(timeout) + { + status = ilclient_wait_for_event(tunnel->source, OMX_EventPortSettingsChanged, + tunnel->source_port, 0, -1, 1, + ILCLIENT_PARAMETER_CHANGED | ILCLIENT_EVENT_ERROR, timeout); + + if (status < 0) + { + ilclient_debug_output( + "ilclient: timed out waiting for port settings changed on port %d", tunnel->source_port); + return status; + } + } + + // disable ports + ilclient_disable_tunnel(tunnel); + + // if this source port uses port streams, we need to select one of them before proceeding + // if getparameter causes an error that's fine, nothing needs selecting + param.nSize = sizeof(OMX_PARAM_U32TYPE); + param.nVersion.nVersion = OMX_VERSION; + param.nPortIndex = tunnel->source_port; + if (OMX_GetParameter(tunnel->source->comp, OMX_IndexParamNumAvailableStreams, &param) == OMX_ErrorNone) + { + if (param.nU32 == 0) + { + // no streams available + // leave the source port disabled, and return a failure + return -3; + } + if (param.nU32 <= portStream) + { + // requested stream not available + // no streams available + // leave the source port disabled, and return a failure + return -4; + } + + param.nU32 = portStream; + error = OMX_SetParameter(tunnel->source->comp, OMX_IndexParamActiveStream, &param); + vc_assert(error == OMX_ErrorNone); + } + + // now create the tunnel + error = OMX_SetupTunnel(tunnel->source->comp, tunnel->source_port, tunnel->sink->comp, tunnel->sink_port); + + enable_error = 0; + + if (error != OMX_ErrorNone || (enable_error=ilclient_enable_tunnel(tunnel)) < 0) + { + // probably format not compatible + error = OMX_SetupTunnel(tunnel->source->comp, tunnel->source_port, NULL, 0); + vc_assert(error == OMX_ErrorNone); + error = OMX_SetupTunnel(tunnel->sink->comp, tunnel->sink_port, NULL, 0); + vc_assert(error == OMX_ErrorNone); + + if(enable_error) + { + //Clean up the errors. This does risk removing an error that was nothing to do with this tunnel :-/ + ilclient_remove_event(tunnel->sink, OMX_EventError, 0, 1, 0, 1); + ilclient_remove_event(tunnel->source, OMX_EventError, 0, 1, 0, 1); + } + + ilclient_debug_output("ilclient: could not setup/enable tunnel (setup=0x%x,enable=%d)", + error, enable_error); + return -5; + } + + return 0; +} + +/*********************************************************** + * Name: ilclient_wait_for_event + * + * Description: waits for a given event to appear on a component event + * list. If not immediately present, will wait on that components + * event group for the given event flag. + * + * Returns: 0 indicates success, negative indicates failure. + * -1: a timeout was received. + * -2: an error event was received. + * -3: a config change event was received. + ***********************************************************/ +int ilclient_wait_for_event(COMPONENT_T *comp, OMX_EVENTTYPE event, + OMX_U32 nData1, int ignore1, OMX_IN OMX_U32 nData2, int ignore2, + int event_flag, int suspend) +{ + int32_t status; + uint32_t set; + + while (ilclient_remove_event(comp, event, nData1, ignore1, nData2, ignore2) < 0) + { + // if we want to be notified of errors, check the list for an error now + // before blocking, the event flag may have been cleared already. + if(event_flag & ILCLIENT_EVENT_ERROR) + { + ILEVENT_T *cur; + ilclient_lock_events(comp->client); + cur = comp->list; + while(cur && cur->eEvent != OMX_EventError) + cur = cur->next; + + if(cur) + { + // clear error flag + vcos_event_flags_get(&comp->event, ILCLIENT_EVENT_ERROR, VCOS_OR_CONSUME, 0, &set); + ilclient_unlock_events(comp->client); + return -2; + } + + ilclient_unlock_events(comp->client); + } + // check for config change event if we are asked to be notified of that + if(event_flag & ILCLIENT_CONFIG_CHANGED) + { + ILEVENT_T *cur; + ilclient_lock_events(comp->client); + cur = comp->list; + while(cur && cur->eEvent != OMX_EventParamOrConfigChanged) + cur = cur->next; + + ilclient_unlock_events(comp->client); + + if(cur) + return ilclient_remove_event(comp, event, nData1, ignore1, nData2, ignore2) == 0 ? 0 : -3; + } + + status = vcos_event_flags_get(&comp->event, event_flag, VCOS_OR_CONSUME, + suspend, &set); + if (status != 0) + return -1; + if (set & ILCLIENT_EVENT_ERROR) + return -2; + if (set & ILCLIENT_CONFIG_CHANGED) + return ilclient_remove_event(comp, event, nData1, ignore1, nData2, ignore2) == 0 ? 0 : -3; + } + + return 0; +} + + + +/*********************************************************** + * Name: ilclient_wait_for_command_complete_dual + * + * Description: Waits for an event signalling command completion. In + * this version we may also return failure if there is an error event + * that has terminated a command on a second component. + * + * Returns: 0 on success, -1 on failure of comp, -2 on failure of other + ***********************************************************/ +int ilclient_wait_for_command_complete_dual(COMPONENT_T *comp, OMX_COMMANDTYPE command, OMX_U32 nData2, COMPONENT_T *other) +{ + OMX_U32 mask = ILCLIENT_EVENT_ERROR; + int ret = 0; + + switch(command) { + case OMX_CommandStateSet: mask |= ILCLIENT_STATE_CHANGED; break; + case OMX_CommandPortDisable: mask |= ILCLIENT_PORT_DISABLED; break; + case OMX_CommandPortEnable: mask |= ILCLIENT_PORT_ENABLED; break; + default: return -1; + } + + if(other) + other->related = comp; + + while(1) + { + ILEVENT_T *cur, *prev = NULL; + VCOS_UNSIGNED set; + + ilclient_lock_events(comp->client); + + cur = comp->list; + while(cur && + !(cur->eEvent == OMX_EventCmdComplete && cur->nData1 == command && cur->nData2 == nData2) && + !(cur->eEvent == OMX_EventError && cur->nData2 == 1)) + { + prev = cur; + cur = cur->next; + } + + if(cur) + { + if(prev == NULL) + comp->list = cur->next; + else + prev->next = cur->next; + + // work out whether this was a success or a fail event + ret = cur->eEvent == OMX_EventCmdComplete || cur->nData1 == OMX_ErrorSameState ? 0 : -1; + + if(cur->eEvent == OMX_EventError) + vcos_event_flags_get(&comp->event, ILCLIENT_EVENT_ERROR, VCOS_OR_CONSUME, 0, &set); + + // add back into spare list + cur->next = comp->client->event_list; + comp->client->event_list = cur; + cur->eEvent = -1; // mark as unused + + ilclient_unlock_events(comp->client); + break; + } + else if(other != NULL) + { + // check the other component for an error event that terminates a command + cur = other->list; + while(cur && !(cur->eEvent == OMX_EventError && cur->nData2 == 1)) + cur = cur->next; + + if(cur) + { + // we don't remove the event in this case, since the user + // can confirm that this event errored by calling wait_for_command on the + // other component + + ret = -2; + ilclient_unlock_events(comp->client); + break; + } + } + + ilclient_unlock_events(comp->client); + + vcos_event_flags_get(&comp->event, mask, VCOS_OR_CONSUME, VCOS_SUSPEND, &set); + } + + if(other) + other->related = NULL; + + return ret; +} + + +/*********************************************************** + * Name: ilclient_wait_for_command_complete + * + * Description: Waits for an event signalling command completion. + * + * Returns: 0 on success, -1 on failure. + ***********************************************************/ +int ilclient_wait_for_command_complete(COMPONENT_T *comp, OMX_COMMANDTYPE command, OMX_U32 nData2) +{ + return ilclient_wait_for_command_complete_dual(comp, command, nData2, NULL); +} + +/*********************************************************** + * Name: ilclient_get_output_buffer + * + * Description: Returns an output buffer returned from a component + * using the OMX_FillBufferDone callback from the output list for the + * given component and port index. + * + * Returns: pointer to buffer if available, otherwise NULL + ***********************************************************/ +OMX_BUFFERHEADERTYPE *ilclient_get_output_buffer(COMPONENT_T *comp, int portIndex, int block) +{ + OMX_BUFFERHEADERTYPE *ret = NULL, *prev = NULL; + VCOS_UNSIGNED set; + + do { + vcos_semaphore_wait(&comp->sema); + ret = comp->out_list; + while(ret != NULL && ret->nOutputPortIndex != portIndex) + { + prev = ret; + ret = ret->pAppPrivate; + } + + if(ret) + { + if(prev == NULL) + comp->out_list = ret->pAppPrivate; + else + prev->pAppPrivate = ret->pAppPrivate; + + ret->pAppPrivate = NULL; + } + vcos_semaphore_post(&comp->sema); + + if(block && !ret) + vcos_event_flags_get(&comp->event, ILCLIENT_FILL_BUFFER_DONE, VCOS_OR_CONSUME, -1, &set); + + } while(block && !ret); + + return ret; +} + +/*********************************************************** + * Name: ilclient_get_input_buffer + * + * Description: Returns an input buffer return from a component using + * the OMX_EmptyBufferDone callback from the output list for the given + * component and port index. + * + * Returns: pointer to buffer if available, otherwise NULL + ***********************************************************/ +OMX_BUFFERHEADERTYPE *ilclient_get_input_buffer(COMPONENT_T *comp, int portIndex, int block) +{ + OMX_BUFFERHEADERTYPE *ret = NULL, *prev = NULL; + + do { + VCOS_UNSIGNED set; + + vcos_semaphore_wait(&comp->sema); + ret = comp->in_list; + while(ret != NULL && ret->nInputPortIndex != portIndex) + { + prev = ret; + ret = ret->pAppPrivate; + } + + if(ret) + { + if(prev == NULL) + comp->in_list = ret->pAppPrivate; + else + prev->pAppPrivate = ret->pAppPrivate; + + ret->pAppPrivate = NULL; + } + vcos_semaphore_post(&comp->sema); + + if(block && !ret) + vcos_event_flags_get(&comp->event, ILCLIENT_EMPTY_BUFFER_DONE, VCOS_OR_CONSUME, -1, &set); + + } while(block && !ret); + + return ret; +} + +/*********************************************************** + * Name: ilclient_debug_output + * + * Description: prints a varg message to the log or the debug screen + * under win32 + * + * Returns: void + ***********************************************************/ +void ilclient_debug_output(char *format, ...) +{ + va_list args; + + va_start(args, format); + vcos_vlog_info(format, args); + va_end(args); +} + +/****************************************************************************** +Static functions +******************************************************************************/ + +/*********************************************************** + * Name: ilclient_lock_events + * + * Description: locks the client event structure + * + * Returns: void + ***********************************************************/ +static void ilclient_lock_events(ILCLIENT_T *st) +{ + vcos_semaphore_wait(&st->event_sema); +} + +/*********************************************************** + * Name: ilclient_unlock_events + * + * Description: unlocks the client event structure + * + * Returns: void + ***********************************************************/ +static void ilclient_unlock_events(ILCLIENT_T *st) +{ + vcos_semaphore_post(&st->event_sema); +} + +/*********************************************************** + * Name: ilclient_event_handler + * + * Description: event handler passed to core to use as component + * callback + * + * Returns: success + ***********************************************************/ +static OMX_ERRORTYPE ilclient_event_handler(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_EVENTTYPE eEvent, + OMX_IN OMX_U32 nData1, + OMX_IN OMX_U32 nData2, + OMX_IN OMX_PTR pEventData) +{ + COMPONENT_T *st = (COMPONENT_T *) pAppData; + ILEVENT_T *event; + OMX_ERRORTYPE error = OMX_ErrorNone; + + ilclient_lock_events(st->client); + + // go through the events on this component and remove any duplicates in the + // existing list, since the client probably doesn't need them. it's better + // than asserting when we run out. + event = st->list; + while(event != NULL) + { + ILEVENT_T **list = &(event->next); + while(*list != NULL) + { + if((*list)->eEvent == event->eEvent && + (*list)->nData1 == event->nData1 && + (*list)->nData2 == event->nData2) + { + // remove this duplicate + ILEVENT_T *rem = *list; + ilclient_debug_output("%s: removing %d/%d/%d", st->name, event->eEvent, event->nData1, event->nData2); + *list = rem->next; + rem->eEvent = -1; + rem->next = st->client->event_list; + st->client->event_list = rem; + } + else + list = &((*list)->next); + } + + event = event->next; + } + + vc_assert(st->client->event_list); + event = st->client->event_list; + + switch (eEvent) { + case OMX_EventCmdComplete: + switch (nData1) { + case OMX_CommandStateSet: + ilclient_debug_output("%s: callback state changed (%s)", st->name, states[nData2]); + vcos_event_flags_set(&st->event, ILCLIENT_STATE_CHANGED, VCOS_OR); + break; + case OMX_CommandPortDisable: + ilclient_debug_output("%s: callback port disable %d", st->name, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_PORT_DISABLED, VCOS_OR); + break; + case OMX_CommandPortEnable: + ilclient_debug_output("%s: callback port enable %d", st->name, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_PORT_ENABLED, VCOS_OR); + break; + case OMX_CommandFlush: + ilclient_debug_output("%s: callback port flush %d", st->name, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_PORT_FLUSH, VCOS_OR); + break; + case OMX_CommandMarkBuffer: + ilclient_debug_output("%s: callback mark buffer %d", st->name, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_MARKED_BUFFER, VCOS_OR); + break; + default: + vc_assert(0); + } + break; + case OMX_EventError: + { + // check if this component failed a command, and we have to notify another command + // of this failure + if(nData2 == 1 && st->related != NULL) + vcos_event_flags_set(&st->related->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + + error = nData1; + switch (error) { + case OMX_ErrorPortUnpopulated: + if (st->error_mask & ILCLIENT_ERROR_UNPOPULATED) + { + ilclient_debug_output("%s: ignore error: port unpopulated (%d)", st->name, nData2); + event = NULL; + break; + } + ilclient_debug_output("%s: port unpopulated %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorSameState: + if (st->error_mask & ILCLIENT_ERROR_SAMESTATE) + { + ilclient_debug_output("%s: ignore error: same state (%d)", st->name, nData2); + event = NULL; + break; + } + ilclient_debug_output("%s: same state %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorBadParameter: + if (st->error_mask & ILCLIENT_ERROR_BADPARAMETER) + { + ilclient_debug_output("%s: ignore error: bad parameter (%d)", st->name, nData2); + event = NULL; + break; + } + ilclient_debug_output("%s: bad parameter %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorIncorrectStateTransition: + ilclient_debug_output("%s: incorrect state transition %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorBadPortIndex: + ilclient_debug_output("%s: bad port index %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorStreamCorrupt: + ilclient_debug_output("%s: stream corrupt %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorInsufficientResources: + ilclient_debug_output("%s: insufficient resources %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorUnsupportedSetting: + ilclient_debug_output("%s: unsupported setting %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorOverflow: + ilclient_debug_output("%s: overflow %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorDiskFull: + ilclient_debug_output("%s: disk full %x (%d)", st->name, error, nData2); + //we do not set the error + break; + case OMX_ErrorMaxFileSize: + ilclient_debug_output("%s: max file size %x (%d)", st->name, error, nData2); + //we do not set the error + break; + case OMX_ErrorDrmUnauthorised: + ilclient_debug_output("%s: drm file is unauthorised %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorDrmExpired: + ilclient_debug_output("%s: drm file has expired %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + case OMX_ErrorDrmGeneral: + ilclient_debug_output("%s: drm library error %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + default: + vc_assert(0); + ilclient_debug_output("%s: unexpected error %x (%d)", st->name, error, nData2); + vcos_event_flags_set(&st->event, ILCLIENT_EVENT_ERROR, VCOS_OR); + break; + } + } + break; + case OMX_EventBufferFlag: + ilclient_debug_output("%s: buffer flag %d/%x", st->name, nData1, nData2); + if (nData2 & OMX_BUFFERFLAG_EOS) + { + vcos_event_flags_set(&st->event, ILCLIENT_BUFFER_FLAG_EOS, VCOS_OR); + nData2 = OMX_BUFFERFLAG_EOS; + } + else + vc_assert(0); + break; + case OMX_EventPortSettingsChanged: + ilclient_debug_output("%s: port settings changed %d", st->name, nData1); + vcos_event_flags_set(&st->event, ILCLIENT_PARAMETER_CHANGED, VCOS_OR); + break; + case OMX_EventMark: + ilclient_debug_output("%s: buffer mark %p", st->name, pEventData); + vcos_event_flags_set(&st->event, ILCLIENT_BUFFER_MARK, VCOS_OR); + break; + case OMX_EventParamOrConfigChanged: + ilclient_debug_output("%s: param/config 0x%X on port %d changed", st->name, nData2, nData1); + vcos_event_flags_set(&st->event, ILCLIENT_CONFIG_CHANGED, VCOS_OR); + break; + default: + vc_assert(0); + break; + } + + if (event) + { + // fill in details + event->eEvent = eEvent; + event->nData1 = nData1; + event->nData2 = nData2; + event->pEventData = pEventData; + + // remove from top of spare list + st->client->event_list = st->client->event_list->next; + + // put at head of component event queue + event->next = st->list; + st->list = event; + } + ilclient_unlock_events(st->client); + + // now call any callbacks without the event lock so the client can + // remove the event in context + switch(eEvent) { + case OMX_EventError: + if(event && st->client->error_callback) + st->client->error_callback(st->client->error_callback_data, st, error); + break; + case OMX_EventBufferFlag: + if ((nData2 & OMX_BUFFERFLAG_EOS) && st->client->eos_callback) + st->client->eos_callback(st->client->eos_callback_data, st, nData1); + break; + case OMX_EventPortSettingsChanged: + if (st->client->port_settings_callback) + st->client->port_settings_callback(st->client->port_settings_callback_data, st, nData1); + break; + case OMX_EventParamOrConfigChanged: + if (st->client->configchanged_callback) + st->client->configchanged_callback(st->client->configchanged_callback_data, st, nData2); + break; + default: + // ignore + break; + } + + return OMX_ErrorNone; +} + +/*********************************************************** + * Name: ilclient_empty_buffer_done + * + * Description: passed to core to use as component callback, puts + * buffer on list + * + * Returns: + ***********************************************************/ +static OMX_ERRORTYPE ilclient_empty_buffer_done(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_BUFFERHEADERTYPE* pBuffer) +{ + COMPONENT_T *st = (COMPONENT_T *) pAppData; + OMX_BUFFERHEADERTYPE *list; + + ilclient_debug_output("%s: empty buffer done %p", st->name, pBuffer); + + vcos_semaphore_wait(&st->sema); + // insert at end of the list, so we process buffers in + // the same order + list = st->in_list; + while(list && list->pAppPrivate) + list = list->pAppPrivate; + + if(!list) + st->in_list = pBuffer; + else + list->pAppPrivate = pBuffer; + + pBuffer->pAppPrivate = NULL; + vcos_semaphore_post(&st->sema); + + vcos_event_flags_set(&st->event, ILCLIENT_EMPTY_BUFFER_DONE, VCOS_OR); + + if (st->client->empty_buffer_done_callback) + st->client->empty_buffer_done_callback(st->client->empty_buffer_done_callback_data, st); + + return OMX_ErrorNone; +} + +/*********************************************************** + * Name: ilclient_empty_buffer_done_error + * + * Description: passed to core to use as component callback, asserts + * on use as client not expecting component to use this callback. + * + * Returns: + ***********************************************************/ +static OMX_ERRORTYPE ilclient_empty_buffer_done_error(OMX_IN OMX_HANDLETYPE hComponent, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_BUFFERHEADERTYPE* pBuffer) +{ + vc_assert(0); + return OMX_ErrorNone; +} + +/*********************************************************** + * Name: ilclient_fill_buffer_done + * + * Description: passed to core to use as component callback, puts + * buffer on list + * + * Returns: + ***********************************************************/ +static OMX_ERRORTYPE ilclient_fill_buffer_done(OMX_OUT OMX_HANDLETYPE hComponent, + OMX_OUT OMX_PTR pAppData, + OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer) +{ + COMPONENT_T *st = (COMPONENT_T *) pAppData; + OMX_BUFFERHEADERTYPE *list; + + ilclient_debug_output("%s: fill buffer done %p", st->name, pBuffer); + + vcos_semaphore_wait(&st->sema); + // insert at end of the list, so we process buffers in + // the correct order + list = st->out_list; + while(list && list->pAppPrivate) + list = list->pAppPrivate; + + if(!list) + st->out_list = pBuffer; + else + list->pAppPrivate = pBuffer; + + pBuffer->pAppPrivate = NULL; + vcos_semaphore_post(&st->sema); + + vcos_event_flags_set(&st->event, ILCLIENT_FILL_BUFFER_DONE, VCOS_OR); + + if (st->client->fill_buffer_done_callback) + st->client->fill_buffer_done_callback(st->client->fill_buffer_done_callback_data, st); + + return OMX_ErrorNone; +} + +/*********************************************************** + * Name: ilclient_fill_buffer_done_error + * + * Description: passed to core to use as component callback, asserts + * on use as client not expecting component to use this callback. + * + * Returns: + ***********************************************************/ +static OMX_ERRORTYPE ilclient_fill_buffer_done_error(OMX_OUT OMX_HANDLETYPE hComponent, + OMX_OUT OMX_PTR pAppData, + OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer) +{ + vc_assert(0); + return OMX_ErrorNone; +} + + + +OMX_HANDLETYPE ilclient_get_handle(COMPONENT_T *comp) +{ + vcos_assert(comp); + return comp->comp; +} + + +static struct { + OMX_PORTDOMAINTYPE dom; + int param; +} port_types[] = { + { OMX_PortDomainVideo, OMX_IndexParamVideoInit }, + { OMX_PortDomainAudio, OMX_IndexParamAudioInit }, + { OMX_PortDomainImage, OMX_IndexParamImageInit }, + { OMX_PortDomainOther, OMX_IndexParamOtherInit }, +}; + +int ilclient_get_port_index(COMPONENT_T *comp, OMX_DIRTYPE dir, OMX_PORTDOMAINTYPE type, int index) +{ + uint32_t i; + // for each possible port type... + for (i=0; i<sizeof(port_types)/sizeof(port_types[0]); i++) + { + if ((port_types[i].dom == type) || (type == (OMX_PORTDOMAINTYPE) -1)) + { + OMX_PORT_PARAM_TYPE param; + OMX_ERRORTYPE error; + uint32_t j; + + param.nSize = sizeof(param); + param.nVersion.nVersion = OMX_VERSION; + error = OMX_GetParameter(ILC_GET_HANDLE(comp), port_types[i].param, &param); + assert(error == OMX_ErrorNone); + + // for each port of this type... + for (j=0; j<param.nPorts; j++) + { + int port = param.nStartPortNumber+j; + + OMX_PARAM_PORTDEFINITIONTYPE portdef; + portdef.nSize = sizeof(portdef); + portdef.nVersion.nVersion = OMX_VERSION; + portdef.nPortIndex = port; + + error = OMX_GetParameter(ILC_GET_HANDLE(comp), OMX_IndexParamPortDefinition, &portdef); + assert(error == OMX_ErrorNone); + + if (portdef.eDir == dir) + { + if (index-- == 0) + return port; + } + } + } + } + return -1; +} + +int ilclient_suggest_bufsize(COMPONENT_T *comp, OMX_U32 nBufSizeHint) +{ + OMX_PARAM_BRCMOUTPUTBUFFERSIZETYPE param; + OMX_ERRORTYPE error; + + param.nSize = sizeof(param); + param.nVersion.nVersion = OMX_VERSION; + param.nBufferSize = nBufSizeHint; + error = OMX_SetParameter(ILC_GET_HANDLE(comp), OMX_IndexParamBrcmOutputBufferSize, + &param); + assert(error == OMX_ErrorNone); + + return (error == OMX_ErrorNone) ? 0 : -1; +} + +unsigned int ilclient_stack_size(void) +{ + return ILCLIENT_THREAD_DEFAULT_STACK_SIZE; +} + diff --git a/gui/libs/ilclient/ilclient.h b/gui/libs/ilclient/ilclient.h @@ -0,0 +1,1039 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + * \file + * + * \brief This API defines helper functions for writing IL clients. + * + * This file defines an IL client side library. This is useful when + * writing IL clients, since there tends to be much repeated and + * common code across both single and multiple clients. This library + * seeks to remove that common code and abstract some of the + * interactions with components. There is a wrapper around a + * component and tunnel, and some operations can be done on lists of + * these. The callbacks from components are handled, and specific + * events can be checked or waited for. + */ + +#ifndef _IL_CLIENT_H +#define _IL_CLIENT_H + +#include "IL/OMX_Broadcom.h" +#include "interface/vcos/vcos.h" + +/** + * The <DFN>ILCLIENT_T</DFN> structure encapsulates the state needed for the IL + * Client API. It contains a set of callback functions used to + * communicate with the user. It also includes a linked list of free + * event structures. + ***********************************************************/ +typedef struct _ILCLIENT_T ILCLIENT_T; + + +/** + * Each <DFN>ILEVENT_T</DFN> structure stores the result of an <DFN>EventHandler</DFN> + * callback from a component, storing the event message type and any + * parameters returned. + ***********************************************************/ +typedef struct _ILEVENT_T ILEVENT_T; + + + +struct _COMPONENT_T; + +/** + * The <DFN>COMPONENT_T</DFN> structure represents an IL component, + * together with the necessary extra information required by the IL + * Client API. This structure stores the handle to the OMX component, + * as well as the event list containing all events sent by this + * component. The component state structure also holds a pair of + * buffer queues, for input and output buffers returned to the client + * by the <DFN>FillBufferDone</DFN> and <DFN>EmptyBufferDone</DFN> + * callbacks. As some operations result in error callbacks that can + * be ignored, an error mask is maintained to allow some errors to be + * ignored. A pointer to the client state structure is also added. + ***********************************************************/ +typedef struct _COMPONENT_T COMPONENT_T; + + +/** + * The generic callback function is used for communicating events from + * a particular component to the user. + * + * @param userdata The data returned from when the callback was registered. + * + * @param comp The component structure representing the component that + * originated this event. + * + * @param data The relevant data field from the event. + * + * @return Void. + ***********************************************************/ +typedef void (*ILCLIENT_CALLBACK_T)(void *userdata, COMPONENT_T *comp, OMX_U32 data); + + +/** + * The buffer callback function is used for indicating that a + * component has returned a buffer on a port using client buffer + * communication. + * + * @param data The data returned from when the callback was registered. + * + * @param comp The component from which the buffer originated. + * + * @return Void. + ***********************************************************/ +typedef void (*ILCLIENT_BUFFER_CALLBACK_T)(void *data, COMPONENT_T *comp); + + +/** + * The malloc function is passed into + * <DFN>ilclient_enable_port_buffers()</DFN> and used for allocating the + * buffer payload. + * + * @param userdata Private pointer passed into + * <DFN>ilclient_enable_port_buffers()</DFN> call. + * + * @param size Size in bytes of the requested memory region. + * + * @param align Alignment requirement in bytes for the base memory address. + * + * @param description Text description of the memory being allocated. + * + * @return The memory address on success, <DFN>NULL</DFN> on failure. + ***********************************************************/ +typedef void *(*ILCLIENT_MALLOC_T)(void *userdata, VCOS_UNSIGNED size, VCOS_UNSIGNED align, const char *description); + + +/** + * The free function is passed into + * <DFN>ilclient_enable_port_buffers()</DFN> and + * <DFN>ilclient_disable_port_buffers()</DFN> and used for freeing the + * buffer payload. + * + * @param userdata Private pointer passed into + * <DFN>ilclient_enable_port_buffers()</DFN> and + * <DFN>ilclient_disable_port_buffers()</DFN>. + * + * @param pointer Memory address to free, that was previously returned + * from <DFN>ILCLIENT_MALLOC_T</DFN> function. + * + * @return Void. + ***********************************************************/ +typedef void (*ILCLIENT_FREE_T)(void *userdata, void *pointer); + + +/** + * The event mask enumeration describes the possible events that the + * user can ask to wait for when waiting for a particular event. + ***********************************************************/ +typedef enum { + ILCLIENT_EMPTY_BUFFER_DONE = 0x1, /**< Set when a buffer is + returned from an input + port */ + + ILCLIENT_FILL_BUFFER_DONE = 0x2, /**< Set when a buffer is + returned from an output + port */ + + ILCLIENT_PORT_DISABLED = 0x4, /**< Set when a port indicates + it has completed a disable + command. */ + + ILCLIENT_PORT_ENABLED = 0x8, /**< Set when a port indicates + is has completed an enable + command. */ + + ILCLIENT_STATE_CHANGED = 0x10, /**< Set when a component + indicates it has completed + a state change command. */ + + ILCLIENT_BUFFER_FLAG_EOS = 0x20, /**< Set when a port signals + an EOS event. */ + + ILCLIENT_PARAMETER_CHANGED = 0x40, /**< Set when a port signals a + port settings changed + event. */ + + ILCLIENT_EVENT_ERROR = 0x80, /**< Set when a component + indicates an error. */ + + ILCLIENT_PORT_FLUSH = 0x100, /**< Set when a port indicates + is has completed a flush + command. */ + + ILCLIENT_MARKED_BUFFER = 0x200, /**< Set when a port indicates + it has marked a buffer. */ + + ILCLIENT_BUFFER_MARK = 0x400, /**< Set when a port indicates + it has received a buffer + mark. */ + + ILCLIENT_CONFIG_CHANGED = 0x800 /**< Set when a config parameter + changed. */ +} ILEVENT_MASK_T; + + +/** + * On component creation the user can set flags to control the + * creation of that component. + ***********************************************************/ +typedef enum { + ILCLIENT_FLAGS_NONE = 0x0, /**< Used if no flags are + set. */ + + ILCLIENT_ENABLE_INPUT_BUFFERS = 0x1, /**< If set we allow the + client to communicate with + input ports via buffer + communication, rather than + tunneling with another + component. */ + + ILCLIENT_ENABLE_OUTPUT_BUFFERS = 0x2, /**< If set we allow the + client to communicate with + output ports via buffer + communication, rather than + tunneling with another + component. */ + + ILCLIENT_DISABLE_ALL_PORTS = 0x4, /**< If set we disable all + ports on creation. */ + + ILCLIENT_HOST_COMPONENT = 0x8, /**< Create a host component. + The default host ilcore + only can create host components + by being locally hosted + so should only be used for testing + purposes. */ + + ILCLIENT_OUTPUT_ZERO_BUFFERS = 0x10 /**< All output ports will have + nBufferCountActual set to zero, + if supported by the component. */ +} ILCLIENT_CREATE_FLAGS_T; + + +/** + * \brief This structure represents a tunnel in the OpenMAX IL API. + * + * Some operations in this API act on a tunnel, so the tunnel state + * structure (<DFN>TUNNEL_T</DFN>) is a convenient store of the source and sink + * of the tunnel. For each, a pointer to the relevant component state + * structure and the port index is stored. + ***********************************************************/ +typedef struct { + COMPONENT_T *source; /**< The source component */ + int source_port; /**< The output port index on the source component */ + COMPONENT_T *sink; /**< The sink component */ + int sink_port; /**< The input port index on the sink component */ +} TUNNEL_T; + + +/** + * The <DFN>set_tunnel</DFN> macro is a useful function that initialises a + * <DFN>TUNNEL_T</DFN> structure. + ***********************************************************/ +#define set_tunnel(t,a,b,c,d) do {TUNNEL_T *_ilct = (t); \ + _ilct->source = (a); _ilct->source_port = (b); \ + _ilct->sink = (c); _ilct->sink_port = (d);} while(0) + +/** + * For calling OpenMAX IL methods directory, we need to access the + * <DFN>OMX_HANDLETYPE</DFN> corresponding to the <DFN>COMPONENT_T</DFN> structure. This + * macro enables this while keeping the <DFN>COMPONENT_T</DFN> structure opaque. + * The parameter <DFN>x</DFN> should be of the type <DFN>*COMPONENT_T</DFN>. + ***********************************************************/ +#define ILC_GET_HANDLE(x) ilclient_get_handle(x) + +/** + * An IL Client structure is created by the <DFN>ilclient_init()</DFN> + * method. This structure is used when creating components, but + * otherwise is not needed in other API functions as a pointer to this + * structure is maintained in the <DFN>COMPONENT_T</DFN> structure. + * + * @return pointer to client structure + ***********************************************************/ +VCHPRE_ ILCLIENT_T VCHPOST_ *ilclient_init(void); + +/** + * When all components have been deleted, the IL Client structure can + * be destroyed by calling the <DFN>ilclient_destroy()</DFN> function. + * + * @param handle The client handle. After calling this function, this + * handle should not be used. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_destroy(ILCLIENT_T *handle); + +/** + * The <DFN>ilclient_set_port_settings_callback()</DFN> function registers a + * callback to be used when the <DFN>OMX_EventPortSettingsChanged</DFN> event is + * received. When the event is received, a pointer to the component + * structure and port index is returned by the callback. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_port_settings_callback(ILCLIENT_T *handle, + ILCLIENT_CALLBACK_T func, + void *userdata); + +/** + * The <DFN>ilclient_set_eos_callback()</DFN> function registers a callback to be + * used when the <DFN>OMX_EventBufferFlag</DFN> is received with the + * <DFN>OMX_BUFFERFLAG_EOS</DFN> flag set. When the event is received, a pointer + * to the component structure and port index is returned by the + * callback. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_eos_callback(ILCLIENT_T *handle, + ILCLIENT_CALLBACK_T func, + void *userdata); + +/** + * The <DFN>ilclient_set_error_callback()</DFN> function registers a callback to be + * used when the <DFN>OMX_EventError</DFN> is received from a component. When + * the event is received, a pointer to the component structure and the + * error code are reported by the callback. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_error_callback(ILCLIENT_T *handle, + ILCLIENT_CALLBACK_T func, + void *userdata); + +/** + * The <DFN>ilclient_set_configchanged_callback()</DFN> function + * registers a callback to be used when an + * <DFN>OMX_EventParamOrConfigChanged</DFN> event occurs. When the + * event is received, a pointer to the component structure and the + * index value that has changed are reported by the callback. The + * user may then use an <DFN>OMX_GetConfig</DFN> call with the index + * as specified to retrieve the updated information. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_configchanged_callback(ILCLIENT_T *handle, + ILCLIENT_CALLBACK_T func, + void *userdata); + + +/** + * The <DFN>ilclient_set_fill_buffer_done_callback()</DFN> function registers a + * callback to be used when a buffer passed to an output port using the + * <DFN>OMX_FillBuffer</DFN> call is returned with the <DFN>OMX_FillBufferDone</DFN> + * callback. When the event is received, a pointer to the component + * structure is returned by the callback. The user may then use the + * <DFN>ilclient_get_output_buffer()</DFN> function to retrieve the buffer. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_fill_buffer_done_callback(ILCLIENT_T *handle, + ILCLIENT_BUFFER_CALLBACK_T func, + void *userdata); + +/** + * The <DFN>ilclient_set_empty_buffer_done_callback()</DFN> function registers a + * callback to be used when a buffer passed to an input port using the + * <DFN>OMX_EmptyBuffer</DFN> call is returned with the <DFN>OMX_EmptyBufferDone</DFN> + * callback. When the event is received, a pointer to the component + * structure is returned by the callback. The user may then use the + * <DFN>ilclient_get_input_buffer()</DFN> function to retrieve the buffer. + * + * @param handle The client handle + * + * @param func The callback function to use. Calling this function + * with a <DFN>NULL</DFN> function pointer will deregister any existing + * registered callback. + * + * @param userdata Data to be passed back when calling the callback + * function. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_set_empty_buffer_done_callback(ILCLIENT_T *handle, + ILCLIENT_BUFFER_CALLBACK_T func, + void *userdata); + + +/** + * Components are created using the <DFN>ilclient_create_component()</DFN> + * function. + * + * @param handle The client handle + * + * @param comp On successful creation, the component structure pointer + * will be written back into <DFN>comp</DFN>. + * + * @param name The name of the component to be created. Component + * names as provided are automatically prefixed with + * <DFN>"OMX.broadcom."</DFN> before passing to the IL core. The name + * provided will also be used in debugging messages added about this + * component. + * + * @param flags The client can specify some creation behaviour by using + * the <DFN>flags</DFN> field. The meaning of each flag is defined + * by the <DFN>ILCLIENT_CREATE_FLAGS_T</DFN> type. + * + * @return 0 on success, -1 on failure + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_create_component(ILCLIENT_T *handle, + COMPONENT_T **comp, + char *name, + ILCLIENT_CREATE_FLAGS_T flags); + +/** + * The <DFN>ilclient_cleanup_components()</DFN> function deallocates all + * state associated with components and frees the OpenMAX component + * handles. All tunnels connecting components should have been torn + * down explicitly, and all components must be in loaded state. + * + * @param list A null-terminated list of component pointers to be + * deallocated. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_cleanup_components(COMPONENT_T *list[]); + + +/** + * The <DFN>ilclient_change_component_state()</DFN> function changes the + * state of an individual component. This will trigger the state + * change, and also wait for that state change to be completed. It + * should not be called if this state change has dependencies on other + * components also changing states. Trying to change a component to + * its current state is treated as success. + * + * @param comp The component to change. + * + * @param state The new state to transition to. + * + * @return 0 on success, -1 on failure. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_change_component_state(COMPONENT_T *comp, + OMX_STATETYPE state); + + +/** + * The <DFN>ilclient_state_transition()</DFN> function transitions a set of + * components that need to perform a simultaneous state transition; + * for example, when two components are tunnelled and the buffer + * supplier port needs to allocate and pass buffers to a non-supplier + * port. All components are sent a command to change state, then the + * function will wait for all components to signal that they have + * changed state. + * + * @param list A null-terminated list of component pointers. + * + * @param state The new state to which to transition all components. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_state_transition(COMPONENT_T *list[], + OMX_STATETYPE state); + + +/** + * The <DFN>ilclient_disable_port()</DFN> function disables a port on a + * given component. This function sends the disable port message to + * the component and waits for the component to signal that this has + * taken place. If the port is already disabled, this is treated as a + * success. + * + * @param comp The component containing the port to disable. + * + * @param portIndex The port index of the port to disable. This must + * be a named port index, rather than a <DFN>OMX_ALL</DFN> value. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_disable_port(COMPONENT_T *comp, + int portIndex); + + +/** + * The <DFN>ilclient_enable_port()</DFN> function enables a port on a + * given component. This function sends the enable port message to + * the component and waits for the component to signal that this has + * taken place. If the port is already disabled, this is treated as a + * success. + * + * @param comp The component containing the port to enable. + * + * @param portIndex The port index of the port to enable. This must + * be a named port index, rather than a <DFN>OMX_ALL</DFN> value. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_enable_port(COMPONENT_T *comp, + int portIndex); + + + +/** + * The <DFN>ilclient_enable_port_buffers()</DFN> function enables a port + * in base profile mode on a given component. The port is not + * tunneled, so requires buffers to be allocated. + * + * @param comp The component containing the port to enable. + * + * @param portIndex The port index of the port to enable. This must + * be a named port index, rather than a <DFN>OMX_ALL</DFN> value. + * + * @param ilclient_malloc This function will be used to allocate + * buffer payloads. If <DFN>NULL</DFN> then + * <DFN>vcos_malloc_aligned</DFN> will be used. + * + * @param ilclient_free If an error occurs, this function is used to + * free previously allocated payloads. If <DFN>NULL</DFN> then + * <DFN>vcos_free</DFN> will be used. + * + * @param userdata The first argument to calls to + * <DFN>ilclient_malloc</DFN> and <DFN>ilclient_free</DFN>, if these + * arguments are not <DFN>NULL</DFN>. + * + * @return 0 indicates success. -1 indicates failure. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_enable_port_buffers(COMPONENT_T *comp, + int portIndex, + ILCLIENT_MALLOC_T ilclient_malloc, + ILCLIENT_FREE_T ilclient_free, + void *userdata); + + +/** + * The <DFN>ilclient_disable_port_buffers()</DFN> function disables a + * port in base profile mode on a given component. The port is not + * tunneled, and has been supplied with buffers by the client. + * + * @param comp The component containing the port to disable. + * + * @param portIndex The port index of the port to disable. This must + * be a named port index, rather than a <DFN>OMX_ALL</DFN> value. + * + * @param bufferList A list of buffers, using <DFN>pAppPrivate</DFN> + * as the next pointer that were allocated on this port, and currently + * held by the application. If buffers on this port are held by IL + * client or the component then these are automatically freed. + * + * @param ilclient_free This function is used to free the buffer payloads. + * If <DFN>NULL</DFN> then <DFN>vcos_free</DFN> will be used. + * + * @param userdata The first argument to calls to + * <DFN>ilclient_free</DFN>. + * + * @return void + */ +VCHPRE_ void VCHPOST_ ilclient_disable_port_buffers(COMPONENT_T *comp, + int portIndex, + OMX_BUFFERHEADERTYPE *bufferList, + ILCLIENT_FREE_T ilclient_free, + void *userdata); + + +/** + * With a populated tunnel structure, the + * <DFN>ilclient_setup_tunnel()</DFN> function connects the tunnel. It + * first transitions the source component to idle if currently in + * loaded state, and then optionally checks the source event list for + * a port settings changed event from the source port. If this event + * is not in the event queue then this function optionally waits for + * it to arrive. To disable this check for the port settings changed + * event, set <DFN>timeout</DFN> to zero. + * + * Both ports are then disabled, and the source port is inspected for + * a port streams parameter. If this is supported, then the + * <DFN>portStream</DFN> argument is used to select which port stream + * to use. The two ports are then tunnelled using the + * <DFN>OMX_SetupTunnel</DFN> function. If this is successful, then + * both ports are enabled. Note that for disabling and enabling the + * tunnelled ports, the functions <DFN>ilclient_disable_tunnel()</DFN> + * and <DFN>ilclient_enable_tunnel()</DFN> are used, so the relevant + * documentation for those functions applies here. + * + * @param tunnel The tunnel structure representing the tunnel to + * set up. + * + * @param portStream If port streams are supported on the output port + * of the tunnel, then this parameter indicates the port stream to + * select on this port. + * + * @param timeout The time duration in milliseconds to wait for the + * output port to signal a port settings changed event before + * returning a timeout failure. If this is 0, then we do not check + * for a port settings changed before setting up the tunnel. + * + * @return 0 indicates success, negative indicates failure: + * - -1: a timeout waiting for the parameter changed + * - -2: an error was returned instead of parameter changed + * - -3: no streams are available from this port + * - -4: requested stream is not available from this port + * - -5: the data format was not acceptable to the sink + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_setup_tunnel(TUNNEL_T *tunnel, + unsigned int portStream, + int timeout); + + +/** + * The <DFN>ilclient_disable_tunnel()</DFN> function disables both ports listed in + * the tunnel structure. It will send a port disable command to each + * port, then waits for both to indicate they have completed the + * transition. The errors <DFN>OMX_ErrorPortUnpopulated</DFN> and + * <DFN>OMX_ErrorSameState</DFN> are both ignored by this function; the former + * since the first port to disable may deallocate buffers before the + * second port has been disabled, leading to the second port reporting + * the unpopulated error. + * + * @param tunnel The tunnel to disable. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_disable_tunnel(TUNNEL_T *tunnel); + + +/** + * The <DFN>ilclient_enable_tunnel()</DFN> function enables both ports listed in + * the tunnel structure. It will first send a port enable command to + * each port. It then checks whether the sink component is not in + * loaded state - if so, the function waits for both ports to complete + * the requested port enable. If the sink component was in loaded + * state, then this component is transitioned to idle to allow the + * ports to exchange buffers and enable the ports. This is since + * typically this function is used when creating a tunnel between two + * components, where the source component is processing data to enable + * it to report the port settings changed event, and the sink port has + * yet to be used. Before transitioning the sink component to idle, + * this function waits for the sink port to be enabled - since the + * component is in loaded state, this will happen quickly. If the + * transition to idle fails, the sink component is transitioned back + * to loaded and the source port disabled. If the transition + * succeeds, the function then waits for the source port to complete + * the requested port enable. + * + * @param tunnel The tunnel to enable. + * + * @return 0 on success, -1 on failure. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_enable_tunnel(TUNNEL_T *tunnel); + + +/** + * The <DFN>ilclient_flush_tunnels()</DFN> function will flush a number of tunnels + * from the list of tunnels presented. For each tunnel that is to be + * flushed, both source and sink ports are sent a flush command. The + * function then waits for both ports to report they have completed + * the flush operation. + * + * @param tunnel List of tunnels. The list must be terminated with a + * tunnel structure with <DFN>NULL</DFN> component entries. + * + * @param max The maximum number of tunnels to flush from the list. + * A value of 0 indicates that all tunnels in the list are flushed. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_flush_tunnels(TUNNEL_T *tunnel, + int max); + + +/** + * The <DFN>ilclient_teardown_tunnels()</DFN> function tears down all tunnels in + * the list of tunnels presented. For each tunnel in the list, the + * <DFN>OMX_SetupTunnel</DFN> is called on the source port and on the sink port, + * where for both calls the destination component is <DFN>NULL</DFN> and the + * destination port is zero. The VMCSX IL implementation requires + * that all tunnels are torn down in this manner before components are + * freed. + * + * @param tunnels List of tunnels to teardown. The list must be + * terminated with a tunnel structure with <DFN>NULL</DFN> component entries. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_teardown_tunnels(TUNNEL_T *tunnels); + + +/** + * The <DFN>ilclient_get_output_buffer()</DFN> function returns a buffer + * that was sent to an output port and that has been returned from a + * component using the <DFN>OMX_FillBufferDone</DFN> callback. + * + * @param comp The component that returned the buffer. + * + * @param portIndex The port index on the component that the buffer + * was returned from. + * + * @param block If non-zero, the function will block until a buffer is available. + * + * @return Pointer to buffer if available, otherwise <DFN>NULL</DFN>. + ***********************************************************/ +VCHPRE_ OMX_BUFFERHEADERTYPE* VCHPOST_ ilclient_get_output_buffer(COMPONENT_T *comp, + int portIndex, + int block); + + +/** + * The <DFN>ilclient_get_input_buffer()</DFN> function returns a buffer + * that was sent to an input port and that has been returned from a + * component using the <DFN>OMX_EmptyBufferDone</DFN> callback. + * + * @param comp The component that returned the buffer. + * + * @param portIndex The port index on the component from which the buffer + * was returned. + * + * @param block If non-zero, the function will block until a buffer is available. + * + * @return pointer to buffer if available, otherwise <DFN>NULL</DFN> + ***********************************************************/ +VCHPRE_ OMX_BUFFERHEADERTYPE* VCHPOST_ ilclient_get_input_buffer(COMPONENT_T *comp, + int portIndex, + int block); + + +/** + * The <DFN>ilclient_remove_event()</DFN> function queries the event list for the + * given component, matching against the given criteria. If a matching + * event is found, it is removed and added to the free event list. + * + * @param comp The component that returned the matching event. + * + * @param event The event type of the matching event. + * + * @param nData1 The <DFN>nData1</DFN> field of the matching event. + * + * @param ignore1 Whether to ignore the <DFN>nData1</DFN> field when finding a + * matching event. A value of 0 indicates that <DFN>nData1</DFN> must match, a + * value of 1 indicates that <DFN>nData1</DFN> does not have to match. + * + * @param nData2 The <DFN>nData2</DFN> field of the matching event. + * + * @param ignore2 Whether to ignore the <DFN>nData2</DFN> field when finding a + * matching event. A value of 0 indicates that <DFN>nData2</DFN> must match, a + * value of 1 indicates that <DFN>nData2</DFN> does not have to match. + * + * @return 0 if the event was removed. -1 if no matching event was + * found. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_remove_event(COMPONENT_T *comp, + OMX_EVENTTYPE event, + OMX_U32 nData1, + int ignore1, + OMX_U32 nData2, + int ignore2); + + +/** + * The <DFN>ilclient_return_events()</DFN> function removes all events + * from a component event list and adds them to the IL client free + * event list. This function is automatically called when components + * are freed. + * + * @param comp The component from which all events should be moved to + * the free list. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_return_events(COMPONENT_T *comp); + + +/** + * The <DFN>ilclient_wait_for_event()</DFN> function is similar to + * <DFN>ilclient_remove_event()</DFN>, but allows the caller to block until that + * event arrives. + * + * @param comp The component that returned the matching event. + * + * @param event The event type of the matching event. + * + * @param nData1 The <DFN>nData1</DFN> field of the matching event. + * + * @param ignore1 Whether to ignore the <DFN>nData1</DFN> field when finding a + * matching event. A value of 0 indicates that <DFN>nData1</DFN> must match, a + * value of 1 indicates that <DFN>nData1</DFN> does not have to match. + * + * @param nData2 The <DFN>nData2</DFN> field of the matching event. + * + * @param ignore2 Whether to ignore the <DFN>nData2</DFN> field when finding a + * matching event. A value of 0 indicates that <DFN>nData2</DFN> must match, a + * value of 1 indicates that <DFN>nData2</DFN> does not have to match. + * + * @param event_flag Specifies a bitfield of IL client events to wait + * for, given in <DFN>ILEVENT_MASK_T</DFN>. If any of these events + * are signalled by the component, the event list is then re-checked + * for a matching event. If the <DFN>ILCLIENT_EVENT_ERROR</DFN> bit + * is included, and an error is signalled by the component, then the + * function will return an error code. If the + * <DFN>ILCLIENT_CONFIG_CHANGED</DFN> bit is included, and this bit is + * signalled by the component, then the function will return an error + * code. + * + * @param timeout Specifies how long to block for in milliseconds + * before returning a failure. + * + * @return 0 indicates success, a matching event has been removed from + * the component's event queue. A negative return indicates failure, + * in this case no events have been removed from the component's event + * queue. + * - -1: a timeout was received. + * - -2: an error event was received. + * - -3: a config changed event was received. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_wait_for_event(COMPONENT_T *comp, + OMX_EVENTTYPE event, + OMX_U32 nData1, + int ignore1, + OMX_U32 nData2, + int ignore2, + int event_flag, + int timeout); + + +/** + * The <DFN>ilclient_wait_for_command_complete()</DFN> function waits + * for a message from a component that indicates that the command + * has completed. This is either a command success message, or an + * error message that signals the completion of an event. + * + * @param comp The component currently processing a command. + * + * @param command The command being processed. This must be either + * <DFN>OMX_CommandStateSet</DFN>, <DFN>OMX_CommandPortDisable</DFN> + * or <DFN>OMX_CommandPortEnable</DFN>. + * + * @param nData2 The expected value of <DFN>nData2</DFN> in the + * command complete message. + * + * @return 0 indicates success, either the command successfully completed + * or the <DFN>OMX_ErrorSameState</DFN> was returned. -1 indicates + * that the command terminated with a different error message. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_wait_for_command_complete(COMPONENT_T *comp, + OMX_COMMANDTYPE command, + OMX_U32 nData2); + + +/** + * The <DFN>ilclient_wait_for_command_complete_dual()</DFN> function + * is similar to <DFN>ilclient_wait_for_command_complete()</DFN>. The + * difference is that while waiting for the component to complete the + * event or raise an error, we can also report if another reports an + * error that terminates a command. This is useful if the two + * components are tunneled, and we need to wait for one component to + * enable a port, or change state to <DFN>OMX_StateIdle</DFN>. If the + * other component is the buffer supplier and reports an error, then + * it will not allocate buffers, so our first component may stall. + * + * @param comp The component currently processing a command. + * + * @param command The command being processed. This must be either + * <DFN>OMX_CommandStateSet</DFN>, <DFN>OMX_CommandPortDisable</DFN> + * or <DFN>OMX_CommandPortEnable</DFN>. + * + * @param nData2 The expected value of <DFN>nData2</DFN> in the + * command complete message. + * + * @param related Another component, where we will return if this + * component raises an error that terminates a command. + * + * @return 0 indicates success, either the command successfully + * completed or the <DFN>OMX_ErrorSameState</DFN> was returned. -1 + * indicates that the command terminated with a different error + * message. -2 indicates that the related component raised an error. + * In this case, the error is not cleared from the related + * component's event list. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_wait_for_command_complete_dual(COMPONENT_T *comp, + OMX_COMMANDTYPE command, + OMX_U32 nData2, + COMPONENT_T *related); + + +/** + * The <DFN>ilclient_debug_output()</DFN> function adds a message to a + * host-specific debug display. For a local VideoCore host the message is + * added to the internal message log. For a Win32 host the message is + * printed to the debug display. This function should be customised + * when IL client is ported to another platform. + * + * @param format A message to add, together with the variable + * argument list similar to <DFN>printf</DFN> and other standard C functions. + * + * @return void + ***********************************************************/ +VCHPRE_ void VCHPOST_ ilclient_debug_output(char *format, ...); + +/** + * The <DFN>ilclient_get_handle()</DFN> function returns the + * underlying OMX component held by an IL component handle. This is + * needed when calling methods such as <DFN>OMX_SetParameter</DFN> on + * a component created using the IL client library. + * + * @param comp IL component handle + * + * @return The <DFN>OMX_HANDLETYPE</DFN> value for the component. + ***********************************************************/ +VCHPRE_ OMX_HANDLETYPE VCHPOST_ ilclient_get_handle(COMPONENT_T *comp); + + +#ifndef OMX_SKIP64BIT + +/** + * Macro to return <DFN>OMX_TICKS</DFN> from a signed 64 bit value. + * This is the version where <DFN>OMX_TICKS</DFN> is a signed 64 bit + * value, an alternative definition is used when <DFN>OMX_TICKS</DFN> + * is a structure. + */ +#define ilclient_ticks_from_s64(s) (s) + +/** + * Macro to return signed 64 bit value from <DFN>OMX_TICKS</DFN>. + * This is the version where <DFN>OMX_TICKS</DFN> is a signed 64 bit + * value, an alternative definition is used when <DFN>OMX_TICKS</DFN> + * is a structure. + */ +#define ilclient_ticks_to_s64(t) (t) + +#else + +/** + * Inline function to return <DFN>OMX_TICKS</DFN> from a signed 64 bit + * value. This is the version where <DFN>OMX_TICKS</DFN> is a + * structure, an alternative definition is used when + * <DFN>OMX_TICKS</DFN> is a signed 64 bit value. + */ +static inline OMX_TICKS ilclient_ticks_from_s64(int64_t s) { + OMX_TICKS ret; + ret.nLowPart = s; + ret.nHighPart = s>>32; + return ret; +} + +/** + * Inline function to return signed 64 bit value from + * <DFN>OMX_TICKS</DFN>. This is the version where + * <DFN>OMX_TICKS</DFN> is a structure, an alternative definition is + * used when <DFN>OMX_TICKS</DFN> is a signed 64 bit value. + */ +static inline int64_t ilclient_ticks_to_s64(OMX_TICKS t) { + uint64_t u = t.nLowPart | ((uint64_t)t.nHighPart << 32); + return u; +} + + +#endif /* OMX_SKIP64BIT */ + +/** + * The <DFN>ilclient_get_port_index()</DFN> function returns the n'th + * port index of the specified type and direction for the given + * component. + * + * @param comp The component of interest + * @param dir The direction + * @param type The type, or -1 for any type. + * @param index Which port (counting from 0). + * + * @return The port index, or -1 if not found. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_get_port_index(COMPONENT_T *comp, + OMX_DIRTYPE dir, + OMX_PORTDOMAINTYPE type, + int index); + + +/** + * The <DFN>ilclient_suggest_bufsize()</DFN> function gives a + * component a hint about the size of buffer it should use. This size + * is set on the component by setting the + * <DFN>OMX_IndexParamBrcmOutputBufferSize</DFN> index on the given + * component. + * + * @param comp IL component handle + * @param nBufSizeHint Size of buffer in bytes + * + * @return 0 indicates success, -1 indicates failure. + ***********************************************************/ +VCHPRE_ int VCHPOST_ ilclient_suggest_bufsize(COMPONENT_T *comp, + OMX_U32 nBufSizeHint); + + +/** + * The <DFN>ilclient_stack_size()</DFN> function suggests a minimum + * stack size that tasks calling into with API will require. + * + * @return Suggested stack size in bytes. + ***********************************************************/ +VCHPRE_ unsigned int VCHPOST_ ilclient_stack_size(void); + +#endif /* ILCLIENT_H */ diff --git a/gui/libs/ilclient/ilcore.c b/gui/libs/ilclient/ilcore.c @@ -0,0 +1,308 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + * \file + * + * \brief Host core implementation. + */ + +#include <stdio.h> +#include <stdarg.h> + +//includes +#include <memory.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "IL/OMX_Component.h" +#include "interface/vcos/vcos.h" + +#include "interface/vmcs_host/vcilcs.h" +#include "interface/vmcs_host/vchost.h" +#include "interface/vmcs_host/vcilcs_common.h" + +static int coreInit = 0; +static int nActiveHandles = 0; +static ILCS_SERVICE_T *ilcs_service = NULL; +static VCOS_MUTEX_T lock; +static VCOS_ONCE_T once = VCOS_ONCE_INIT; + +/* Atomic creation of lock protecting shared state */ +static void initOnce(void) +{ + VCOS_STATUS_T status; + status = vcos_mutex_create(&lock, VCOS_FUNCTION); + vcos_demand(status == VCOS_SUCCESS); +} + +/* OMX_Init */ +OMX_ERRORTYPE OMX_APIENTRY OMX_Init(void) +{ + VCOS_STATUS_T status; + OMX_ERRORTYPE err = OMX_ErrorNone; + + status = vcos_once(&once, initOnce); + vcos_demand(status == VCOS_SUCCESS); + + vcos_mutex_lock(&lock); + + if(coreInit == 0) + { + // we need to connect via an ILCS connection to VideoCore + VCHI_INSTANCE_T initialise_instance; + VCHI_CONNECTION_T *connection; + ILCS_CONFIG_T config; + + vc_host_get_vchi_state(&initialise_instance, &connection); + + vcilcs_config(&config); + + ilcs_service = ilcs_init((VCHIQ_INSTANCE_T) initialise_instance, (void **) &connection, &config, 0); + + if(ilcs_service == NULL) + { + err = OMX_ErrorHardware; + goto end; + } + + coreInit = 1; + } + else + coreInit++; + +end: + vcos_mutex_unlock(&lock); + return err; +} + +/* OMX_Deinit */ +OMX_ERRORTYPE OMX_APIENTRY OMX_Deinit(void) +{ + if(coreInit == 0) // || (coreInit == 1 && nActiveHandles > 0)) + return OMX_ErrorNotReady; + + vcos_mutex_lock(&lock); + + coreInit--; + + if(coreInit == 0) + { + // we need to teardown the ILCS connection to VideoCore + ilcs_deinit(ilcs_service); + ilcs_service = NULL; + } + + vcos_mutex_unlock(&lock); + + return OMX_ErrorNone; +} + + +/* OMX_ComponentNameEnum */ +OMX_ERRORTYPE OMX_APIENTRY OMX_ComponentNameEnum( + OMX_OUT OMX_STRING cComponentName, + OMX_IN OMX_U32 nNameLength, + OMX_IN OMX_U32 nIndex) +{ + if(ilcs_service == NULL) + return OMX_ErrorBadParameter; + + return vcil_out_component_name_enum(ilcs_get_common(ilcs_service), cComponentName, nNameLength, nIndex); +} + + +/* OMX_GetHandle */ +OMX_ERRORTYPE OMX_APIENTRY OMX_GetHandle( + OMX_OUT OMX_HANDLETYPE* pHandle, + OMX_IN OMX_STRING cComponentName, + OMX_IN OMX_PTR pAppData, + OMX_IN OMX_CALLBACKTYPE* pCallBacks) +{ + OMX_ERRORTYPE eError; + OMX_COMPONENTTYPE *pComp; + OMX_HANDLETYPE hHandle = 0; + + if (pHandle == NULL || cComponentName == NULL || pCallBacks == NULL || ilcs_service == NULL) + { + if(pHandle) + *pHandle = NULL; + return OMX_ErrorBadParameter; + } + + { + pComp = (OMX_COMPONENTTYPE *)malloc(sizeof(OMX_COMPONENTTYPE)); + if (!pComp) + { + vcos_assert(0); + return OMX_ErrorInsufficientResources; + } + memset(pComp, 0, sizeof(OMX_COMPONENTTYPE)); + hHandle = (OMX_HANDLETYPE)pComp; + pComp->nSize = sizeof(OMX_COMPONENTTYPE); + pComp->nVersion.nVersion = OMX_VERSION; + eError = vcil_out_create_component(ilcs_get_common(ilcs_service), hHandle, cComponentName); + + if (eError == OMX_ErrorNone) { + // Check that all function pointers have been filled in. + // All fields should be non-zero. + int i; + uint32_t *p = (uint32_t *) pComp; + for(i=0; i<sizeof(OMX_COMPONENTTYPE)>>2; i++) + if(*p++ == 0) + eError = OMX_ErrorInvalidComponent; + + if(eError != OMX_ErrorNone && pComp->ComponentDeInit) + pComp->ComponentDeInit(hHandle); + } + + if (eError == OMX_ErrorNone) { + eError = pComp->SetCallbacks(hHandle,pCallBacks,pAppData); + if (eError != OMX_ErrorNone) + pComp->ComponentDeInit(hHandle); + } + if (eError == OMX_ErrorNone) { + *pHandle = hHandle; + } + else { + *pHandle = NULL; + free(pComp); + } + } + + if (eError == OMX_ErrorNone) { + vcos_mutex_lock(&lock); + nActiveHandles++; + vcos_mutex_unlock(&lock); + } + + return eError; +} + +/* OMX_FreeHandle */ +OMX_ERRORTYPE OMX_APIENTRY OMX_FreeHandle( + OMX_IN OMX_HANDLETYPE hComponent) +{ + OMX_ERRORTYPE eError = OMX_ErrorNone; + OMX_COMPONENTTYPE *pComp; + + if (hComponent == NULL || ilcs_service == NULL) + return OMX_ErrorBadParameter; + + pComp = (OMX_COMPONENTTYPE*)hComponent; + + if (ilcs_service == NULL) + return OMX_ErrorBadParameter; + + eError = (pComp->ComponentDeInit)(hComponent); + if (eError == OMX_ErrorNone) { + vcos_mutex_lock(&lock); + --nActiveHandles; + vcos_mutex_unlock(&lock); + free(pComp); + } + + vcos_assert(nActiveHandles >= 0); + + return eError; +} + +/* OMX_SetupTunnel */ +OMX_ERRORTYPE OMX_APIENTRY OMX_SetupTunnel( + OMX_IN OMX_HANDLETYPE hOutput, + OMX_IN OMX_U32 nPortOutput, + OMX_IN OMX_HANDLETYPE hInput, + OMX_IN OMX_U32 nPortInput) +{ + OMX_ERRORTYPE eError = OMX_ErrorNone; + OMX_COMPONENTTYPE *pCompIn, *pCompOut; + OMX_TUNNELSETUPTYPE oTunnelSetup; + + if ((hOutput == NULL && hInput == NULL) || ilcs_service == NULL) + return OMX_ErrorBadParameter; + + oTunnelSetup.nTunnelFlags = 0; + oTunnelSetup.eSupplier = OMX_BufferSupplyUnspecified; + + pCompOut = (OMX_COMPONENTTYPE*)hOutput; + + if (hOutput){ + eError = pCompOut->ComponentTunnelRequest(hOutput, nPortOutput, hInput, nPortInput, &oTunnelSetup); + } + + if (eError == OMX_ErrorNone && hInput) { + pCompIn = (OMX_COMPONENTTYPE*)hInput; + eError = pCompIn->ComponentTunnelRequest(hInput, nPortInput, hOutput, nPortOutput, &oTunnelSetup); + + if (eError != OMX_ErrorNone && hOutput) { + /* cancel tunnel request on output port since input port failed */ + pCompOut->ComponentTunnelRequest(hOutput, nPortOutput, NULL, 0, NULL); + } + } + return eError; +} + +/* OMX_GetComponentsOfRole */ +OMX_ERRORTYPE OMX_GetComponentsOfRole ( + OMX_IN OMX_STRING role, + OMX_INOUT OMX_U32 *pNumComps, + OMX_INOUT OMX_U8 **compNames) +{ + OMX_ERRORTYPE eError = OMX_ErrorNone; + + *pNumComps = 0; + return eError; +} + +/* OMX_GetRolesOfComponent */ +OMX_ERRORTYPE OMX_GetRolesOfComponent ( + OMX_IN OMX_STRING compName, + OMX_INOUT OMX_U32 *pNumRoles, + OMX_OUT OMX_U8 **roles) +{ + OMX_ERRORTYPE eError = OMX_ErrorNone; + + *pNumRoles = 0; + return eError; +} + +/* OMX_GetDebugInformation */ +OMX_ERRORTYPE OMX_GetDebugInformation ( + OMX_OUT OMX_STRING debugInfo, + OMX_INOUT OMX_S32 *pLen) +{ + if(ilcs_service == NULL) + return OMX_ErrorBadParameter; + + return vcil_out_get_debug_information(ilcs_get_common(ilcs_service), debugInfo, pLen); +} + + + +/* File EOF */ + diff --git a/gui/libs/vgfont/Makefile b/gui/libs/vgfont/Makefile @@ -0,0 +1,7 @@ +OBJS=font.o vgft.o graphics.o +LIB=libvgfont.a + +INCLUDES+=-I$(SDKSTAGE)/usr/include/freetype2 -I$(SDKSTAGE)/usr/include -I$(SDKSTAGE)/usr/include/arm-linux-gnueabi + +include ../../Makefile.include + diff --git a/gui/libs/vgfont/font.c b/gui/libs/vgfont/font.c @@ -0,0 +1,355 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Font handling for graphicsx + +/** @file font.c + * + * Fairly primitive font handling, just enough to emulate the old API. + * + * Hinting and Font Size + * + * The old API does not create fonts explicitly, it just renders them + * as needed. That works fine for unhinted fonts, but for hinted fonts we + * care about font size. + * + * Since we now *can* do hinted fonts, we should do. Regenerating the + * fonts each time becomes quite slow, so we maintain a cache of fonts. + * + * For the typical applications which use graphics_x this is fine, but + * won't work well if lots of fonts sizes are used. + * + * Unicode + * + * This API doesn't support unicode at all at present, nor UTF-8. + */ + +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/types.h> + +#include "graphics_x_private.h" +#include "vgft.h" + +#define VMCS_INSTALL_PREFIX "" + +/** The one and only (default) font we support for now. + */ +static struct +{ + const char *file; + void *mem; + size_t len; +} default_font = { "VeraMono.ttf" }; + +/** An entry in our list of fonts + */ +typedef struct gx_font_cache_entry_t +{ + struct gx_font_cache_entry_t *next; + VGFT_FONT_T font; + uint32_t ptsize; /** size in points, 26.6 */ +} gx_font_cache_entry_t; + +static char fname[128]; +static int inited; +static gx_font_cache_entry_t *fonts; + +static VGFT_FONT_T *find_font(const char *text, uint32_t text_size); + +VCOS_STATUS_T gx_priv_font_init(const char *font_dir) +{ + VCOS_STATUS_T ret; + size_t len; + int rc; + if (vgft_init()) + { + ret = VCOS_ENOMEM; + goto fail_init; + } + + int fd = -1; + // search for the font + sprintf(fname, "%s/%s", font_dir, default_font.file); + fd = open(fname, O_RDONLY); + + if (fd < 0) + { + GX_ERROR("Could not open font file '%s'", default_font.file); + ret = VCOS_ENOENT; + goto fail_open; + } + + len = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + + default_font.mem = vcos_malloc(len, default_font.file); + if (!default_font.mem) + { + GX_ERROR("No memory for font %s", fname); + ret = VCOS_ENOMEM; + goto fail_mem; + } + + rc = read(fd, default_font.mem, len); + if (rc != len) + { + GX_ERROR("Could not read font %s", fname); + ret = VCOS_EINVAL; + goto fail_rd; + } + default_font.len = len; + close(fd); + + GX_TRACE("Opened font file '%s'", fname); + + inited = 1; + return VCOS_SUCCESS; + +fail_rd: + vcos_free(default_font.mem); +fail_mem: + if (fd >= 0) close(fd); +fail_open: + vgft_term(); +fail_init: + return ret; +} + +void gx_priv_font_term(void) +{ + gx_font_cache_flush(); + vgft_term(); + vcos_free(default_font.mem); +} + +/** Render text. + * + * FIXME: Not at all optimal - re-renders each time. + * FIXME: Not UTF-8 aware + * FIXME: better caching + */ +VCOS_STATUS_T gx_priv_render_text( GX_DISPLAY_T *disp, + GRAPHICS_RESOURCE_HANDLE res, + int32_t x, + int32_t y, + uint32_t width, + uint32_t height, + uint32_t fg_colour, + uint32_t bg_colour, + const char *text, + uint32_t text_length, + uint32_t text_size ) +{ + VGfloat vg_colour[4]; + VGFT_FONT_T *font; + VGPaint fg; + GX_CLIENT_STATE_T save; + VCOS_STATUS_T status = VCOS_SUCCESS; + int clip = 1; + + vcos_demand(inited); // has gx_font_init() been called? + + gx_priv_save(&save, res); + + if (width == GRAPHICS_RESOURCE_WIDTH && + height == GRAPHICS_RESOURCE_HEIGHT) + { + clip = 0; + } + + width = (width == GRAPHICS_RESOURCE_WIDTH) ? res->width : width; + height = (height == GRAPHICS_RESOURCE_HEIGHT) ? res->height : height; + font = find_font(text, text_size); + if (!font) + { + status = VCOS_ENOMEM; + goto finish; + } + + // setup the clipping rectangle + if (clip) + { + VGint coords[] = {x,y,width,height}; + vgSeti(VG_SCISSORING, VG_TRUE); + vgSetiv(VG_SCISSOR_RECTS, 4, coords); + } + + // setup the background colour if needed + if (bg_colour != GRAPHICS_TRANSPARENT_COLOUR) + { + int err; + VGfloat rendered_w, rendered_h; + VGfloat vg_bg_colour[4]; + + // setup the background colour... + gx_priv_colour_to_paint(bg_colour, vg_bg_colour); + vgSetfv(VG_CLEAR_COLOR, 4, vg_bg_colour); + + // fill in a rectangle... + vgft_get_text_extents(font, text, text_length, (VGfloat)x, (VGfloat)y, &rendered_w, &rendered_h); + + if ( ( 0 < (VGint)rendered_w ) && ( 0 < (VGint)rendered_h ) ) + { + // Have to compensate for the messed up y position of multiline text. + VGfloat offset = vgft_first_line_y_offset(font); + int32_t bottom = y + offset - rendered_h; + + vgClear(x, bottom, (VGint)rendered_w, (VGint)rendered_h); + err = vgGetError(); + if (err) + { + GX_LOG("Error %d clearing bg text %d %d %g %g", + err, x, y, rendered_w, rendered_h); + vcos_assert(0); + } // if + } // if + } // if + // setup the foreground colour + fg = vgCreatePaint(); + if (!fg) + { + status = VCOS_ENOMEM; + goto finish; + } + + // draw the foreground text + vgSetParameteri(fg, VG_PAINT_TYPE, VG_PAINT_TYPE_COLOR); + gx_priv_colour_to_paint(fg_colour, vg_colour); + vgSetParameterfv(fg, VG_PAINT_COLOR, 4, vg_colour); + vgSetPaint(fg, VG_FILL_PATH); + + vgft_font_draw(font, (VGfloat)x, (VGfloat)y, text, text_length, VG_FILL_PATH); + + vgDestroyPaint(fg); + + vcos_assert(vgGetError() == 0); + vgSeti(VG_SCISSORING, VG_FALSE); + +finish: + gx_priv_restore(&save); + + return status; +} + + +/** Find a font in our cache, or create a new entry in the cache. + * + * Very primitive at present. + */ +static VGFT_FONT_T *find_font(const char *text, uint32_t text_size) +{ + int ptsize, dpi_x = 0, dpi_y = 0; + VCOS_STATUS_T status; + gx_font_cache_entry_t *font; + + ptsize = text_size << 6; // freetype takes size in points, in 26.6 format. + + for (font = fonts; font; font = font->next) + { + if (font->ptsize == ptsize) + return &font->font; + } + + font = vcos_malloc(sizeof(*font), "font"); + if (!font) + return NULL; + + font->ptsize = ptsize; + + status = vgft_font_init(&font->font); + if (status != VCOS_SUCCESS) + { + vcos_free(font); + return NULL; + } + + // load the font + status = vgft_font_load_mem(&font->font, default_font.mem, default_font.len); + if (status != VCOS_SUCCESS) + { + GX_LOG("Could not load font from memory: %d", status); + vgft_font_term(&font->font); + vcos_free(font); + return NULL; + } + + status = vgft_font_convert_glyphs(&font->font, ptsize, dpi_x, dpi_y); + if (status != VCOS_SUCCESS) + { + GX_LOG("Could not convert font '%s' at size %d", fname, ptsize); + vgft_font_term(&font->font); + vcos_free(font); + return NULL; + } + + font->next = fonts; + fonts = font; + + return &font->font; +} + +void gx_font_cache_flush(void) +{ + while (fonts != NULL) + { + struct gx_font_cache_entry_t *next = fonts->next; + vgft_font_term(&fonts->font); + vcos_free(fonts); + fonts = next; + } +} + +int32_t graphics_resource_text_dimensions_ext(GRAPHICS_RESOURCE_HANDLE res, + const char *text, + const uint32_t text_length, + uint32_t *width, + uint32_t *height, + const uint32_t text_size ) +{ + GX_CLIENT_STATE_T save; + VGfloat w, h; + int ret = -1; + + gx_priv_save(&save, res); + + VGFT_FONT_T *font = find_font(text, text_size); + if (!font) + goto finish; + + + vgft_get_text_extents(font, text, text_length, 0.0, 0.0, &w, &h); + *width = w; + *height = h; + ret = 0; + +finish: + gx_priv_restore(&save); + return ret; +} + diff --git a/gui/libs/vgfont/graphics.c b/gui/libs/vgfont/graphics.c @@ -0,0 +1,1608 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Graphics library for VG + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include "vgfont.h" +#include "graphics_x_private.h" + +/****************************************************************************** +Defines. +******************************************************************************/ +#define ATEXT_FONT_SIZE 12 /*< Default font size (font size can be set with *_ext functions). */ + +/****************************************************************************** +Local data +******************************************************************************/ +static GX_DISPLAY_T display; /*< Our one and only EGL display. */ + +/** + * We create one eglContext for each of the possible graphics_x resource types + * that are supported. + ***********************************************************/ +static EGLContext gx_contexts[GRAPHICS_RESOURCE_HANDLE_TYPE_MAX]; + +/** Note: we have to share all our contexts, because otherwise it seems + * to be not valid to blit from one image to another if the images + * have different contexts. + * + * That means we have to use a single global lock to serialise all accesses + * to any contexts. + ***********************************************************/ +static VCOS_MUTEX_T lock; + +static EGLConfig gx_configs[GRAPHICS_RESOURCE_HANDLE_TYPE_MAX]; + +static int inited; + +/****************************************************************************** +Local Functions +******************************************************************************/ + +/** Convert graphics_x colour formats into EGL format. */ +static int gx_egl_attrib_colours(EGLint *attribs, GRAPHICS_RESOURCE_TYPE_T res_type) +{ + int i, n; + static EGLint rgba[] = {EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE}; + static uint8_t rgb565[] = {5,6,5,0}; + static uint8_t rgb888[] = {8,8,8,0}; + static uint8_t rgb32a[] = {8,8,8,8}; + + uint8_t *sizes = NULL; + + switch (res_type) + { + case GRAPHICS_RESOURCE_RGB565: + sizes = rgb565; + break; + case GRAPHICS_RESOURCE_RGB888: + sizes = rgb888; + break; + case GRAPHICS_RESOURCE_RGBA32: + sizes = rgb32a; + break; + default: + vcos_assert(0); + return -1; + } + for (n=0, i=0; i<countof(rgba); i++) + { + attribs[n++] = rgba[i]; + attribs[n++] = sizes[i]; + } + return n; +} + +/* Create an EGLContext for a given GRAPHICS_RESOURCE_TYPE */ +static VCOS_STATUS_T create_context(EGLDisplay disp, + GRAPHICS_RESOURCE_TYPE_T image_type, + EGLContext *shared_with) +{ + int n; + EGLConfig configs[1]; + EGLint nconfigs, attribs[32]; + n = gx_egl_attrib_colours(attribs, image_type); + + // we want to be able to do OpenVG on this surface... + attribs[n++] = EGL_RENDERABLE_TYPE; attribs[n++] = EGL_OPENVG_BIT; + attribs[n++] = EGL_SURFACE_TYPE; attribs[n++] = EGL_WINDOW_BIT; + + attribs[n] = EGL_NONE; + + EGLBoolean egl_ret = eglChooseConfig(disp, + attribs, configs, + countof(configs), &nconfigs); + + if (!egl_ret || !nconfigs) + { + GX_LOG("%s: no suitable configurations for res type %d", + __FUNCTION__, image_type); + return VCOS_EINVAL; + } + + EGLContext cxt = eglCreateContext(disp, configs[0], *shared_with, 0); + if (!cxt) + { + GX_LOG("Could not create context for image type %d: 0x%x", + image_type, eglGetError()); + return VCOS_ENOSPC; + } + + gx_contexts[image_type] = cxt; + gx_configs[image_type] = configs[0]; + *shared_with = cxt; + + return VCOS_SUCCESS; +} + +/****************************************************************************** +Functions private to code inside GraphicsX +******************************************************************************/ + +static VCOS_STATUS_T gx_priv_initialise( void ) +{ + int i; + EGLDisplay disp; + EGLint egl_maj, egl_min; + int32_t ret = VCOS_EINVAL; + EGLBoolean result; + + vcos_demand(inited == 0); + + vcos_log_set_level(&gx_log_cat, VCOS_LOG_WARN); + vcos_log_register("graphics", &gx_log_cat); + + memset(&display,0,sizeof(display)); + + gx_priv_init(); + + disp = eglGetDisplay(EGL_DEFAULT_DISPLAY); + + if (disp == EGL_NO_DISPLAY) + { + GX_LOG("Could not open display: 0x%x", eglGetError()); + vcos_assert(0); + goto fail_disp; + } + + result = eglInitialize(disp, &egl_maj, &egl_min); + if (!result) + { + GX_LOG("Could not init display :0x%x", eglGetError()); + vcos_assert(0); // really can't continue + goto fail_egl_init; + } + + result = eglBindAPI(EGL_OPENVG_API); + vcos_assert(result); // really should succeed + + display.disp = disp; + + GX_TRACE("Supported client APIS: %s", eglQueryString(disp, EGL_CLIENT_APIS)); + + // create the available contexts + EGLContext shared_context = EGL_NO_CONTEXT; + ret = create_context(disp,GRAPHICS_RESOURCE_RGB565, &shared_context); + ret |= create_context(disp,GRAPHICS_RESOURCE_RGB888, &shared_context); + ret |= create_context(disp,GRAPHICS_RESOURCE_RGBA32, &shared_context); + + if (ret != VCOS_SUCCESS) + goto fail_cxt; + + eglSwapInterval(disp, 1); + + inited = 1; + + return ret; + +fail_cxt: + for (i=0; i<GRAPHICS_RESOURCE_HANDLE_TYPE_MAX; i++) + { + if (gx_contexts[i]) + { + eglDestroyContext(display.disp,gx_contexts[i]); + vcos_mutex_delete(&lock); + } + } + eglTerminate(display.disp); +fail_egl_init: +fail_disp: + return ret; +} + +/*****************************************************************************/ +void gx_priv_save(GX_CLIENT_STATE_T *state, GRAPHICS_RESOURCE_HANDLE res) +{ + EGLBoolean egl_result; + vcos_assert(res == NULL || (res->magic == RES_MAGIC)); + vcos_assert(res == NULL || !res->context_bound); + + state->context = eglGetCurrentContext(); + state->api = eglQueryAPI(); + state->read_surface = eglGetCurrentSurface(EGL_READ); + state->draw_surface = eglGetCurrentSurface(EGL_DRAW); + state->res = res; + + vcos_assert(state->api); // should never be anything other than VG or GL + + vcos_mutex_lock(&lock); + + egl_result = eglBindAPI(EGL_OPENVG_API); + vcos_assert(egl_result); + + if (res) + { + GX_TRACE("gx_priv_save: eglMakeCurrent: %s, res %x surface %x, cxt %x", vcos_thread_get_name(vcos_thread_current()), + (uint32_t)res, (uint32_t)res->surface, (uint32_t)res->context); + + egl_result = eglMakeCurrent(display.disp, res->surface, + res->surface, res->context); + vcos_assert(egl_result); + + res->context_bound = 1; + } +} + +/*****************************************************************************/ +void gx_priv_restore(GX_CLIENT_STATE_T *state) +{ + EGLBoolean egl_result; + + GX_TRACE("gx_priv_restore: eglMakeCurrent: %s, res %x draw_surface %x, surface %x, cxt %x", vcos_thread_get_name(vcos_thread_current()), + (uint32_t)state->res, (uint32_t)state->draw_surface, (uint32_t)state->read_surface, (uint32_t)state->context); + + // disconnect our thread from this context, so we other threads can use it via + // this API + egl_result = eglMakeCurrent(display.disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + vcos_assert(egl_result); + + // now return to the client's API binding + egl_result = eglBindAPI(state->api); + vcos_assert(egl_result); + + egl_result = eglMakeCurrent(display.disp, state->draw_surface, state->read_surface, state->context); + vcos_assert(egl_result); + + if (state->res) state->res->context_bound = 0; + + vcos_mutex_unlock(&lock); +} + +/****************************************************************************** +Functions and data exported as part of the public GraphicsX API +******************************************************************************/ + +VCOS_LOG_CAT_T gx_log_cat; /*< Logging category for GraphicsX. */ + +int32_t graphics_initialise( void ) +{ + // dummy initialisation function. This is typically called + // early in the day before VLLs are available, and so cannot + // do anything useful. + return 0; +} + +/*****************************************************************************/ +int32_t graphics_uninitialise( void ) +{ + int i; + vcos_assert(inited); + + gx_priv_font_term(); + + for (i=0; i<GRAPHICS_RESOURCE_HANDLE_TYPE_MAX; i++) + if (gx_contexts[i]) + eglDestroyContext(display.disp,gx_contexts[i]); + + eglTerminate(display.disp); + gx_priv_destroy(); + vcos_log_unregister(&gx_log_cat); + inited = 0; + return 0; +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_create_window( uint32_t screen_id, + uint32_t width, + uint32_t height, + GRAPHICS_RESOURCE_TYPE_T image_type, + GRAPHICS_RESOURCE_HANDLE *resource_handle ) +{ + int rc; + VCOS_STATUS_T status = VCOS_SUCCESS; + GRAPHICS_RESOURCE_HANDLE h; + EGLBoolean egl_result; + void *cookie; + GX_CLIENT_STATE_T save; + + if (!gx_contexts[image_type]) + { + GX_LOG("Invalid image type %d", image_type); + return VCOS_EINVAL; + } + + h = vcos_calloc(1,sizeof(*h), "graphics_x_resource"); + if (!h) + { + GX_LOG("%s: no memory for resource", __FUNCTION__); + return VCOS_ENOMEM; + } + + // now need to get the native window + rc = gx_priv_create_native_window(screen_id, + width, height, image_type, + &h->u.native_window, + &cookie); + if (rc < 0) + { + GX_LOG("%s: could not create native window", __FUNCTION__); + status = VCOS_ENOMEM; + goto fail_create_native_win; + } + + h->magic = RES_MAGIC; + h->type = GX_WINDOW; + h->alpha = 1.0; + + h->surface = eglCreateWindowSurface(display.disp, gx_configs[image_type], &h->u.native_window.egl_win, NULL); + if (!h->surface) + { + GX_LOG("Could not create window surface: 0x%x", eglGetError()); + status = VCOS_ENOMEM; + goto fail_win; + } + + egl_result = eglSurfaceAttrib(display.disp, h->surface, + EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED); + vcos_assert(egl_result); + + h->context = gx_contexts[image_type]; + h->screen_id = screen_id; + h->width = width; + h->height = height; + h->restype = image_type; + + gx_priv_save(&save, h); + + // fill it with black + status = gx_priv_resource_fill(h, 0, 0, width, height, GRAPHICS_RGBA32(0,0,0,0xff)); + vcos_assert(status == VCOS_SUCCESS); + + gx_priv_finish_native_window(h, cookie); + gx_priv_flush(h); + + *resource_handle = h; + gx_priv_restore(&save); + return status; + +fail_win: + gx_priv_destroy_native_window(h); +fail_create_native_win: + vcos_free(h); + return status; +} + +/*****************************************************************************/ +int32_t graphics_delete_resource( GRAPHICS_RESOURCE_HANDLE res ) +{ + EGLBoolean result; + + if (!res) + { + // let it slide - mimics old behaviour + return 0; + } + GX_TRACE("delete resource @%p", res); + + vcos_assert(res->magic == RES_MAGIC); + + if (res->type == GX_PBUFFER) + { + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + vgDestroyImage(res->u.pixmap); + vcos_assert(vgGetError() == 0); + gx_priv_restore(&save); + } + + GX_TRACE("graphics_delete_resource: calling eglDestroySurface..."); + result = eglDestroySurface(display.disp, res->surface); + vcos_assert(result); + + GX_TRACE("graphics_delete_resource: calling eglWaitClient..."); + eglWaitClient(); // wait for EGL to finish sorting out its surfaces + + if (res->type == GX_WINDOW) + { + GX_TRACE("graphics_delete_resource: calling gx_priv_destroy_native_window..."); + gx_priv_destroy_native_window(res); + } + + res->magic = ~RES_MAGIC; + vcos_free(res); + GX_TRACE("graphics_delete_resource: done"); + + return 0; +} + +/*****************************************************************************/ +int32_t graphics_update_displayed_resource(GRAPHICS_RESOURCE_HANDLE res, + const uint32_t x_offset, + const uint32_t y_offset, + const uint32_t width, + const uint32_t height ) +{ + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + + gx_priv_flush(res); + + gx_priv_restore(&save); + + return 0; +} + +/*****************************************************************************/ +int32_t graphics_resource_fill(GRAPHICS_RESOURCE_HANDLE res, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height, + uint32_t fill_colour ) +{ + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + + VCOS_STATUS_T st = gx_priv_resource_fill( + res, + x, res->height-y-height, + width, height, + fill_colour); + + gx_priv_restore(&save); + + return st == VCOS_SUCCESS ? 0 : -1; +} + +/*****************************************************************************/ +int32_t graphics_resource_render_text_ext( GRAPHICS_RESOURCE_HANDLE res, + const int32_t x, + const int32_t y, + const uint32_t width, + const uint32_t height, + const uint32_t fg_colour, + const uint32_t bg_colour, + const char *text, + const uint32_t text_length, + const uint32_t text_size ) +{ + + /* + * FIXME: Not at all optimal - re-renders each time. + * FIXME: Not UTF-8 safe + * FIXME: much better caching (or any caching) + */ + VCOS_STATUS_T rc = gx_priv_render_text( + &display, res, + x, res->height-y-text_size, width, height, fg_colour, bg_colour, + text, text_length, text_size); + + return (rc == VCOS_SUCCESS) ? 0 : -1; +} + +/*****************************************************************************/ +int32_t graphics_resource_render_text( GRAPHICS_RESOURCE_HANDLE res, + const int32_t x, + const int32_t y, + const uint32_t width, /* this can be GRAPHICS_RESOURCE_WIDTH for no clipping */ + const uint32_t height, /* this can be GRAPHICS_RESOURCE_HEIGHT for no clipping */ + const uint32_t fg_colour, + const uint32_t bg_colour, + const char *text, + const uint32_t text_length) +{ + return graphics_resource_render_text_ext(res, x, y, width, height, + fg_colour, bg_colour, + text, text_length, + ATEXT_FONT_SIZE); +} + +/*****************************************************************************/ +int32_t graphics_get_resource_size( + const GRAPHICS_RESOURCE_HANDLE res, + uint32_t *w, + uint32_t *h) +{ + if (w) *w = res->width; + if (h) *h = res->height; + return 0; +} + +/*****************************************************************************/ +int32_t graphics_get_resource_type(const GRAPHICS_RESOURCE_HANDLE res, GRAPHICS_RESOURCE_TYPE_T *type) +{ + if (type) *type = res->restype; + return 0; +} + +/*****************************************************************************/ +int32_t graphics_bitblt( const GRAPHICS_RESOURCE_HANDLE src, + const uint32_t x, // offset within source + const uint32_t y, // offset within source + const uint32_t width, + const uint32_t height, + GRAPHICS_RESOURCE_HANDLE dest, + const uint32_t x_pos, + const uint32_t y_pos ) +{ + int rc = -1; + VGfloat old[9]; + uint32_t w, h; + VGPaint paint = VG_INVALID_HANDLE; + GX_CLIENT_STATE_T save; + int is_child = 0; + VGImage img = VG_INVALID_HANDLE; + + gx_priv_save(&save, dest); + + if (src->type != GX_PBUFFER) + { + vcos_assert(0); + goto finish; + } + + // create a child image that contains just the part wanted + w = width == GRAPHICS_RESOURCE_WIDTH ? src->width : width; + h = height == GRAPHICS_RESOURCE_HEIGHT ? src->height : height; + + if (x==0 && y==0 && + w == src->width && + h == src->height) + { + img = src->u.pixmap; + } + else + { + is_child = 1; + img = vgChildImage(src->u.pixmap, x, y, w, h); + if (img == VG_INVALID_HANDLE) + { + vcos_assert(0); + goto finish; + } + } + + vcos_assert(vgGetError()==0); + + vgSeti(VG_MATRIX_MODE, VG_MATRIX_IMAGE_USER_TO_SURFACE); + vgGetMatrix(old); + vgLoadIdentity(); + vgTranslate((VGfloat)x_pos, (VGfloat)(dest->height-y_pos)); + vgScale(1.0, -1.0); + + // Do we have a translucency going on? + if (src->alpha != 1.0) + { + VGfloat colour[4] = {1.0,1.0,1.0,src->alpha}; + paint = vgCreatePaint(); + + vgSetParameterfv(paint, VG_PAINT_COLOR, 4, colour); + vgSeti(VG_IMAGE_MODE, VG_DRAW_IMAGE_MULTIPLY); + vgSetPaint(paint, VG_STROKE_PATH | VG_FILL_PATH); + } + vcos_assert(vgGetError()==0); + + vgDrawImage(img); + vcos_assert(vgGetError()==0); + vgLoadMatrix(old); + + int err = vgGetError(); + + if (err) + { + GX_LOG("vg error %x blitting area", err); + vcos_assert(0); + rc = -1; + } + else + { + rc = 0; + } +finish: + if (paint != VG_INVALID_HANDLE) + vgDestroyPaint(paint); + + if (is_child) + vgDestroyImage(img); + + gx_priv_restore(&save); + return rc; +} + +void gx_priv_flush(GRAPHICS_RESOURCE_HANDLE res) +{ + EGLBoolean result; + result = eglSwapBuffers(display.disp, res->surface); + vcos_assert(result); +} + + +/** Map a colour, which the client will have supplied in RGB888. + */ + +void gx_priv_colour_to_paint(uint32_t col, VGfloat *rgba) +{ + // with OpenVG we use RGB order. + rgba[0] = ((VGfloat)((col & R_888_MASK) >> 16 )) / 0xff; + rgba[1] = ((VGfloat)((col & G_888_MASK) >> 8 )) / 0xff; + rgba[2] = ((VGfloat)((col & B_888_MASK) >> 0 )) / 0xff; + rgba[3] = ((VGfloat)((col & ALPHA_888_MASK) >> 24)) / 0xff; +} + +/** Fill an area of a surface with a fixed colour. + */ +VCOS_STATUS_T gx_priv_resource_fill(GRAPHICS_RESOURCE_HANDLE res, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height, + uint32_t fill_colour ) +{ + VGfloat vg_clear_colour[4]; + + gx_priv_colour_to_paint(fill_colour, vg_clear_colour); + vgSeti(VG_SCISSORING, VG_FALSE); + + vgSetfv(VG_CLEAR_COLOR, 4, vg_clear_colour); + vgClear(x, y, width, height); + + int err = vgGetError(); + if (err) + { + GX_LOG("vg error %x filling area", err); + vcos_assert(0); + } + + return VCOS_SUCCESS; +} + +VCOS_STATUS_T gx_priv_get_pixels(const GRAPHICS_RESOURCE_HANDLE res, void **p_pixels, GX_RASTER_ORDER_T raster_order) +{ + VCOS_STATUS_T status = VCOS_SUCCESS; + void *pixels, *dest; + uint32_t width, height; + int data_size, pitch; + VGImageFormat image_format; + + if (!p_pixels) + { + status = VCOS_EINVAL; + goto finish; + } + + GX_TRACE("%s: res %p", __FUNCTION__, res); + + graphics_get_resource_size(res, &width, &height); + + /* FIXME: implement e.g. gx_get_pitch */ + switch (res->restype) + { + case GRAPHICS_RESOURCE_RGB565: + pitch = ((width + 31)&(~31)) << 1; + break; + case GRAPHICS_RESOURCE_RGB888: + case GRAPHICS_RESOURCE_RGBA32: + pitch = ((width + 31)&(~31)) << 2; + break; + default: + { + GX_LOG("Unsupported pixel format"); + status = VCOS_EINVAL; + goto finish; + } + } + + data_size = pitch * height; + + /* NB: vgReadPixels requires that the data pointer is aligned, but does not + require the stride to be aligned. Most implementations probably will + require that as well though... */ + pixels = vcos_malloc(data_size, "gx_get_pixels data"); + if (!pixels) + { + GX_LOG("Could not allocate %d bytes for vgReadPixels", data_size); + status = VCOS_ENOMEM; + goto finish; + } + /* FIXME: introduce e.g. GX_COLOR_FORMAT and mapping to VGImageFormat... */ + + /* Hand out image data formatted to match OpenGL RGBA format. + */ + switch (res->restype) + { + case GRAPHICS_RESOURCE_RGB565: + image_format = VG_sBGR_565; + break; + case GRAPHICS_RESOURCE_RGB888: + image_format = VG_sXBGR_8888; + break; + case GRAPHICS_RESOURCE_RGBA32: + image_format = VG_sABGR_8888; + break; + default: + { + GX_LOG("Unsupported pixel format"); + status = VCOS_EINVAL; + goto finish; + } + } + + /* VG raster order is bottom-to-top */ + if (raster_order == GX_TOP_BOTTOM) + { + dest = ((uint8_t*)pixels)+(pitch*(height-1)); + pitch = -pitch; + } + else + { + dest = pixels; + } + + vgReadPixels(dest, pitch, image_format, 0, 0, width, height); + + vcos_assert(vgGetError() == 0); + + *p_pixels = pixels; + +finish: + return status; +} + +static VCOS_STATUS_T convert_image_type(GRAPHICS_RESOURCE_TYPE_T image_type, + VGImageFormat *vg_image_type, + int *pbytes_per_pixel) +{ + int bytes_per_pixel; + + switch (image_type) + { + case GRAPHICS_RESOURCE_RGB565: + *vg_image_type = VG_sRGB_565; + bytes_per_pixel = 2; + break; + case GRAPHICS_RESOURCE_RGB888: + *vg_image_type = VG_sRGBX_8888; + bytes_per_pixel = 3; // 24 bpp + break; + case GRAPHICS_RESOURCE_RGBA32: + *vg_image_type = VG_sARGB_8888; + bytes_per_pixel = 4; + break; + default: + vcos_assert(0); + *vg_image_type = 0; + return VCOS_EINVAL; + } + if (pbytes_per_pixel) + *pbytes_per_pixel = bytes_per_pixel; + + return VCOS_SUCCESS; +} + + +/*****************************************************************************/ +VCOS_STATUS_T gx_create_pbuffer( uint32_t width, + uint32_t height, + GRAPHICS_RESOURCE_TYPE_T image_type, + GRAPHICS_RESOURCE_HANDLE *resource_handle ) +{ + VCOS_STATUS_T status = VCOS_SUCCESS; + GRAPHICS_RESOURCE_HANDLE h; + VGImage image; + VGImageFormat vg_image_type; + GX_CLIENT_STATE_T save; + + h = vcos_calloc(1,sizeof(*h), "graphics_x_resource"); + if (!h) + { + GX_LOG("%s: no memory for resource", __FUNCTION__); + return VCOS_ENOMEM; + } + + status = convert_image_type(image_type, &vg_image_type, NULL); + if (status != VCOS_SUCCESS) + { + vcos_free(h); + return status; + } + + h->magic = RES_MAGIC; + h->context = gx_contexts[image_type]; + h->config = gx_configs[image_type]; + h->alpha = 1.0; + h->type = GX_PBUFFER; + h->width = width; + h->height = height; + h->restype = image_type; + + GX_TRACE("Creating pbuffer surface"); + + EGLint attribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; + h->surface = eglCreatePbufferSurface(display.disp, h->config, + attribs); + if (!h->surface) + { + GX_LOG("Could not create EGL pbuffer surface: 0x%x", eglGetError()); + vcos_free(h); + return VCOS_EINVAL; + } + + gx_priv_save(&save, h); + + image = vgCreateImage(vg_image_type, width, height, VG_IMAGE_QUALITY_BETTER); + if (image == VG_INVALID_HANDLE) + { + GX_LOG("Could not create vg image type %d: vg error 0x%x", + vg_image_type, vgGetError()); + eglDestroySurface(display.disp, h->surface); + vcos_free(h); + status = VCOS_ENOMEM; + goto finish; + } + + h->u.pixmap = image; + + // fill it with black + status = gx_priv_resource_fill(h, 0, 0, width, height, GRAPHICS_RGBA32(0,0,0,0xff)); + vcos_assert(status == VCOS_SUCCESS); + + *resource_handle = h; +finish: + gx_priv_restore(&save); + return status; +} + +/*****************************************************************************/ +GX_PAINT_T *gx_create_gradient(GRAPHICS_RESOURCE_HANDLE res, + uint32_t start_colour, + uint32_t end_colour) +{ + // holds the two colour stops (offset,r,g,b,a). + VGfloat fill_stops[10]; + GX_CLIENT_STATE_T save; + VGPaint paint = VG_INVALID_HANDLE; + + gx_priv_save(&save, res); + + paint = vgCreatePaint(); + if (!paint) + { + gx_priv_restore(&save); + vcos_log("Could not create paint: vg %d\n", vgGetError()); + vcos_assert(0); + goto finish; + } + + fill_stops[0] = 0.0; + gx_priv_colour_to_paint(start_colour, fill_stops+1); + + fill_stops[5] = 1.0; + gx_priv_colour_to_paint(end_colour, fill_stops+6); + + vgSetParameteri(paint, VG_PAINT_TYPE, VG_PAINT_TYPE_LINEAR_GRADIENT); + vgSetParameterfv(paint, VG_PAINT_COLOR_RAMP_STOPS, 5*2, fill_stops); + +finish: + gx_priv_restore(&save); + return (GX_PAINT_T*)paint; +} + +/*****************************************************************************/ +void gx_destroy_paint(GRAPHICS_RESOURCE_HANDLE res, GX_PAINT_T *p) +{ + GX_CLIENT_STATE_T save; + VGPaint paint = (VGPaint)p; + gx_priv_save(&save, res); + vgDestroyPaint(paint); + gx_priv_restore(&save); +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_fill_gradient(GRAPHICS_RESOURCE_HANDLE dest, + uint32_t x, uint32_t y, + uint32_t width, uint32_t height, + uint32_t radius, + GX_PAINT_T *p) +{ + /* Define start and end points of gradient, see OpenVG specification, + section 9.3.3. */ + VGfloat gradient[4] = {0.0, 0.0, 0.0, 0.0}; + VGPaint paint = (VGPaint)p; + VGPath path; + GX_CLIENT_STATE_T save; + VCOS_STATUS_T status = VCOS_SUCCESS; + + if (!paint) + return VCOS_EINVAL; + + gx_priv_save(&save, dest); + + if (width == GRAPHICS_RESOURCE_WIDTH) + width = dest->width; + + if (height == GRAPHICS_RESOURCE_HEIGHT) + height = dest->height; + + gradient[2] = width; + + vgSetParameterfv(paint, VG_PAINT_LINEAR_GRADIENT, 4, gradient); + vgSetPaint(paint, VG_FILL_PATH); + + path = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_S_32, + 1.0, 0.0, 8, 8, VG_PATH_CAPABILITY_ALL); + if (!path) + { + status = VCOS_ENOMEM; + goto finish; + } + + vguRoundRect(path, (VGfloat)x, (VGfloat)y, (VGfloat)width, (VGfloat)height, + (VGfloat)radius, (VGfloat)radius); + vgDrawPath(path, VG_FILL_PATH); + vgDestroyPath(path); + + vcos_assert(vgGetError() == 0); + +finish: + gx_priv_restore(&save); + + return status; +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_graphics_init(const char *font_dir) +{ + GX_CLIENT_STATE_T save; + VCOS_STATUS_T rc; + + gx_priv_save(&save, NULL); + + rc = gx_priv_initialise(); + if (rc == VCOS_SUCCESS) + rc = gx_priv_font_init(font_dir); + + gx_priv_restore(&save); + + return rc; +} + +/*****************************************************************************/ +int gx_is_double_buffered(void) +{ + return 1; +} + +/*****************************************************************************/ +int32_t graphics_userblt(GRAPHICS_RESOURCE_TYPE_T src_type, + const void *src_data, + const uint32_t src_x, + const uint32_t src_y, + const uint32_t width, + const uint32_t height, + const uint32_t pitch, + GRAPHICS_RESOURCE_HANDLE dest, + const uint32_t x_pos, + const uint32_t y_pos ) +{ + VCOS_STATUS_T status; + VGImageFormat vg_src_type; + int bytes_per_pixel; + GX_CLIENT_STATE_T save; + + status = convert_image_type(src_type, &vg_src_type, &bytes_per_pixel); + if (status != VCOS_SUCCESS) + return status; + + gx_priv_save(&save, dest); + + if (dest->type == GX_PBUFFER) + { + vgImageSubData(dest->u.pixmap, + src_data, + pitch, + vg_src_type, + x_pos, y_pos, width, height); + } + else if (dest->type == GX_WINDOW) + { + // need to invert this as VG thinks zero is at the bottom + // while graphics_x thinks it is at the top. + vgWritePixels((uint8_t*)src_data + pitch*(height-1), + -pitch, + vg_src_type, + x_pos, dest->height-y_pos-height, width, height); + } + else + { + vcos_assert(0); + } + + if (vgGetError() == 0) + status = VCOS_SUCCESS; + else + { + vcos_assert(0); + status = VCOS_EINVAL; + } + + gx_priv_restore(&save); + return status; +} + +/*****************************************************************************/ +int32_t graphics_resource_text_dimensions( GRAPHICS_RESOURCE_HANDLE resource_handle, + const char *text, + const uint32_t text_length, + uint32_t *width, + uint32_t *height ) +{ + return graphics_resource_text_dimensions_ext(resource_handle, text, text_length, width, height, ATEXT_FONT_SIZE); +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_render_arrowhead(GRAPHICS_RESOURCE_HANDLE res, + uint32_t tip_x, uint32_t tip_y, + int32_t w, int32_t h, + GX_PAINT_T *p) +{ + VGfloat gradient[4]; + VGPaint paint = (VGPaint)p; + VGPath path; + VCOS_STATUS_T status = VCOS_SUCCESS; + + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + + if (!paint) + { + vcos_assert(0); + status = VCOS_EINVAL; + goto finish; + } + + gradient[0] = 0.0; gradient[1] = 0.0; + gradient[2] = w; gradient[2] = 0.0; + + vgSetParameterfv(paint, VG_PAINT_LINEAR_GRADIENT, 4, gradient); + vgSetPaint(paint, VG_FILL_PATH); + + path = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_S_32, + 1.0, 0.0, 8, 8, VG_PATH_CAPABILITY_ALL); + if (!path) + { + status = VCOS_ENOMEM; + goto finish; + } + VGfloat points[] = { + (VGfloat)tip_x, (VGfloat)tip_y, + (VGfloat)tip_x + w, (VGfloat)tip_y + h/2, + (VGfloat)tip_x + w, (VGfloat)tip_y - h/2, + }; + + vguPolygon(path, points, 3, 1); + + vgDrawPath(path, VG_FILL_PATH); + vgDestroyPath(path); + + vcos_assert(vgGetError()==0); + +finish: + gx_priv_restore(&save); + return status; +} + +/*****************************************************************************/ +int32_t gx_apply_alpha( GRAPHICS_RESOURCE_HANDLE resource_handle, + const uint8_t alpha ) +{ + vcos_assert(resource_handle); + if (resource_handle->type != GX_PBUFFER) + { + vcos_assert(0); + return -1; + } + resource_handle->alpha = 1.0*alpha/255; + return 0; +} + +/*****************************************************************************/ +int32_t graphics_resource_set_alpha_per_colour( GRAPHICS_RESOURCE_HANDLE res, + const uint32_t colour, + const uint8_t alpha ) +{ + GX_ERROR("Not implemented yet!"); + return 0; +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_get_pixels(const GRAPHICS_RESOURCE_HANDLE res, void **pixels) +{ + VCOS_STATUS_T status = VCOS_SUCCESS; + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + + /* Default to top-top-bottom raster scan order */ + status = gx_priv_get_pixels(res, pixels, GX_TOP_BOTTOM); + + gx_priv_restore(&save); + return status; +} + +/*****************************************************************************/ +VCOS_STATUS_T gx_get_pixels_in_raster_order(const GRAPHICS_RESOURCE_HANDLE res, + void **pixels, + GX_RASTER_ORDER_T raster_order) +{ + VCOS_STATUS_T status = VCOS_SUCCESS; + GX_CLIENT_STATE_T save; + gx_priv_save(&save, res); + + status = gx_priv_get_pixels(res, pixels, raster_order); + + gx_priv_restore(&save); + return status; +} + +/*****************************************************************************/ +void gx_free_pixels(const GRAPHICS_RESOURCE_HANDLE res, void *pixels) +{ + vcos_free(pixels); +} + +VCOS_STATUS_T gx_bind_vg( GX_CLIENT_STATE_T *save, GRAPHICS_RESOURCE_HANDLE res ) +{ + gx_priv_save(save, res); + vcos_assert(vgGetError()==0); + return VCOS_SUCCESS; +} + +/** Unbind VG */ +void gx_unbind_vg(GX_CLIENT_STATE_T *restore) +{ + gx_priv_restore(restore); +} + + +GX_CLIENT_STATE_T *gx_alloc_context(void) +{ + GX_CLIENT_STATE_T *ret = vcos_calloc(1,sizeof(*ret), "gx_client_state"); + return ret; +} + +void gx_free_context(GX_CLIENT_STATE_T *state) +{ + vcos_free(state); +} + +void gx_convert_colour(uint32_t colour, float *dest) +{ + gx_priv_colour_to_paint(colour, dest); +} + + +#define MAX_DISPLAY_HANDLES 4 + +#define CHANGE_LAYER (1<<0) +#define CHANGE_OPACITY (1<<1) +#define CHANGE_DEST (1<<2) +#define CHANGE_SRC (1<<3) +#define CHANGE_MASK (1<<4) +#define CHANGE_XFORM (1<<5) + +typedef struct +{ + /** Keep a display handle going for each connected screen (LCD, HDMI). */ + DISPMANX_DISPLAY_HANDLE_T screens[MAX_DISPLAY_HANDLES]; + int refcounts[MAX_DISPLAY_HANDLES]; + + //a flag to count the number of dispman starts that have been invoked + + uint32_t dispman_start_count; + // maintain the single global handle to the update in progress + DISPMANX_UPDATE_HANDLE_T current_update; + + VCOS_MUTEX_T lock; +} gx_priv_state_t; + +static gx_priv_state_t gx; + +void gx_priv_init(void) +{ + vcos_mutex_create(&gx.lock,NULL); +} + +void gx_priv_destroy(void) +{ + vcos_mutex_delete(&gx.lock); +} + + +static +int32_t gx_priv_open_screen(uint32_t index, DISPMANX_DISPLAY_HANDLE_T *pscreen) +{ + int ret = -1; + vcos_mutex_lock(&gx.lock); + + if (gx.refcounts[index] != 0) + { + *pscreen = gx.screens[index]; + gx.refcounts[index]++; + ret = 0; + } + else + { + DISPMANX_DISPLAY_HANDLE_T h = vc_dispmanx_display_open(index); + if (h == DISPMANX_NO_HANDLE) + { + GX_LOG("Could not open dispmanx display %d", index); + ret = -1; + goto finish; + } + gx.screens[index] = h; + gx.refcounts[index] = 1; + *pscreen = h; + ret = 0; + } +finish: + vcos_mutex_unlock(&gx.lock); + return ret; +} + +static +int32_t gx_priv_release_screen(uint32_t index) +{ + vcos_mutex_lock(&gx.lock); + gx.refcounts[index]--; + if (gx.refcounts[index] == 0) + { + vc_dispmanx_display_close(gx.screens[index]); + gx.screens[index] = DISPMANX_NO_HANDLE; + } + vcos_mutex_unlock(&gx.lock); + return 0; +} + + + + +int gx_priv_create_native_window(uint32_t screen_id, + uint32_t w, uint32_t h, + GRAPHICS_RESOURCE_TYPE_T type, + GX_NATIVE_WINDOW_T *win, + void **cookie) +{ + int rc; + DISPMANX_DISPLAY_HANDLE_T dispmanx_display; + VC_RECT_T dst_rect; + VC_RECT_T src_rect; + DISPMANX_UPDATE_HANDLE_T current_update; + *cookie = NULL; + + rc = gx_priv_open_screen(screen_id, &dispmanx_display); + if (rc < 0) + { + GX_LOG("Could not open display %d", screen_id); + goto fail_screen; + } + + current_update = vc_dispmanx_update_start(0); + if (!current_update) + { + GX_LOG("Could not start update on screen %d", screen_id); + goto fail_update; + } + + src_rect.x = src_rect.y = 0; + src_rect.width = w << 16; + src_rect.height = h << 16; + + dst_rect.x = dst_rect.y = 0; + dst_rect.width = dst_rect.height = 1; + + win->egl_win.width = w; + win->egl_win.height = h; + VC_DISPMANX_ALPHA_T alpha; + memset(&alpha, 0x0, sizeof(VC_DISPMANX_ALPHA_T)); + alpha.flags = DISPMANX_FLAGS_ALPHA_FROM_SOURCE; + + DISPMANX_CLAMP_T clamp; + memset(&clamp, 0x0, sizeof(DISPMANX_CLAMP_T)); + + win->egl_win.element = vc_dispmanx_element_add(current_update, dispmanx_display, + 0 /* layer */, &dst_rect, + 0 /* src */, &src_rect, + DISPMANX_PROTECTION_NONE, + &alpha /* alpha */, + &clamp /* clamp */, + 0 /* transform */); + + if ( !win->egl_win.element ) + { + GX_LOG("Could not add element %dx%d",w,h); + vc_dispmanx_update_submit_sync(current_update); + rc = -1; + } + + // have to pass back the update so it can be completed *After* the + // window has been initialised (filled with background colour). + *cookie = (void*)current_update; + + return 0; + +fail_update: + gx_priv_release_screen(screen_id); +fail_screen: + return rc; +} + +void gx_priv_finish_native_window(GRAPHICS_RESOURCE_HANDLE_TABLE_T *res, + void *current_update) +{ + vc_dispmanx_update_submit_sync((DISPMANX_UPDATE_HANDLE_T)current_update); +} + +void +gx_priv_destroy_native_window(GRAPHICS_RESOURCE_HANDLE_TABLE_T *res) +{ + DISPMANX_UPDATE_HANDLE_T current_update; + + if((current_update = vc_dispmanx_update_start(0)) != 0) + { + int ret = vc_dispmanx_element_remove(current_update, res->u.native_window.egl_win.element); + vcos_assert(ret == 0); + ret = vc_dispmanx_update_submit_sync(current_update); + vcos_assert(ret == 0); + } + + gx_priv_release_screen(res->screen_id); +} + + +/*********************************************************** + * Name: graphics_get_display_size + * + * Arguments: + * void + * + * Description: Return size of display + * + * Returns: int32_t: + * >=0 if it succeeded + * + ***********************************************************/ +int32_t graphics_get_display_size( const uint16_t display_number, + uint32_t *width, + uint32_t *height) +{ + DISPMANX_MODEINFO_T mode_info; + int32_t success = -1; + DISPMANX_DISPLAY_HANDLE_T disp; + vcos_assert(width && height); + *width = *height = 0; + + if(vcos_verify(display_number < MAX_DISPLAY_HANDLES)) + { + // TODO Shouldn't this close the display if it wasn't previously open? + if (gx_priv_open_screen(display_number, &disp) < 0) + { + vcos_assert(0); + return -1; + } + success = vc_dispmanx_display_get_info(disp, &mode_info); + + if( success >= 0 ) + { + *width = mode_info.width; + *height = mode_info.height; + vcos_assert(*height > 64); + } + else + { + vcos_assert(0); + } + } + + return success; +} + +static inline uint16_t auto_size(uint16_t arg, uint16_t actual_size) +{ + return arg == GRAPHICS_RESOURCE_WIDTH ? actual_size : arg; +} + +int32_t graphics_display_resource( GRAPHICS_RESOURCE_HANDLE res, + const uint16_t screen_number, + const int16_t z_order, + const uint16_t offset_x, + const uint16_t offset_y, + const uint16_t dest_width, + const uint16_t dest_height, + const VC_DISPMAN_TRANSFORM_T transform, + const uint8_t display ) +{ + DISPMANX_UPDATE_HANDLE_T update; + int32_t rc; + int xform_changed; + + if (!res) + { + // mimics old behaviour. + (void)vcos_verify(0); + return 0; + } + vcos_assert(res->magic == RES_MAGIC); + + xform_changed = transform != res->transform; + res->transform = transform; + + rc = graphics_update_start(); + update = gx.current_update; + vcos_assert(rc == 0); + + if (display) + { + VC_RECT_T src_rect, dest_rect; + + int32_t src_width = res->width; + int32_t src_height = res->height; + + uint32_t change_flags = CHANGE_LAYER; + + // has the destination position changed? + uint32_t w = auto_size(dest_width, res->width); + uint32_t h = auto_size(dest_height, res->height); + + vcos_assert(screen_number == res->screen_id); + + if (gx.screens[screen_number] == 0) + { + vcos_assert(0); + DISPMANX_DISPLAY_HANDLE_T display_handle; + gx_priv_open_screen(screen_number, &display_handle); + } + + if ((offset_x != res->dest.x) || + (offset_y != res->dest.y) || + (h != res->dest.height) || + (w != res->dest.width)) + { + change_flags |= CHANGE_DEST; + res->dest.x = offset_x; + res->dest.y = offset_y; + res->dest.height = h; + res->dest.width = w; + } + + if (xform_changed) + change_flags |= CHANGE_XFORM; + + vc_dispmanx_rect_set( &src_rect, 0, 0, ((uint32_t)src_width)<<16, ((uint32_t)src_height)<<16 ); + vc_dispmanx_rect_set( &dest_rect, offset_x, offset_y, w, h); + + rc = vc_dispmanx_element_change_attributes(update, + res->u.native_window.egl_win.element, + change_flags, + z_order, /* layer */ + 0xff, /* opacity */ + &dest_rect, + &src_rect, + 0, transform); + + vcos_assert(rc==0); + gx_priv_flush(res); + + } + else + { + vgFinish(); + eglWaitClient(); + rc = vc_dispmanx_element_change_source(update, res->u.native_window.egl_win.element, 0); + vcos_assert(rc==0); + } + + rc = graphics_update_end(); + vcos_assert(rc==0); + + return rc; +} + +/*********************************************************** + * Name: graphics_update_start + * + * Arguments: + * void + * + * Description: Starts an update UNLESS and update is already in progress + * + * Returns: int32_t: + * >=0 if it succeeded + * + ***********************************************************/ +int32_t graphics_update_start(void) +{ + int32_t success = 0; + + //check we are not already in an update + if ( 0 == gx.dispman_start_count ) + { + gx.current_update = vc_dispmanx_update_start( 10 ); + if( gx.current_update == DISPMANX_NO_HANDLE ) + { + //error + success = -1; + vc_assert( 0 ); + } + } + + if( success == 0 ) + { + //inc the counter + gx.dispman_start_count++; + } + + return success; +} + + +/*********************************************************** + * Name: graphics_update_end + * + * Arguments: + * void + * + * Description: Ends an update UNLESS more than one update is in progress + * + * Returns: int32_t: + * >=0 if it succeeded + * + ***********************************************************/ +int32_t graphics_update_end( void ) +{ + int32_t success = -1; + + // make sure you are checking the return value of graphics_update_start + if(vcos_verify(gx.current_update != DISPMANX_NO_HANDLE)) + { + //check we are in an update + if(vcos_verify(gx.dispman_start_count > 0)) + { + //dec the counter + gx.dispman_start_count--; + + success = 0; + + //is the counter now 0? + if( 0 == gx.dispman_start_count ) + { + eglWaitClient(); + if( vc_dispmanx_update_submit_sync( gx.current_update ) != 0 ) + { + //error + success = -1; + vc_assert( 0 ); + } + } + } + } + + return success; +} + diff --git a/gui/libs/vgfont/graphics_x_private.h b/gui/libs/vgfont/graphics_x_private.h @@ -0,0 +1,366 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Graphics library for VG + +#ifndef GRAPHICS_X_PRIVATE_H +#define GRAPHICS_X_PRIVATE_H + +#define VCOS_LOG_CATEGORY (&gx_log_cat) + +#include "EGL/egl.h" +#include "EGL/eglext.h" +#include "VG/openvg.h" +#include "VG/vgu.h" + +#include "vgfont.h" +#include "bcm_host.h" + +extern VCOS_LOG_CAT_T gx_log_cat; + +#define LOG_ERR( fmt, arg... ) vcos_log_error( "%s:%d " fmt, __func__, __LINE__, ##arg) + +#define GX_ERROR(format, arg...) if (1) {} else printf( format "\n", ##arg) +#define GX_LOG(format, arg...) if (1) {} else printf( format "\n", ##arg) +#define GX_TRACE(format, arg...) if (1) {} else printf( format "\n", ##arg) + +typedef struct +{ + EGL_DISPMANX_WINDOW_T egl_win; +} GX_NATIVE_WINDOW_T; + +typedef enum +{ + GX_TOP_BOTTOM, + GX_BOTTOM_TOP, +} GX_RASTER_ORDER_T; + +typedef struct {} GX_PAINT_T; + +typedef struct GX_CLIENT_STATE_T GX_CLIENT_STATE_T; +typedef struct { + EGLDisplay disp; +} GX_DISPLAY_T; + +struct GX_DISPLAY_T +{ + EGLDisplay disp; +}; + +typedef enum +{ + GX_WINDOW, GX_PIXMAP, GX_PBUFFER +} GX_RES_TYPE; + +#define RES_MAGIC ('G'<<24|'X'<<16|'R'<<8|'S'<<0) +#define GX_PRIV_FLAG_FLIP (1<<0) + +/** + * Structure encapsulating the per-surface state. + ***********************************************************/ +typedef struct GRAPHICS_RESOURCE_HANDLE_TABLE_T +{ + union + { + GX_NATIVE_WINDOW_T native_window; + VGImage pixmap; + } u; + GX_RES_TYPE type; + + uint32_t magic; /** To work around broken create interface */ + int context_bound; + const char *last_caller; + EGLSurface surface; + EGLContext context; + EGLConfig config; + uint32_t screen_id; /** 0-LCD, etc */ + uint16_t width; + uint16_t height; + GRAPHICS_RESOURCE_TYPE_T restype; + VC_DISPMAN_TRANSFORM_T transform; + + VC_RECT_T dest; /** destination rectangle in use, for book-keeping */ + + VGfloat alpha; +} GRAPHICS_RESOURCE_HANDLE_TABLE_T; + +/** + * Structure used to store an EGL client state. + ***********************************************************/ +struct GX_CLIENT_STATE_T +{ + EGLSurface read_surface; + EGLSurface draw_surface; + EGLContext context; + EGLenum api; + GRAPHICS_RESOURCE_HANDLE res; +}; + +/** + * \fixme add documentation + * + ***********************************************************/ +void gx_priv_init(void); + +/** + * \fixme add documentation + * + ***********************************************************/ +void gx_priv_destroy(void); + +/** + * \fixme add documentation + * + * @param col colour + * + * @param rgba OpenVG paint colour + * + ***********************************************************/ +void gx_priv_colour_to_paint(uint32_t col, VGfloat *rgba); + +/** + * Save current EGL client state. + * + * @param state upon return, holds the saved EGL client state. + * + * @param res handle to the surface the EGL client state belongs to (may be <code>NULL</code>). + * + */ +void gx_priv_save(GX_CLIENT_STATE_T *state, GRAPHICS_RESOURCE_HANDLE res); + +/** + * Restore current EGL client state. + * + * @param state the EGL client state to restore. + * + */ +void gx_priv_restore(GX_CLIENT_STATE_T *state); + +/** + * Create a native window for a surface. + * + * @param screen_id \fixme + * + * @param w width of the window + * + * @param h height of the window + * + * @param type color/raster format of the resource + * + * @param win upon successful return, holds a handle to the native window + * + * @param cookie \fixme + * + * @return VCOS_SUCCESS on success, or error code. + */ +int gx_priv_create_native_window(uint32_t screen_id, + uint32_t w, uint32_t h, + GRAPHICS_RESOURCE_TYPE_T type, + GX_NATIVE_WINDOW_T *win, + void **cookie); + +/** + * Destroy native window bound to surface. + * + * @param res Handle to surface. + * + */ +void gx_priv_destroy_native_window(GRAPHICS_RESOURCE_HANDLE_TABLE_T *res); + +/** + * Initialise font from the given directory. + * + * @param font_dir path to font + * + * \fixme only supports Vera.tff at the moment? + * + * @return VCOS_SUCCESS on success, or error code. + */ +VCOS_STATUS_T gx_priv_font_init(const char *font_dir); + +/** + * \fixme add documentation + * + ***********************************************************/ +void gx_priv_font_term(void); + +/** + * Fill an area of a surface with a single colour. + * + * @param res Handle to surface. + * + * @param x x-offset of area to fill + * + * @param y y-offset of area to fill + * + * @param width width of area to fill + * + * @param height height of area to fill + * + * @param fill_colour fill colour + * + ***********************************************************/ +VCOS_STATUS_T gx_priv_resource_fill(GRAPHICS_RESOURCE_HANDLE res, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height, + uint32_t fill_colour ); + +/** + * Render text into a surface + * + * @param disp Handle to display. + * + * @param res Handle to surface. + * + * @param x x-offset + * + * @param y y-offset + * + * @param width bounding rectangle width + * + * @param height bounding rectangle height + * + * @param fg_colour foreground color + * + * @param bg_colour background color + * + * @param text text to render + * + * @param text_length length of text + * + * @param text_size size of text + * + ***********************************************************/ +VCOS_STATUS_T gx_priv_render_text( GX_DISPLAY_T *disp, + GRAPHICS_RESOURCE_HANDLE res, + int32_t x, + int32_t y, + uint32_t width, + uint32_t height, + uint32_t fg_colour, + uint32_t bg_colour, + const char *text, + uint32_t text_length, + uint32_t text_size ); + +/** + * Flush a surface. + * + * @param res Handle to surface. + * + ***********************************************************/ +void gx_priv_flush(GRAPHICS_RESOURCE_HANDLE res); + +/** + * Called after the EGL/VG initialisation of a window has completed + * following its creation. + * + * @param res ??? + * + * @param cookie ??? + * + ***********************************************************/ +void gx_priv_finish_native_window(GRAPHICS_RESOURCE_HANDLE_TABLE_T *res, + void *cookie); + +/** + * Flush font cache. + * + ***********************************************************/ +void gx_font_cache_flush(void); + +/** + * Read a bitmap (.BMP) image from the given file. + * + * @param filename filename (must not be <code>NULL</code>). + * + * @param width holds the width of the image upon return. + * + * @param height holds the height of the image upon return. + * + * @param pitch_bytes holds the pitch of the image data (in bytes) upon return. + * + * @param restype holds the type of the image upon return. + * + * @param vg_format holds the OpenVG image format upon return. + * + * @param flags holds flags describing image properties upon return. + * + * @param image_data_size holds size of the image data upon return. + * + * @param pimage_data holds the image data buffer upon return (must not be <code>NULL</code>), + * the caller is responsible for releasing the buffer afterwards. + * + * @return 0 if success, non-zero otherwise (in which case the output parameters + * may be invalid). + * + ***********************************************************/ +int gx_priv_read_bmp(const char *file_name, + uint32_t *width, uint32_t *height, uint32_t *pitch_bytes, + GRAPHICS_RESOURCE_TYPE_T *restype, + VGImageFormat *vg_format, + uint32_t *flags, + uint32_t *image_data_size, + void **pimage_data); + +/** + * Read a Targa (.TGA) image from the given file. + * + * @param filename filename (must not be <code>NULL</code>). + * + * @param width holds the width of the image upon return. + * + * @param height holds the height of the image upon return. + * + * @param pitch_bytes holds the pitch of the image data (in bytes) upon return. + * + * @param restype holds the type of the image upon return. + * + * @param vg_format holds the OpenVG image format upon return. + * + * @param flags holds flags describing image properties upon return. + * + * @param image_data_size holds size of the image data upon return. + * + * @param pimage_data holds the image data buffer upon return (must not be <code>NULL</code>), + * the caller is responsible for releasing the memory afterwards. + * + * @return 0 if success, non-zero otherwise (in which case the output parameters. + * may be invalid). + * + ***********************************************************/ +int gx_priv_read_tga(const char *file_name, + uint32_t *width, uint32_t *height, uint32_t *pitch_bytes, + GRAPHICS_RESOURCE_TYPE_T *restype, + VGImageFormat *vg_format, + uint32_t *flags, + uint32_t *image_data_size, + void **pimage_data); + +#endif diff --git a/gui/libs/vgfont/libvgfont.a b/gui/libs/vgfont/libvgfont.a Binary files differ. diff --git a/gui/libs/vgfont/vgfont.h b/gui/libs/vgfont/vgfont.h @@ -0,0 +1,136 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Font handling for graphicsx + +#ifndef VCFTLIB_H +#define VCFTLIB_H + +#include <stdint.h> +#include "interface/vmcs_host/vc_dispservice_x_defs.h" +#include "interface/vctypes/vc_image_types.h" +#include "interface/vcos/vcos.h" + +//Definitions which in certain functions can be used to mean the actual width and height of a resource, without +//having to know the data implicitly. +#define GRAPHICS_RESOURCE_WIDTH 0xFFFF +#define GRAPHICS_RESOURCE_HEIGHT 0xFFFF + +#define R_888_MASK (0x00FF0000) +#define G_888_MASK (0x0000FF00) +#define B_888_MASK (0x000000FF) +#define ALPHA_888_MASK (0xFF000000) +#define GRAPHICS_RGBA32( r, g, b, a ) GRAPHICS_RGBA888( r, g, b, a ) +#define GRAPHICS_RGBA888( r, g, b, a ) ( (((a) << (8+8+8)) & ALPHA_888_MASK) | (((b) << (8+8)) & R_888_MASK) | (((g) << 8) & G_888_MASK) | ((r) & B_888_MASK) ) +#define GRAPHICS_TRANSPARENT_COLOUR 0x00000001UL + +//resource defs + +typedef enum +{ + GRAPHICS_RESOURCE_HANDLE_TYPE_MIN, + + GRAPHICS_RESOURCE_RGB565, + GRAPHICS_RESOURCE_RGB888, /* 888 format is ONLY used when loading bitmaps + - you can't create or delete bitmaps with this format */ + GRAPHICS_RESOURCE_RGBA32, + GRAPHICS_RESOURCE_TF_RGB32A, + GRAPHICS_RESOURCE_TF_RGB565, + GRAPHICS_RESOURCE_YUV420, + + GRAPHICS_RESOURCE_HANDLE_TYPE_MAX + +} GRAPHICS_RESOURCE_TYPE_T; + + +typedef struct GRAPHICS_RESOURCE_HANDLE_TABLE_T *GRAPHICS_RESOURCE_HANDLE; + +VCOS_STATUS_T gx_graphics_init(const char *font_dir); +int32_t graphics_delete_resource( GRAPHICS_RESOURCE_HANDLE res ); +VCOS_STATUS_T gx_create_window( uint32_t screen_id, + uint32_t width, + uint32_t height, + GRAPHICS_RESOURCE_TYPE_T image_type, + GRAPHICS_RESOURCE_HANDLE *resource_handle ); + +int32_t graphics_display_resource( GRAPHICS_RESOURCE_HANDLE res, + const uint16_t screen_number, + const int16_t z_order, + const uint16_t offset_x, + const uint16_t offset_y, + const uint16_t dest_width, + const uint16_t dest_height, + const VC_DISPMAN_TRANSFORM_T transform, + const uint8_t display ); + +int32_t graphics_resource_fill(GRAPHICS_RESOURCE_HANDLE res, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height, + uint32_t fill_colour ); + +int32_t graphics_update_displayed_resource(GRAPHICS_RESOURCE_HANDLE res, + const uint32_t x_offset, + const uint32_t y_offset, + const uint32_t width, + const uint32_t height ); + +int32_t graphics_resource_render_text_ext( GRAPHICS_RESOURCE_HANDLE res, + const int32_t x, + const int32_t y, + const uint32_t width, + const uint32_t height, + const uint32_t fg_colour, + const uint32_t bg_colour, + const char *text, + const uint32_t text_length, + const uint32_t text_size ); + +int32_t graphics_resource_text_dimensions_ext(GRAPHICS_RESOURCE_HANDLE res, + const char *text, + const uint32_t text_length, + uint32_t *width, + uint32_t *height, + const uint32_t text_size ); + +int32_t graphics_get_resource_size( + const GRAPHICS_RESOURCE_HANDLE res, + uint32_t *w, + uint32_t *h); + +int32_t graphics_update_start(void); + +int32_t graphics_update_end( void ); + +int32_t graphics_resource_text_dimensions( GRAPHICS_RESOURCE_HANDLE resource_handle, + const char *text, + const uint32_t text_length, + uint32_t *width, + uint32_t *height ); + +#endif diff --git a/gui/libs/vgfont/vgft.c b/gui/libs/vgfont/vgft.c @@ -0,0 +1,416 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Font handling for graphicsx + +#include <assert.h> +#include <stdlib.h> + +#include "graphics_x_private.h" +#include "vgft.h" + +static FT_Library lib; + +int vgft_init(void) +{ + if (FT_Init_FreeType(&lib) == 0) + return 0; + else + { + return -1; + } +} + +void vgft_term(void) +{ + FT_Done_FreeType(lib); +} + +#define SEGMENTS_COUNT_MAX 256 +#define COORDS_COUNT_MAX 1024 + +static VGuint segments_count; +static VGubyte segments[SEGMENTS_COUNT_MAX]; +static VGuint coords_count; +static VGfloat coords[COORDS_COUNT_MAX]; + +static VGfloat float_from_26_6(FT_Pos x) +{ + return (VGfloat)x / 64.0f; +} + +static void convert_contour(const FT_Vector *points, const char *tags, short points_count) +{ + int first_coords = coords_count; + + int first = 1; + char last_tag = 0; + int c = 0; + + for (; points_count != 0; ++points, ++tags, --points_count) { + ++c; + + char tag = *tags; + if (first) { + assert(tag & 0x1); + assert(c==1); c=0; + segments[segments_count++] = VG_MOVE_TO; + first = 0; + } else if (tag & 0x1) { + /* on curve */ + + if (last_tag & 0x1) { + /* last point was also on -- line */ + assert(c==1); c=0; + segments[segments_count++] = VG_LINE_TO; + } else { + /* last point was off -- quad or cubic */ + if (last_tag & 0x2) { + /* cubic */ + assert(c==3); c=0; + segments[segments_count++] = VG_CUBIC_TO; + } else { + /* quad */ + assert(c==2); c=0; + segments[segments_count++] = VG_QUAD_TO; + } + } + } else { + /* off curve */ + + if (tag & 0x2) { + /* cubic */ + + assert((last_tag & 0x1) || (last_tag & 0x2)); /* last either on or off and cubic */ + } else { + /* quad */ + + if (!(last_tag & 0x1)) { + /* last was also off curve */ + + assert(!(last_tag & 0x2)); /* must be quad */ + + /* add on point half-way between */ + assert(c==2); c=1; + segments[segments_count++] = VG_QUAD_TO; + VGfloat x = (coords[coords_count - 2] + float_from_26_6(points->x)) * 0.5f; + VGfloat y = (coords[coords_count - 1] + float_from_26_6(points->y)) * 0.5f; + coords[coords_count++] = x; + coords[coords_count++] = y; + } + } + } + last_tag = tag; + + coords[coords_count++] = float_from_26_6(points->x); + coords[coords_count++] = float_from_26_6(points->y); + } + + if (last_tag & 0x1) { + /* last point was also on -- line (implicit with close path) */ + assert(c==0); + } else { + ++c; + + /* last point was off -- quad or cubic */ + if (last_tag & 0x2) { + /* cubic */ + assert(c==3); c=0; + segments[segments_count++] = VG_CUBIC_TO; + } else { + /* quad */ + assert(c==2); c=0; + segments[segments_count++] = VG_QUAD_TO; + } + + coords[coords_count++] = coords[first_coords + 0]; + coords[coords_count++] = coords[first_coords + 1]; + } + + segments[segments_count++] = VG_CLOSE_PATH; +} + +static void convert_outline(const FT_Vector *points, const char *tags, const short *contours, short contours_count, short points_count) +{ + segments_count = 0; + coords_count = 0; + + short last_contour = 0; + for (; contours_count != 0; ++contours, --contours_count) { + short contour = *contours + 1; + convert_contour(points + last_contour, tags + last_contour, contour - last_contour); + last_contour = contour; + } + assert(last_contour == points_count); + + assert(segments_count <= SEGMENTS_COUNT_MAX); /* oops... we overwrote some memory */ + assert(coords_count <= COORDS_COUNT_MAX); +} + +VCOS_STATUS_T vgft_font_init(VGFT_FONT_T *font) +{ + font->ft_face = NULL; + font->vg_font = vgCreateFont(0); + if (font->vg_font == VG_INVALID_HANDLE) + { + return VCOS_ENOMEM; + } + return VCOS_SUCCESS; +} + +VCOS_STATUS_T vgft_font_load_mem(VGFT_FONT_T *font, void *mem, size_t len) +{ + if (FT_New_Memory_Face(lib, mem, len, 0, &font->ft_face)) + { + return VCOS_EINVAL; + } + return VCOS_SUCCESS; +} + +VCOS_STATUS_T vgft_font_load_file(VGFT_FONT_T *font, const char *file) +{ + if (FT_New_Face(lib, file, 0, &font->ft_face)) { + return VCOS_EINVAL; + } + return VCOS_SUCCESS; +} + +VCOS_STATUS_T vgft_font_convert_glyphs(VGFT_FONT_T *font, unsigned int char_height, unsigned int dpi_x, unsigned int dpi_y) +{ + FT_UInt glyph_index; + FT_ULong ch; + + if (FT_Set_Char_Size(font->ft_face, 0, char_height, dpi_x, dpi_y)) + { + FT_Done_Face(font->ft_face); + vgDestroyFont(font->vg_font); + return VCOS_EINVAL; + } + + ch = FT_Get_First_Char(font->ft_face, &glyph_index); + + while (ch != 0) + { + if (FT_Load_Glyph(font->ft_face, glyph_index, FT_LOAD_DEFAULT)) { + FT_Done_Face(font->ft_face); + vgDestroyFont(font->vg_font); + return VCOS_ENOMEM; + } + + VGPath vg_path; + FT_Outline *outline = &font->ft_face->glyph->outline; + if (outline->n_contours != 0) { + vg_path = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F, 1.0f, 0.0f, 0, 0, VG_PATH_CAPABILITY_ALL); + assert(vg_path != VG_INVALID_HANDLE); + + convert_outline(outline->points, outline->tags, outline->contours, outline->n_contours, outline->n_points); + vgAppendPathData(vg_path, segments_count, segments, coords); + } else { + vg_path = VG_INVALID_HANDLE; + } + + VGfloat origin[] = { 0.0f, 0.0f }; + VGfloat escapement[] = { float_from_26_6(font->ft_face->glyph->advance.x), float_from_26_6(font->ft_face->glyph->advance.y) }; + vgSetGlyphToPath(font->vg_font, glyph_index, vg_path, VG_FALSE, origin, escapement); + + if (vg_path != VG_INVALID_HANDLE) { + vgDestroyPath(vg_path); + } + ch = FT_Get_Next_Char(font->ft_face, ch, &glyph_index); + } + + return VCOS_SUCCESS; +} + +void vgft_font_term(VGFT_FONT_T *font) +{ + if (font->ft_face) + FT_Done_Face(font->ft_face); + if (font->vg_font) + vgDestroyFont(font->vg_font); + memset(font, 0, sizeof(*font)); +} + + +#define CHAR_COUNT_MAX 200 +static VGuint glyph_indices[CHAR_COUNT_MAX]; +static VGfloat adjustments_x[CHAR_COUNT_MAX]; +static VGfloat adjustments_y[CHAR_COUNT_MAX]; + +// Draws the first char_count characters from text, with adjustments, starting +// from the current origin. The peek argument indicates whether to peek ahead +// and get a final adjustment based on the next character past char_count, or +// else just assume that this is the end of the text and add no final +// adjustment. + +static void draw_chars(VGFT_FONT_T *font, const char *text, int char_count, VGbitfield paint_modes, int peek) { + // Put in first character + glyph_indices[0] = FT_Get_Char_Index(font->ft_face, text[0]); + int prev_glyph_index = glyph_indices[0]; + + // Calculate glyph_indices and adjustments + int i; + FT_Vector kern; + for (i = 1; i != char_count; ++i) { + int glyph_index = FT_Get_Char_Index(font->ft_face, text[i]); + if (!glyph_index) { return; } + glyph_indices[i] = glyph_index; + + if (FT_Get_Kerning(font->ft_face, prev_glyph_index, glyph_index, FT_KERNING_DEFAULT, &kern)) assert(0); + adjustments_x[i - 1] = float_from_26_6(kern.x); + adjustments_y[i - 1] = float_from_26_6(kern.y); + + prev_glyph_index = glyph_index; + } + + // Get the last adjustment? + if (peek) { + int peek_glyph_index = FT_Get_Char_Index(font->ft_face, text[i]); + if (!peek_glyph_index) { return; } + if (FT_Get_Kerning(font->ft_face, prev_glyph_index, peek_glyph_index, FT_KERNING_DEFAULT, &kern)) assert(0); + adjustments_x[char_count - 1] = float_from_26_6(kern.x); + adjustments_y[char_count - 1] = float_from_26_6(kern.y); + } else { + adjustments_x[char_count - 1] = 0.0f; + adjustments_y[char_count - 1] = 0.0f; + } + + vgDrawGlyphs(font->vg_font, char_count, glyph_indices, adjustments_x, adjustments_y, paint_modes, VG_FALSE); +} + +// Goes to the x,y position and draws arbitrary number of characters, draws +// iteratively if the char_count exceeds the max buffer size given above. + +static void draw_line(VGFT_FONT_T *font, VGfloat x, VGfloat y, const char *text, int char_count, VGbitfield paint_modes) { + if (char_count == 0) return; + + // Set origin to requested x,y + VGfloat glor[] = { x, y }; + vgSetfv(VG_GLYPH_ORIGIN, 2, glor); + + // Draw the characters in blocks to reuse buffer memory + const char *curr_text = text; + int chars_left = char_count; + while (chars_left > CHAR_COUNT_MAX) { + draw_chars(font, curr_text, CHAR_COUNT_MAX, paint_modes, 1); + chars_left -= CHAR_COUNT_MAX; + curr_text += CHAR_COUNT_MAX; + } + + // Draw the last block + draw_chars(font, curr_text, chars_left, paint_modes, 0); +} + +void vgft_font_draw(VGFT_FONT_T *font, VGfloat x, VGfloat y, const char *text, unsigned text_length, VGbitfield paint_modes) +{ + VGfloat descent = float_from_26_6(font->ft_face->size->metrics.descender); + int last_draw = 0; + int i = 0; + y -= descent; + for (;;) { + int last = !text[i] || (text_length && i==text_length); + + if ((text[i] == '\n') || last) + { + draw_line(font, x, y, text + last_draw, i - last_draw, paint_modes); + last_draw = i+1; + y -= float_from_26_6(font->ft_face->size->metrics.height); + } + if (last) + { + break; + } + ++i; + } +} + +// Get text extents for a single line + +static void line_extents(VGFT_FONT_T *font, VGfloat *x, VGfloat *y, const char *text, int chars_count) +{ + int i; + int prev_glyph_index = 0; + if (chars_count == 0) return; + + for (i=0; i < chars_count; i++) + { + int glyph_index = FT_Get_Char_Index(font->ft_face, text[i]); + if (!glyph_index) return; + + if (i != 0) + { + FT_Vector kern; + if (FT_Get_Kerning(font->ft_face, prev_glyph_index, glyph_index, + FT_KERNING_DEFAULT, &kern)) + { + assert(0); + } + *x += float_from_26_6(kern.x); + *y += float_from_26_6(kern.y); + } + FT_Load_Glyph(font->ft_face, glyph_index, FT_LOAD_DEFAULT); + *x += float_from_26_6(font->ft_face->glyph->advance.x); + + prev_glyph_index = glyph_index; + } +} + +// Text extents for some ASCII text. +// +// Use text_length if non-zero, otherwise look for trailing '\0'. + +void vgft_get_text_extents(VGFT_FONT_T *font, + const char *text, + unsigned text_length, + VGfloat unused0, VGfloat unused1, + VGfloat *w, VGfloat *h) { + int last_draw = 0; + VGfloat max_x = 0; + VGfloat y = 0; + + int i, last; + for (i = 0, last = 0; !last; ++i) { + last = !text[i] || (text_length && i==text_length); + if ((text[i] == '\n') || last) { + VGfloat x = 0; + line_extents(font, &x, &y, text + last_draw, i - last_draw); + last_draw = i + 1; + y -= float_from_26_6(font->ft_face->size->metrics.height); + if (x > max_x) max_x = x; + } + } + *w = max_x; + *h = -y; +} + +// Get y offset for first line; mitigates issue of start y being middle of block +// for multiline renders by vgft_font_draw. Currently simple, may be worth +// adding y kerning? + +VGfloat vgft_first_line_y_offset(VGFT_FONT_T *font) { + return float_from_26_6(font->ft_face->size->metrics.height); +} diff --git a/gui/libs/vgfont/vgft.h b/gui/libs/vgfont/vgft.h @@ -0,0 +1,70 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Font handling for graphicsx + +#ifndef VGFT_H +#define VGFT_H + +#include "interface/vcos/vcos.h" +#include <VG/openvg.h> +#include <ft2build.h> + +typedef int VGFT_BOOL; +#define VGFT_FALSE 0 +#define VGFT_TRUE (!VGFT_FALSE) + +#include FT_FREETYPE_H + +/* Returns 0 on success */ +extern int vgft_init(void); +extern void vgft_term(void); + +typedef struct { + VGFont vg_font; + FT_Face ft_face; +} VGFT_FONT_T; + +/** Initialise a FT->VG font */ +VCOS_STATUS_T vgft_font_init(VGFT_FONT_T *font); + +/** Load a font file from memory */ +VCOS_STATUS_T vgft_font_load_mem(VGFT_FONT_T *font, void *mem, size_t len); + +/** Convert a font into VG glyphs */ +VCOS_STATUS_T vgft_font_convert_glyphs(VGFT_FONT_T *font, unsigned int char_height, unsigned int dpi_x, unsigned int dpi_y); + +/** Release a font. */ +void vgft_font_term(VGFT_FONT_T *font); + +void vgft_font_draw(VGFT_FONT_T *font, VGfloat x, VGfloat y, const char *text, unsigned text_length, VGbitfield paint_modes); + +void vgft_get_text_extents(VGFT_FONT_T *font, const char *text, unsigned text_length, VGfloat start_x, VGfloat start_y, VGfloat *w, VGfloat *h); + +VGfloat vgft_first_line_y_offset(VGFT_FONT_T *font); + +#endif diff --git a/gui/src/Makefile b/gui/src/Makefile @@ -0,0 +1,7 @@ +OBJS=main.o +BIN=../tarinagui.bin + +LDFLAGS+=-lvgfont -lfreetype -lz + +include ../Makefile.include + diff --git a/gui/src/VeraMono.ttf b/gui/src/VeraMono.ttf Binary files differ. diff --git a/gui/src/fixedsys.ttf b/gui/src/fixedsys.ttf Binary files differ. diff --git a/gui/src/main.c b/gui/src/main.c @@ -0,0 +1,317 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Test app for VG font library. + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <unistd.h> + +#include "bcm_host.h" +#include "vgfont.h" + +int32_t render_subtitle(GRAPHICS_RESOURCE_HANDLE img, const char *text, const uint32_t text_size, const uint32_t text_size_selected, const uint32_t x_offset, const uint32_t y_offset, uint32_t fontcolor) +{ + uint32_t height=0; + uint32_t img_w, img_h; + + graphics_get_resource_size(img, &img_w, &img_h); + + // split now points to last line of text. split-text = length of initial text. text_length-(split-text) is length of last line + if (fontcolor == 7) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(255,255,255,100), /* fg */ + GRAPHICS_RGBA32(20,20,20,200), /* bg */ + text, 90, text_size); + } + if (fontcolor == 6) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(225,255,255,0), /* fg */ + GRAPHICS_RGBA32(0,0,0,0), /* bg */ + text, 90, text_size); + } + if (fontcolor == 5) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(255,255,255,0xff), /* fg */ + GRAPHICS_RGBA32(0,0,0,150), /* bg */ + text, 90, text_size); + } + if (fontcolor == 4) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(30,255,255,0xff), /* fg */ + GRAPHICS_RGBA32(0,0,0,0), /* bg */ + text, 90, text_size); + } + if (fontcolor == 3) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(30,30,255,0xff), /* fg */ + GRAPHICS_RGBA32(0,0,0,0), /* bg */ + text, 90, text_size); + } + if (fontcolor == 2) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(30,255,30,0xff), /* fg */ + GRAPHICS_RGBA32(0,0,0,0), /* bg */ + text, 90, text_size); + } + if (fontcolor == 1) { + graphics_resource_render_text_ext(img, x_offset, y_offset-height, + GRAPHICS_RESOURCE_WIDTH, + GRAPHICS_RESOURCE_HEIGHT, + GRAPHICS_RGBA32(0,0,0,0xff), /* fg */ + GRAPHICS_RGBA32(30,255,255,0xff), /* bg */ + text, 90, text_size_selected); + } + return 0; + } + +int main(void) +{ + GRAPHICS_RESOURCE_HANDLE img; + uint32_t width, height; + int LAYER=10; + bcm_host_init(); + int s; + + s = gx_graphics_init("."); + assert(s == 0); + + s = graphics_get_display_size(0, &width, &height); + assert(s == 0); + + s = gx_create_window(0, width, height, GRAPHICS_RESOURCE_RGBA32, &img); + assert(s == 0); + + // transparent before display to avoid screen flash + graphics_resource_fill(img, 0, 0, width, height, GRAPHICS_RGBA32(0,0,0,0x00)); + + graphics_display_resource(img, 0, LAYER, 0, 0, GRAPHICS_RESOURCE_WIDTH, GRAPHICS_RESOURCE_HEIGHT, VC_DISPMAN_ROT0, 1); + + FILE * fp; + FILE * fp2; + FILE * fp3; + char * line = 0; + //char * b = NULL; + size_t len = 0; + ssize_t read = 0; + int linenr = 0; + int selected; + int showmenu; + int header; + int menuadd = 1; + char newread[500]; + char oldread[500]; + char vumeter[130]; + uint32_t y_offset2 = 0; + uint32_t y_offset3 = 21; + uint32_t y_offset4 = 42; + uint32_t y_offset5 = 421; + uint32_t y_offset6 = 442; + uint32_t y_offset = 466; + uint32_t text_size = 15; + uint32_t text_size_selected = 16; + int space = 10; + int morespace = 12; + int rectime = 700; + int color = 3; + int row1 = 0; + int row2 = 0; + int row3 = 0; + int row4 = 0; + int row5 = 0; + int moverow = 0; + if (width == 1920){ + y_offset2 = 5; + y_offset3 = 45; + y_offset4 = 85; + y_offset5 = 960; + y_offset6 = 1000; + y_offset = 1040; + rectime = 1600; + text_size = 30; + text_size_selected = 32; + space = 23; + morespace = 27; + } + if (width < 799){ + y_offset2 = 16; + y_offset3 = 30; + y_offset4 = 43; + y_offset5 = 433; + y_offset6 = 445; + y_offset = 457; + rectime = 600; + text_size = 11; + text_size_selected = 11; + space = 7; + morespace = 10; + moverow = 20; + } + while (1) { + // char ch; + linenr = 0; + fp = fopen("/dev/shm/interface","r"); + if (fp != NULL){ + fread(newread, sizeof(char), 500, fp); + fclose(fp); + } + fp3 = fopen("/dev/shm/vumeter","r"); + while(fgets(vumeter, 130, fp3) != NULL); + fclose(fp3); + // check if something has changed + if (strcmp(newread, oldread) != 0) { + strcpy(oldread, newread); + //const char *text = "Never give up on your dreams"; + color = 3; + row1 = 0+moverow; + row2 = 0+moverow; + row3 = 0+moverow; + row4 = 0+moverow; + row5 = 0+moverow; + graphics_resource_fill(img, 0, 0, width, height, GRAPHICS_RGBA32(0,0,0,0x00)); + // blue, at the top (y=40) + // selected 0 1 2 3 4 5 6 7 8 + + // draw the text if updated + fp2 = fopen("/dev/shm/interface", "r"); + if (fp2 != NULL){ + while ((read = getline(&line, &len, fp2)) != -1) { + //line = b; + read = read - 1; //don't count the selected line + line[read] = '\0'; //remove return char + //strcat(line, " "); + //printf("%s",line); + if (linenr == 0) + selected = atoi(line); + if (linenr == 1) + showmenu = atoi(line); + if (linenr == selected + 2 + menuadd) + color = 1; //selected color + else { + if (showmenu == 1) + color = 5; //unselected; + if (showmenu == 2) + color = 7; + if (showmenu == 0) + color = 6; + }; + if ((linenr == 2) && (read == 0)) + header = 0; + if ((linenr == 2) && (read > 0)) + header = 1; + if (selected == 420){ + if (linenr == 1) + render_subtitle(img, line, text_size, text_size_selected, moverow, y_offset2, 5); + if (linenr > 1) { + render_subtitle(img, line, text_size, text_size_selected, row1, y_offset3, 5); + row1 += read * space + morespace; + } + } + if (header == 0){ //check if normal menu or header menu + if (selected < 420){ + if ((linenr == 6+menuadd) && (read > 0)){ //show recording time if there is any + render_subtitle(img, line, text_size, text_size_selected, rectime, y_offset2, 3); + } + if (linenr >= 2+menuadd && linenr <= 5+menuadd){ + if (color == 6) + color = 5; + render_subtitle(img, line, text_size, text_size_selected, row1, y_offset2, color); + row1 += read * space + morespace; + } + if (linenr >= 7+menuadd && linenr <= 12+menuadd){ + render_subtitle(img, line, text_size, text_size_selected, row2, y_offset3, color); + row2 += read * space + morespace; + } + if (linenr >= 13+menuadd && linenr <= 20+menuadd){ + render_subtitle(img, line, text_size, text_size_selected, row3, y_offset4, color); + row3 += read * space + morespace; + } + if (linenr >= 21+menuadd && linenr <= 27+menuadd){ + render_subtitle(img, line, text_size, text_size_selected, row4, y_offset5, color); + row4 += read * space + morespace; + } + if (linenr >= 28+menuadd && linenr <= 40+menuadd){ + render_subtitle(img, line, text_size, text_size_selected, row5, y_offset6, color); + row5 += read * space + morespace; + } + } + } + else { // header menu + if (linenr == 1+menuadd) + render_subtitle(img, line, text_size, text_size_selected, moverow, y_offset2, 5); + if (linenr > 1+menuadd){ + render_subtitle(img, line, text_size, text_size_selected, row1, y_offset3, color); + row1 += read * space + morespace; + } + } + linenr += 1; + free(line); + line = NULL; + } + free(line); + line = NULL; + fclose(fp2); + } + //graphics_update_displayed_resource(img, 0, 0, 0, 0); + } + char s_vol1 = vumeter[85]; + char s_vol2 = vumeter[86]; + char s_vol[1]; + s_vol[0] = s_vol1; + s_vol[1] = s_vol2; + int vol = atoi(s_vol); + int vucolor = 6; + //printf("%s", s_vol); + if (vol >= 0 && vol < 35) + vucolor = 4; + if (vol >= 35 && vol < 99) + vucolor = 2; + if (vol >= 99) + vucolor = 3; + render_subtitle(img, vumeter, text_size, text_size_selected , moverow, y_offset, vucolor); + graphics_update_displayed_resource(img, 0, 0, 0, 0); + usleep(20000); + } + graphics_display_resource(img, 0, LAYER, 0, 0, GRAPHICS_RESOURCE_WIDTH, GRAPHICS_RESOURCE_HEIGHT, VC_DISPMAN_ROT0, 0); + graphics_delete_resource(img); + return 0; +} + diff --git a/gui/src/main_new.c b/gui/src/main_new.c @@ -0,0 +1,42 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +int main(void) +{ + FILE * fp; + FILE * fp2; + char * line = NULL; + size_t len = 0; + ssize_t read; + int linenr = 0; + int selected; + char newread[500]; + char oldread[500]; + + while (1) { + linenr = 0; + fp = fopen("interface", "r"); + fread(newread, sizeof(char), 500, fp); + fclose(fp); + if (strcmp(newread, oldread) != 0) { + strcpy(oldread, newread); + fp2 = fopen("interface", "r"); + while ((read = getline(&line, &len, fp2)) != -1) { + if (linenr == 0) + selected = atoi(line); + if (linenr == selected) + printf("selected"); + printf("%zu",read); + printf(line); + linenr += 1; + } + fclose(fp2); + //if (line) + // free(line); + } + usleep(20000); + } +} diff --git a/gui/tarinagui.bin b/gui/tarinagui.bin Binary files differ. diff --git a/install.sh b/install.sh @@ -0,0 +1,392 @@ +#!/bin/bash +#sed -i '/FILESYSTEMS=/c\FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat"' /etc/usbmount/usbmount.conf + +ROOT_UID=0 # Root has $UID 0. + +update=$1 + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + echo "sudo ./install.sh" + exit 0 +fi + +echo "Hurray, you are root! Let's do this.." +cat <<'EOF' + ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ +█ █ █ █ █ █ █ █ █ █ +█ ▄▄▄▄█ ▄ █ █▄█ █▄▄▄▄ █ ▄ █ ▄ █ █ +█ █ ▄▄█ █ █ █ █▄▄▄▄█ █ █ █ █ █▄█ █ █ +█ █ █ █ █▄█ █ ▄ █ ▄▄▄▄▄▄█ █▄█ █ ▄▄▄█ █ +█ █▄▄█ █ █ █ █ █ █▄▄▄▄▄█ █ █ █ █ +█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄█ █▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█ +EOF +sleep 1 + +if grep -q -F '#gonzopi-rpi-configuration-1.0' /boot/config.txt +then +echo "screen drivers found! remove them in /boot/config.txt" +else +echo "Select screen driver to be installed" +select screen in hyperpixel4 ugeek-hdtft hdmi-screen +do +echo $screen +break +done +fi +echo "setting up system for filmmaking flow..." +echo "if something goes wrong please submit bug to https://github.com/rbckman/gonzopi" +sleep 2 +version="$(lsb_release -c -s)" +if [ "$version" = "buster" ] +then + echo "Debian Buster found" +else + echo "Debian Stretch found" +fi +echo "Installing all dependencies..." + +apt-get update +apt-get upgrade -y +if [ "$version" = "buster" ] +then + apt-get -y install git python3-pip python-configparser ffmpeg mediainfo gpac omxplayer sox cpufrequtils apache2 libapache2-mod-wsgi-py3 libdbus-glib-1-dev dbus libdbus-1-dev usbmount python3-numpy python3-pil python3-smbus python3-shortuuid wiringpi make gcc cmake pmount python3-ifaddr +else + apt-get -y install git python3-pip python-configparser libav-tools mediainfo gpac omxplayer sox cpufrequtils apache2 libapache2-mod-wsgi-py3 libdbus-glib-1-dev dbus libdbus-1-dev usbmount python3-numpy python3-pil python3-smbus python3-shortuuid wiringpi make gcc cmake python3-ifaddr +fi +echo "installing python-omxplayer-wrapper..." +sudo pip3 install omxplayer-wrapper +echo "installing blessed..." +sudo pip3 install blessed +echo "installing secrets..." +sudo pip3 install secrets +sudo pip3 install numpy +sudo pip3 install RPi.GPIO +echo "installing picamerax with lens shading correction..." +#sudo pip3 --no-cache-dir install https://github.com/chrisruk/picamera/archive/hq-camera-new-framerates.zip --upgrade +sudo pip3 install --upgrade picamerax +echo "installing web.py for the gonzopi webserver..." +sudo pip3 install web.py==0.61 +if [ "$screen" = "ugeek-hdtft" ] +then +echo "installing ugeek screen drivers" +echo "Tarina configuration seems to be in order in /boot/config.txt" +echo "Adding to /boot/config.txt" +cat <<'EOF' >> /boot/config.txt +#-----Tarina configuration starts here------- +#gonzopi-rpi-configuration-ugeek-1.0 +#Rpi-hd-tft +dtoverlay=dpi18 +overscan_left=0 +overscan_right=0 +overscan_top=0 +overscan_bottom=0 +framebuffer_width=800 +framebuffer_height=480 +enable_dpi_lcd=1 +display_default_lcd=1 +dpi_group=2 +dpi_mode=87 +dpi_output_format=0x6f015 +hdmi_timings=480 0 16 16 24 800 0 4 2 2 0 0 0 60 0 32000000 6 +dtoverlay=pi3-disable-bt-overlay +dtoverlay=i2c-gpio,i2c_gpio_scl=24,i2c_gpio_sda=23framebuffer_height=480 +display_rotate=3 +start_x=1 +gpu_mem=256 +disable_splash=1 +force_turbo=1 +boot_delay=1 +dtparam=i2c_arm=on +# dtparam=sd_overclock=90 +# Disable the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off +# Disable the PWR LED. +dtparam=pwr_led_trigger=none +dtparam=pwr_led_activelow=off + +#--------Tarina configuration end here--------- +EOF +elif [ "$screen" = "hyperpixel4" ] +then +apt-get -y install curl +echo "installing hyperpixel4 screen drivers" +curl -sSL get.pimoroni.com/hyperpixel4-legacy | bash +cat <<'EOF' >> /etc/udev/rules.d/98-hyperpixel4-calibration.rules +ATTRS{name}=="Goodix Capacitive TouchScreen", ENV{LIBINPUT_CALIBRATION_MATRIX}="0 1 0 -1 0 1" +EOF +echo "Tarina configuration seems to be in order in /boot/config.txt" +echo "Adding to /boot/config.txt" +cat <<'EOF' >> /boot/config.txt +#-----Tarina configuration starts here------- +#gonzopi-rpi-configuration-hyperpixel-1.0 +#hyperpixel +start_x=1 +gpu_mem=256 +disable_splash=1 +force_turbo=1 +boot_delay=1 +# dtparam=sd_overclock=90 +# Disable the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off +# Disable the PWR LED. +dtparam=pwr_led_trigger=none +dtparam=pwr_led_activelow=off +framebuffer_width=800 +framebuffer_height=480 +#hdmi_force_hotplug=1 +hdmi_group=1 +hdmi_mode=3 +[EDID=N/A-] ##Hyperpixel HD CONFIG + +dtoverlay=hyperpixel4 +overscan_left=0 +overscan_right=0 +overscan_top=0 +overscan_bottom=0 +enable_dpi_lcd=1 +display_default_lcd=1 +display_rotate=1 +dpi_group=2 +hdmi_group=2 +dpi_mode=87 +dpi_output_format=0x7f216 +hdmi_timings=480 0 10 16 59 800 0 15 113 15 0 0 0 60 0 32000000 6 + +#--------Tarina configuration end here--------- +EOF + +if [ "$screen" = "hdmi-screen" ] +then +echo "configuring hdmi screen" +echo "Tarina configuration seems to be in order in /boot/config.txt" +echo "Adding to /boot/config.txt" +cat <<'EOF' >> /boot/config.txt +#-----Tarina configuration starts here------- +#gonzopi-rpi-configuration-hdmi-screen +start_x=1 +gpu_mem=256 +disable_splash=1 +force_turbo=1 +boot_delay=1 +# dtparam=sd_overclock=90 +# Disable the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off +# Disable the PWR LED. +dtparam=pwr_led_trigger=none +dtparam=pwr_led_activelow=off +framebuffer_width=800 +framebuffer_height=480 +#hdmi_force_hotplug=1 +hdmi_group=1 +hdmi_mode=3 +#--------Tarina configuration end here--------- +EOF + +else +echo "screen driver already there, to change it remove gonzopi config in /boot/config.txt" +fi + +echo "Change hostname to gonzopi" +cat <<'EOF' > /etc/hostname +gonzopi +EOF + +cat <<'EOF' > /etc/hosts +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +127.0.1.1 gonzopi +EOF + +echo "consoleblank=0 logo.nologo loglevel=0" +echo "may be put at the end of line in this file /boot/cmdline.txt" +sleep 4 + +echo "Make USB soundcard default" +echo "writing to /etc/modprobe.d/alsa-base.conf" +if [ "$version" = "buster" ] +then +echo "Debian Buster Alsa config" +cat <<'EOF' > /etc/modprobe.d/alsa-base.conf +#set index value +options snd-usb-audio index=0 +options snd_bcm2835 index=1 +#reorder +options snd slots=snd_usb_audio, snd_bcm2835 +EOF +else +echo "Debian Stretch Alsa config" +cat <<'EOF' > /etc/modprobe.d/alsa-base.conf +#set index value +options snd_usb_audio index=0 +options snd_bcm2835 index=1 +#reorder +options snd slots=snd_usb_audio, snd_bcm2835 +EOF +fi + +echo "Automatically boot to Tarina" +echo "creating a gonzopi.service file" +cat <<'EOF' > /etc/systemd/system/gonzopi.service +[Unit] +Description=gonzopi +After=getty.target +Conflicts=getty@tty1.service +DefaultDependencies=false + +[Service] +Type=simple +RemainAfterExit=yes +ExecStart=/usr/bin/python3 /home/pi/gonzopi/gonzopi.py default +User=pi +Restart=on-failure +StandardInput=tty-force +StandardOutput=inherit +StandardError=inherit +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +Nice=-20 +CPUSchedulingPolicy=rr +CPUSchedulingPriority=99 + +[Install] +WantedBy=local-fs.target +EOF + +#dont kill process if user log out or in + +cat <<'EOF' >> /etc/systemd/logind.conf +KillUserProcesses=no +EOF + + +#thanx systemd for making me search for years to make this all workd like a normal programd. +loginctl enable-linger +loginctl enable-linger pi + +chmod +x /home/pi/gonzopi/gonzopi.py +systemctl enable gonzopi.service +systemctl daemon-reload +echo "systemd configuration done!" + +echo "Installing gonzopi apache server configuration" +cp extras/gonzopi.conf /etc/apache2/sites-available/ +#ln -s -t /var/www/ /home/pi/gonzopi/srv/ +a2dissite 000-default.conf +a2ensite gonzopi.conf +echo "configure srv path to /home/pi/gonzopi/srv" + +cat <<'EOF' >> /etc/apache2/apache2.conf +<Directory /home/pi/gonzopi/srv> + Options Indexes FollowSymLinks + AllowOverride None + Require all granted +</Directory> +EOF +systemctl reload apache2 + +echo 'Dont do sync while copying to usb drives, does increase speed alöt!' +sed -i '/MOUNTOPTIONS=/c\MOUNTOPTIONS="noexec,nodev,noatime,nodiratime"' /etc/usbmount/usbmount.conf + +echo "Adding harddrive tools..." +cat <<'EOF' +All this hard work to figure out how to keep NTFS mounted was done by F. Untermoser +https://raspberrypi.stackexchange.com/questions/41959/automount-various-usb-stick-file-systems-on-jessie-lite +Thanks alot! + +while we are at it :) +To all the amazing FOSS people out there big big props and + _____ ______ _____ _____ ______ _____ _______ _ + | __ \| ____|/ ____| __ \| ____/ ____|__ __| | + | |__) | |__ | (___ | |__) | |__ | | | | | | + | _ /| __| \___ \| ___/| __|| | | | | | + | | \ \| |____ ____) | | | |___| |____ | | |_| + |_| \_\______|_____/|_| |______\_____| |_| (_) + +EOF +apt-get -y install ntfs-3g exfat-fuse +#sed -i -e 's/MountFlags=slave/MountFlags=shared/g' /lib/systemd/system/systemd-udevd.service +#sed -i '/FS_MOUNTOPTIONS=/c\FS_MOUNTOPTIONS="-fstype=ntfs-3g,nls=utf8,umask=007,gid=46 -fstype=fuseblk,nls=utf8,umask=007,gid=46 -fstype=vfat,gid=1000,uid=1000,umask=007"' /etc/usbmount/usbmount.conf +#sed -i '/FILESYSTEMS=/c\FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat"' /etc/usbmount/usbmount.conf + +cat <<'EOF' >> /etc/usbmount/usbmount.conf +FS_MOUNTOPTIONS="-fstype=ntfs-3g,nls=utf8,umask=007,gid=46 -fstype=fuseblk,nls=utf8,umask=007,gid=46 -fstype=vfat,gid=1000,uid=1000,umask=007" +FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat" +EOF + +cat <<'EOF' > /etc/udev/rules.d/usbmount.rules +KERNEL=="sd*", DRIVERS=="sbp2", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="sd*", SUBSYSTEMS=="usb", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="ub*", SUBSYSTEMS=="usb", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="sd*", ACTION=="remove", RUN+="/usr/share/usbmount/usbmount remove" +KERNEL=="ub*", ACTION=="remove", RUN+="/usr/share/usbmount/usbmount remove" +EOF + +cat <<'EOF' > /etc/systemd/system/usbmount@.service +[Unit] +BindTo=%i.device +After=%i.device + +[Service] +Type=oneshot +TimeoutStartSec=0 +Environment=DEVNAME=%I +ExecStart=/usr/share/usbmount/usbmount add +RemainAfterExit=yes +EOF + +echo "Adding hacking tools..." +apt-get -y install vim htop screen nmap +cp extras/.vimrc /root/.vimrc +cp extras/.vimrc /home/pi/.vimrc + +echo "Installing youtube upload mod..." +pip3 install pyshorteners +pip3 install google-api-python-client==1.7.3 oauth2client==4.1.2 progressbar2==3.38.0 httplib2==0.15.0 + +cd mods +./install-youtube-upload.sh +cd .. + +echo "Setting up network configuration to use wicd program..." +echo "it works nicer from the terminal than raspberry pi default" +apt-get -y purge dhcpcd5 plymouth +apt-get -y install wicd wicd-curses + +echo "Removing unnecessary programs from startup..." +systemctl disable lightdm.service --force +systemctl disable graphical.target --force +systemctl disable plymouth.service --force +systemctl disable bluetooth.service +systemctl disable hciuart.service + +echo "Configure wifi region settings to FI, finland" +echo "You can change settings in extras/wifiset.sh file" +cp extras/wifiset.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable wifiset.service + +echo "HURRAY! WE ARE" +cat <<'EOF' + _____ ____ _ _ ______ _ + | __ \ / __ \| \ | | ____| | + | | | | | | | \| | |__ | | + | | | | | | | . ` | __| | | + | |__| | |__| | |\ | |____|_| + |_____/ \____/|_| \_|______(_) + +EOF +sleep 2 +echo "Rebooting into up-to-date Tarina..." +sleep 2 +reboot diff --git a/lenses/cs6mm b/lenses/cs6mm @@ -0,0 +1,133 @@ +uint8_t ls_grid[] = { +//R - Ch 3 +44, 43, 42, 42, 41, 40, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, +44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, +43, 42, 42, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 37, +43, 42, 41, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 36, 36, +42, 42, 41, 40, 39, 39, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, +42, 41, 41, 40, 39, 39, 38, 38, 38, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, +42, 41, 40, 39, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, +42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, +41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, +41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, +41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +41, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +41, 40, 39, 39, 38, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +41, 41, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +41, 41, 40, 39, 39, 38, 38, 38, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, +41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, +41, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 33, 33, 33, 33, 33, 34, 34, +42, 41, 41, 40, 39, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, +42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, +43, 42, 41, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, +43, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 36, 36, +44, 43, 42, 41, 41, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, +44, 43, 42, 42, 41, 41, 41, 41, 39, 38, 38, 37, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, +45, 44, 43, 42, 41, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, +45, 44, 43, 43, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 37, 37, 38, +46, 45, 44, 44, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 36, 36, 35, 35, 36, 35, 35, 35, 36, 36, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 38, 38, 38, 38, +//Gr - Ch 2 +52, 51, 50, 49, 48, 47, 46, 45, 45, 44, 43, 43, 43, 42, 42, 41, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 44, 45, 45, 46, 46, +52, 50, 49, 48, 47, 47, 46, 45, 44, 44, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 43, 43, 44, 45, 45, 46, 46, +51, 50, 49, 48, 47, 46, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 39, 38, 38, 39, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 44, 44, 45, 46, 45, +50, 49, 49, 47, 46, 46, 46, 45, 44, 42, 42, 41, 40, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 43, 44, 44, 45, 45, +50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 41, 42, 43, 44, 44, 45, 45, +50, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 38, 39, 39, 39, 40, 41, 42, 43, 43, 44, 45, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 35, 35, 35, 35, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 43, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 43, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 34, 33, 33, 34, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 39, 41, 41, 42, 43, 43, 44, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 43, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 34, 33, 33, 32, 32, 32, 33, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 44, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 43, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 43, 42, 41, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 43, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 43, 43, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 33, 32, 33, 33, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 43, +49, 48, 47, 46, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 42, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44, 43, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, +50, 49, 47, 46, 45, 44, 43, 42, 42, 41, 40, 39, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 42, 43, 44, 44, 44, +50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 41, 40, 39, 39, 39, 38, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45, +51, 50, 48, 47, 47, 46, 44, 44, 43, 42, 41, 41, 40, 40, 39, 39, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 42, 43, 43, 44, 45, 45, +51, 50, 49, 48, 47, 46, 46, 44, 43, 43, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 41, 42, 42, 43, 44, 45, 45, 46, +52, 51, 50, 49, 47, 48, 48, 46, 44, 43, 43, 42, 41, 41, 40, 40, 39, 39, 39, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 42, 43, 44, 44, 45, 46, 46, +53, 51, 50, 49, 48, 48, 48, 47, 45, 44, 43, 43, 42, 41, 41, 41, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 43, 44, 44, 45, 46, 46, 46, +53, 52, 51, 49, 48, 48, 48, 46, 45, 45, 44, 43, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 44, 44, 45, 46, 46, 47, 46, +53, 52, 51, 50, 49, 48, 47, 47, 46, 45, 45, 44, 43, 43, 43, 42, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 44, 44, 45, 46, 47, 47, 47, 47, +53, 52, 51, 51, 50, 49, 48, 48, 47, 47, 46, 46, 45, 44, 44, 44, 44, 43, 43, 43, 42, 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 45, 45, 45, 45, 46, 47, 48, 48, 49, 47, +//Gb - Ch 1 +52, 51, 50, 49, 48, 47, 46, 45, 44, 44, 43, 42, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 42, 42, 43, 43, 43, 44, 45, 46, 46, 46, +52, 50, 49, 48, 47, 46, 46, 45, 44, 43, 43, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 43, 44, 45, 46, 46, 46, +51, 50, 49, 48, 47, 46, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 44, 45, 45, 46, 46, +51, 50, 48, 47, 46, 46, 46, 45, 44, 42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 44, 44, 45, 45, 46, +50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 42, 42, 43, 44, 44, 45, 45, +50, 49, 48, 46, 46, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 39, 39, 40, 40, 41, 42, 43, 43, 44, 45, 45, +50, 48, 47, 46, 45, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 45, +50, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 41, 42, 42, 43, 44, 44, 45, +49, 48, 47, 45, 44, 43, 42, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, 44, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42, 43, 44, 45, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 44, 43, 42, 40, 40, 39, 38, 37, 36, 35, 35, 34, 34, 34, 33, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 43, 42, 41, 41, 40, 39, 38, 37, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 44, 42, 41, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 33, 33, 33, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 34, 33, 33, 33, 32, 33, 33, 33, 33, 34, 34, 35, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, 45, +50, 49, 47, 46, 45, 44, 43, 42, 42, 41, 40, 39, 38, 38, 38, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 39, 39, 40, 40, 41, 42, 43, 44, 45, 45, +50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 38, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 45, +50, 49, 48, 47, 46, 45, 44, 44, 43, 42, 41, 40, 39, 39, 39, 39, 37, 37, 37, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45, 45, +51, 50, 49, 48, 47, 46, 46, 44, 43, 42, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 40, 41, 41, 42, 43, 43, 44, 45, 46, 46, +52, 51, 49, 48, 47, 47, 47, 45, 44, 43, 42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, +52, 51, 50, 49, 48, 48, 48, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 45, 46, 46, 47, +53, 52, 50, 49, 48, 47, 47, 46, 45, 44, 43, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 43, 43, 44, 44, 45, 46, 46, 47, 47, +53, 51, 50, 49, 48, 47, 47, 46, 46, 45, 44, 43, 43, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 43, 43, 43, 44, 44, 45, 46, 47, 47, 47, 48, +53, 52, 52, 50, 49, 48, 48, 47, 46, 46, 45, 44, 45, 44, 43, 43, 43, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 44, 44, 44, 45, 45, 46, 47, 48, 48, 49, 48, +//B - Ch 0 +45, 44, 43, 42, 42, 41, 40, 39, 39, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 35, 34, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, +45, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, +45, 44, 43, 42, 41, 40, 40, 40, 38, 38, 37, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 39, +44, 43, 43, 42, 41, 40, 40, 40, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, 38, +44, 43, 42, 41, 41, 40, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, +44, 43, 42, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 37, 37, 38, +43, 43, 42, 41, 40, 39, 39, 38, 38, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 37, 36, +43, 42, 41, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, +43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, +42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, +42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 36, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 33, 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, +43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 35, +43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 35, +43, 42, 42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, +44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, +44, 43, 42, 41, 41, 40, 40, 39, 39, 38, 38, 37, 36, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 36, +44, 43, 43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, +45, 44, 43, 42, 41, 41, 41, 40, 39, 39, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, +45, 44, 43, 43, 42, 42, 42, 41, 39, 39, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 34, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, +46, 44, 44, 43, 42, 42, 42, 41, 40, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 38, 38, 38, 39, +46, 45, 44, 43, 42, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 39, +46, 45, 44, 43, 43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 39, 40, 40, +48, 45, 46, 44, 44, 42, 42, 41, 41, 41, 40, 40, 39, 39, 38, 37, 37, 37, 36, 37, 37, 37, 36, 37, 36, 37, 37, 37, 37, 37, 38, 37, 38, 38, 38, 39, 40, 41, 41, 41, 40, +}; +uint32_t ref_transform = 3; +uint32_t grid_width = 31; +uint32_t grid_height = 41; diff --git a/mods/collab-edit.sh b/mods/collab-edit.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION EDIT +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync --rsh='/usr/bin/ssh -p 18888' -avr -P /home/pi/Videos/$1 tarina@tarina.org:/home/tarina/Videos --delete diff --git a/mods/collab-pull.sh b/mods/collab-pull.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION PULL +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync -e '/usr/bin/ssh -p 18888' -avr -P tarina@tarina.org:/home/tarina/Videos/$1 /home/pi/Videos diff --git a/mods/collab-push.sh b/mods/collab-push.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION PUSH +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync -e "/usr/bin/ssh -p 18888" -avr -P /home/pi/Videos/$1 tarina@tarina.org:/home/tarina/Videos diff --git a/mods/install-youtube-upload.sh b/mods/install-youtube-upload.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Install script for youtube upload mod +# https://github.com/tokland/youtube-upload + +ROOT_UID=0 # Root has $UID 0. + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + exit 0 +fi + +echo "INSTALLING AND ENABLING MOD: youtube-upload" + +sudo pip3 install --upgrade google-api-python-client oauth2client progressbar2 + +echo "youtube-upload" >> mods-enabled diff --git a/mods/tarina-upload.sh b/mods/tarina-upload.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA.ORG MOD +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/scp -P 18888 $2 tarina@tarina.org:/home/tarina/videos/$1.mp4 diff --git a/mods/upload-mods-enabled b/mods/upload-mods-enabled @@ -0,0 +1,5 @@ +youtube-upload +tarina-upload +collab-pull +collab-push +collab-edit diff --git a/mods/youtube-upload.sh b/mods/youtube-upload.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# YOUTUBE-UPLOAD MOD +# $1 filmtitle +# $2 filename +PATH=`pwd` + +printf "\033c" + +/bin/cat <<'EOF' + + _ _ _ + _ | | | | | | + _ _ ___ _ _| |_ _ _| | _ ____ _ _ ____ | | ___ ____ _ | | +| | | |/ _ \| | | | _) | | | || \ / _ ) | | | | _ \| |/ _ \ / _ |/ || | +| |_| | |_| | |_| | |_| |_| | |_) | (/ / | |_| | | | | | |_| ( ( | ( (_| | + \__ |\___/ \____|\___)____|____/ \____) \____| ||_/|_|\___/ \_||_|\____| +(____/ |_| + + +EOF +read -p "Youtube film title: " title +read -p "Film description: " description + + +while true; do + read -p 'Do you want your video public (y)es (n)o?:' yn + case $yn in + [Yy]* ) privacy='public' ; break ;; + [Nn]* ) privacy='private'; break;; + * ) echo "Please answer yes or no.";; + esac +done + +while true; do + echo "Do you want to upload video $title with description $description to a $privacy Youtube video?" + read -p 'Is this correct (y)es (n)o?:' yn + case $yn in + [Yy]* ) echo "OK!" ; break ;; + [Nn]* ) echo "NOPE!" ; exit;; + * ) echo "Please answer yes or no.";; + esac +done + +/usr/bin/python3 $PATH/mods/youtube-upload/youtube_upload/__main__.py --title="$title" --description="$description" --privacy="$privacy" $2 diff --git a/mods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md b/mods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +I am not working on youtube-upload anymore. However, if you prepare a PR, I'll gladly merge it. Please write me (@tokland) if you want to become the maintainer of youtube-upload. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Desktop (please complete the following information):** + - OS: [e.g. GNU/Linux] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/mods/youtube-upload/.gitignore b/mods/youtube-upload/.gitignore @@ -0,0 +1,59 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ +\ No newline at end of file diff --git a/mods/youtube-upload/Dockerfile b/mods/youtube-upload/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7-alpine3.8 + +ENV workdir /data +WORKDIR ${workdir} + +RUN mkdir -p ${workdir} && adduser python --disabled-password +COPY . ${workdir} +WORKDIR ${workdir} +RUN pip install --upgrade google-api-python-client oauth2client progressbar2 && \ + python setup.py install + +USER python + +ENTRYPOINT ["youtube-upload"] diff --git a/mods/youtube-upload/README.md b/mods/youtube-upload/README.md @@ -0,0 +1,161 @@ +**THIS PROJECT NEEDS A MAINTAINER**. If someone is willing to take over the project, let me know (pyarnau AT gmail.com). + +Introduction +============ + +Command-line script to upload videos to Youtube using theYoutube [APIv3](https://developers.google.com/youtube/v3/). It should work on any platform (GNU/Linux, BSD, OS X, Windows, ...) that runs Python. + +Dependencies +============ + + * [Python 2.6/2.7/3.x](http://www.python.org). + * Packages: [google-api-python-client](https://developers.google.com/api-client-library/python), [progressbar2](https://pypi.python.org/pypi/progressbar2) (optional). + +Check if your operating system provides those packages (check also those [deb/rpm/mac files](https://github.com/qiuwei/youtube-upload/releases)), otherwise install them with `pip`: + +``` +$ sudo pip install --upgrade google-api-python-client oauth2client progressbar2 +``` + +Install +======= + +``` +$ wget https://github.com/tokland/youtube-upload/archive/master.zip +$ unzip master.zip +$ cd youtube-upload-master +$ sudo python setup.py install +``` + +Or run directly from sources: + +``` +$ cd youtube-upload-master +$ PYTHONPATH=. python bin/youtube-upload ... +``` + +Setup +===== + +You'll see that there is no email/password options. Instead, the Youtube API uses [OAuth 2.0](https://developers.google.com/accounts/docs/OAuth2) to authenticate the upload. The first time you try to upload a video, you will be asked to follow a URL in your browser to get an authentication token. If you have multiple channels for the logged in user, you will also be asked to pick which one you want to upload the videos to. You can use multiple credentials, just use the option ```--credentials-file```. Also, check the [token expiration](https://developers.google.com/youtube/v3/) policies. + +The package used to include a default ```client_secrets.json``` file. It does not work anymore, Google has revoked it. So you now must [create and use your own OAuth 2.0 file](https://developers.google.com/youtube/registering_an_application), it's a free service. Steps: + +* Go to the Google [console](https://console.developers.google.com/). +* _Create project_. +* Side menu: _APIs & auth_ -> _APIs_ +* Top menu: _Enabled API(s)_: Enable all Youtube APIs. +* Side menu: _APIs & auth_ -> _Credentials_. +* _Create a Client ID_: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: youtube-upload -> Create -> OK +* _Download JSON_: Under the section "OAuth 2.0 client IDs". Save the file to your local system. +* Use this JSON as your credentials file: `--client-secrets=CLIENT_SECRETS` or copy it to `~/client_secrets.json`. + +*Note: ```client_secrets.json``` is a file you can download from the developer console, the credentials file is something auto generated after the first time the script is run and the google account sign in is followed, the file is stored at ```~/.youtube-upload-credentials.json```.* + +Examples +======== + +* Upload a video (a valid `~/.client_secrets.json` should exist, check the Setup section): + +``` +$ youtube-upload --title="A.S. Mutter" anne_sophie_mutter.flv +pxzZ-fYjeYs +``` + +* Upload a video with extra metadata, with your own client secrets and credentials file, and to a playlist (if not found, it will be created): + +``` +$ youtube-upload \ + --title="A.S. Mutter" " \ + --description="A.S. Mutter plays Beethoven" \ + --category="Music" \ + --tags="mutter, beethoven" \ + --recording-date="2011-03-10T15:32:17.0Z" \ + --default-language="en" \ + --default-audio-language="en" \ + --client-secrets="my_client_secrets.json" \ + --credentials-file="my_credentials.json" \ + --playlist="My favorite music" \ + --embeddable=True|False \ + anne_sophie_mutter.flv +tx2Zb-145Yz +``` +*Other extra medata available :* + ``` + --privacy (public | unlisted | private) + --publish-at (YYYY-MM-DDThh:mm:ss.sZ) + --location (latitude=VAL,longitude=VAL[,altitude=VAL]) + --thumbnail (string) + ``` + +* Upload a video using a browser GUI to authenticate: + +``` +$ youtube-upload --title="A.S. Mutter" --auth-browser anne_sophie_mutter.flv +``` + +* Split a video with _ffmpeg_ + +If your video is too big or too long for Youtube limits, split it before uploading: + +``` +$ bash examples/split_video_for_youtube.sh video.avi +video.part1.avi +video.part2.avi +video.part3.avi +``` +* Use a HTTP proxy + +Set environment variables *http_proxy* and *https_proxy*: + +``` +$ export http_proxy=http://user:password@host:port +$ export https_proxy=$http_proxy +$ youtube-upload .... +``` + +Get available categories +======================== + +* Go to the [API Explorer](https://developers.google.com/apis-explorer) +- Search "youtube categories" -> *youtube.videoCategories.list* +- This bring you to [youtube.videoCategories.list service](https://developers.google.com/apis-explorer/#search/youtube%20categories/m/youtube/v3/youtube.videoCategories.list) +- part: `id,snippet` +- regionCode: `es` (2 letter code of your country) +- _Authorize and execute_ + +And see the JSON response below. Note that categories with the attribute `assignable` equal to `false` cannot be used. + +Using [shoogle](https://github.com/tokland/shoogle): + +``` +$ shoogle execute --client-secret-file client_secret.json \ + youtube:v3.videoCategories.list <(echo '{"part": "id,snippet", "regionCode": "es"}') | + jq ".items[] | select(.snippet.assignable) | {id: .id, title: .snippet.title}" +``` + +Notes for developers +==================== + +* Main logic of the upload: [main.py](youtube_upload/main.py) (function ```upload_video```). +* Check the [Youtube Data API](https://developers.google.com/youtube/v3/docs/). +* Some Youtube API [examples](https://github.com/youtube/api-samples/tree/master/python) provided by Google. + +Alternatives +============ + +* [shoogle](https://github.com/tokland/shoogle) can send requests to any Google API service, so it can be used not only to upload videos, but also to perform any operation regarding the Youtube API. + +* [youtubeuploader](https://github.com/porjo/youtubeuploader) uploads videos to Youtube from local disk or from the web. It also provides rate-limited uploads. + +More +==== + +* License: [GNU/GPLv3](http://www.gnu.org/licenses/gpl.html). + +Feedback +======== + +* [Donations](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=pyarnau%40gmail%2ecom&lc=US&item_name=youtube%2dupload&no_note=0&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHostedGuest). +* If you find a bug, [open an issue](https://github.com/tokland/youtube-upload/issues). +* If you want a new feature to be added, you'll have to send a pull request (or find a programmer to do it for you), currently I am not adding new features. diff --git a/mods/youtube-upload/bin/youtube-upload b/mods/youtube-upload/bin/youtube-upload @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + + #Allows you to a relative import from the parent folder + import os.path, sys + sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) + + from youtube_upload import main + main.run() diff --git a/mods/youtube-upload/bin/youtube-upload.bat b/mods/youtube-upload/bin/youtube-upload.bat @@ -0,0 +1 @@ +python %~dp0youtube-upload %* diff --git a/mods/youtube-upload/examples/split_video_for_youtube.sh b/mods/youtube-upload/examples/split_video_for_youtube.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Split a video file (to MKV format) suitable for standard users in Youtube (<15') +# +# $ bash split_video_for_youtube.sh video.avi +# video.part1.mkv +# video.part2.mkv +# +# $ youtube-upload [OPTIONS] video.part*.mkv +# + +# Echo to standard error +debug() { + echo "$@" >&2 +} + +# Returns duration (in seconds) of a video $1 (uses ffmpeg). +get_video_duration() { + OUTPUT=$(ffmpeg -i "$1" -vframes 1 -f rawvideo -y /dev/null 2>&1) || + { debug -e "get_video_duration: error running ffmpeg:\n$OUTPUT"; return 1; } + DURATION=$(echo "$OUTPUT" | grep -m1 "^[[:space:]]*Duration:" | + cut -d":" -f2- | cut -d"," -f1 | sed "s/[:\.]/ /g") || + { debug -e "get_video_duration: error parsing duration:\n$OUTPUT"; return 1; } + read HOURS MINUTES SECONDS DECISECONDS <<< "$DURATION" + echo $((10#$HOURS * 3600 + 10#$MINUTES * 60 + 10#$SECONDS)) +} + +main() { + set -e -u -o pipefail + if test $# -eq 0; then + debug "Usage: $(basename $0) VIDEO [EXTRA_OPTIONS_FOR_FFMPEG]" + exit 1 + fi + CHUNK_DURATION=$((60*15)) + VIDEO=$1 + shift 1 + + DURATION=$(get_video_duration "$VIDEO") + if test $DURATION -le $CHUNK_DURATION; then + debug "no need to split, duration of video: $DURATION <= $CHUNK_DURATION" + echo "$VIDEO" + exit 0 + fi + + EXTENSION=${VIDEO##*.} + BASENAME=$(basename "$VIDEO" ".$EXTENSION") + debug "start split: $VIDEO ($DURATION seconds)" + seq 0 $CHUNK_DURATION $DURATION | cat -n | while read INDEX OFFSET; do + debug "$VIDEO: from position $OFFSET take $CHUNK_DURATION seconds" + PADDED_INDEX=$(printf '%03d' $INDEX) + OUTPUT_FILE="${BASENAME}.part${PADDED_INDEX}.mkv" + ffmpeg -i "$VIDEO" -vcodec copy -acodec copy "$@" \ + -ss $OFFSET -t $CHUNK_DURATION -y "$OUTPUT_FILE" </dev/null + echo "$OUTPUT_FILE" + done +} + +test "$NOEXEC" = 1 || main "$@" diff --git a/mods/youtube-upload/setup.py b/mods/youtube-upload/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +"""Upload videos to Youtube.""" +from distutils.core import setup + +setup_kwargs = { + "name": "youtube-upload", + "version": "0.8.0", + "description": "Upload videos to Youtube", + "author": "Arnau Sanchez", + "author_email": "pyarnau@gmail.com", + "url": "https://github.com/tokland/youtube-upload", + "packages": ["youtube_upload/", "youtube_upload/auth"], + "scripts": ["bin/youtube-upload"], + "license": "GNU Public License v3.0", + "long_description": " ".join(__doc__.strip().splitlines()), + "classifiers": [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + ], + "entry_points": { + 'console_scripts': [ + 'youtube-upload = youtube_upload.main:run' + ], + }, + "install_requires":[ + 'google-api-python-client', + 'progressbar2' + ] +} + +setup(**setup_kwargs) diff --git a/mods/youtube-upload/youtube_upload/__init__.py b/mods/youtube-upload/youtube_upload/__init__.py @@ -0,0 +1 @@ +VERSION = "0.8.0" diff --git a/mods/youtube-upload/youtube_upload/__main__.py b/mods/youtube-upload/youtube_upload/__main__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +from __future__ import unicode_literals + +# Execute with +# $ python youtube_upload/__main__.py (2.6+) +# $ python -m youtube_upload (2.7+) + +import sys + +if __package__ is None and not hasattr(sys, 'frozen'): + # direct call of __main__.py + import os.path + path = os.path.realpath(os.path.abspath(__file__)) + sys.path.insert(0, os.path.dirname(os.path.dirname(path))) + +import youtube_upload.main + +if __name__ == '__main__': + youtube_upload.main.main(sys.argv[1:]) diff --git a/mods/youtube-upload/youtube_upload/auth/__init__.py b/mods/youtube-upload/youtube_upload/auth/__init__.py @@ -0,0 +1,42 @@ +"""Wrapper for Google OAuth2 API.""" +import sys +import json + +import googleapiclient.discovery +import oauth2client +import httplib2 + +from youtube_upload import lib +from youtube_upload.auth import console +from youtube_upload.auth import browser + +YOUTUBE_UPLOAD_SCOPE = ["https://www.googleapis.com/auth/youtube.upload", "https://www.googleapis.com/auth/youtube"] + +def _get_credentials_interactively(flow, storage, get_code_callback): + """Return the credentials asking the user.""" + flow.redirect_uri = oauth2client.client.OOB_CALLBACK_URN + authorize_url = flow.step1_get_authorize_url() + code = get_code_callback(authorize_url) + if code: + credential = flow.step2_exchange(code, http=None) + storage.put(credential) + credential.set_store(storage) + return credential + +def _get_credentials(flow, storage, get_code_callback): + """Return the user credentials. If not found, run the interactive flow.""" + existing_credentials = storage.get() + if existing_credentials and not existing_credentials.invalid: + return existing_credentials + else: + return _get_credentials_interactively(flow, storage, get_code_callback) + +def get_resource(client_secrets_file, credentials_file, get_code_callback): + """Authenticate and return a googleapiclient.discovery.Resource object.""" + get_flow = oauth2client.client.flow_from_clientsecrets + flow = get_flow(client_secrets_file, scope=YOUTUBE_UPLOAD_SCOPE) + storage = oauth2client.file.Storage(credentials_file) + credentials = _get_credentials(flow, storage, get_code_callback) + if credentials: + http = credentials.authorize(httplib2.Http()) + return googleapiclient.discovery.build("youtube", "v3", http=http) diff --git a/mods/youtube-upload/youtube_upload/auth/browser.py b/mods/youtube-upload/youtube_upload/auth/browser.py @@ -0,0 +1,19 @@ +from .. import lib + +try: + from youtube_upload.auth import webkit_qt as backend + WEBKIT_BACKEND = "qt" +except ImportError: + try: + from youtube_upload.auth import webkit_gtk as backend + WEBKIT_BACKEND = "gtk" + except ImportError: + WEBKIT_BACKEND = None + +def get_code(url, size=(640, 480), title="Google authentication"): + if WEBKIT_BACKEND: + lib.debug("Using webkit backend: " + WEBKIT_BACKEND) + with lib.default_sigint(): + return backend.get_code(url, size=size, title=title) + else: + raise NotImplementedError("GUI auth requires pywebkitgtk or qtwebkit") diff --git a/mods/youtube-upload/youtube_upload/auth/console.py b/mods/youtube-upload/youtube_upload/auth/console.py @@ -0,0 +1,23 @@ +import sys +import pyshorteners + +def shorten_url(url): + s = pyshorteners.Shortener() + short_url = s.tinyurl.short(url) + return short_url + +def get_code(authorize_url): + sys.stderr.write("\x1b[2J\x1b[H") + short_url = shorten_url(authorize_url) + """Show authorization URL and return the code the user wrote.""" + message = "Check this link in your browser: " + short_url + sys.stderr.write("\n") + sys.stderr.write("\n") + sys.stderr.write("Youtube authentication required!\n") + sys.stderr.write(message + "\n") + try: input = raw_input #For Python2 compatability + except NameError: + #For Python3 on Windows compatability + try: from builtins import input as input + except ImportError: pass + return input("Enter verification code: ") diff --git a/mods/youtube-upload/youtube_upload/auth/webkit_gtk.py b/mods/youtube-upload/youtube_upload/auth/webkit_gtk.py @@ -0,0 +1,48 @@ +import json + +CHECK_AUTH_JS = """ + var code = document.getElementById("code"); + var access_denied = document.getElementById("access_denied"); + var result; + + if (code) { + result = {authorized: true, code: code.value}; + } else if (access_denied) { + result = {authorized: false, message: access_denied.innerText}; + } else { + result = {}; + } + window.status = JSON.stringify(result); +""" + +def _on_webview_status_bar_changed(webview, status, dialog): + if status: + authorization = json.loads(status) + if authorization.has_key("authorized"): + dialog.set_data("authorization_code", authorization["code"]) + dialog.response(0) + +def get_code(url, size=(640, 480), title="Google authentication"): + """Open a GTK webkit window and return the access code.""" + import gtk + import webkit + dialog = gtk.Dialog(title=title) + webview = webkit.WebView() + scrolled = gtk.ScrolledWindow() + scrolled.add(webview) + dialog.get_children()[0].add(scrolled) + webview.load_uri(url) + dialog.resize(*size) + dialog.show_all() + dialog.connect("delete-event", + lambda event, data: dialog.response(1)) + webview.connect("load-finished", + lambda view, frame: view.execute_script(CHECK_AUTH_JS)) + webview.connect("status-bar-text-changed", + _on_webview_status_bar_changed, dialog) + dialog.set_data("authorization_code", None) + status = dialog.run() + dialog.destroy() + while gtk.events_pending(): + gtk.main_iteration(False) + return dialog.get_data("authorization_code") diff --git a/mods/youtube-upload/youtube_upload/auth/webkit_qt.py b/mods/youtube-upload/youtube_upload/auth/webkit_qt.py @@ -0,0 +1,54 @@ +CHECK_AUTH_JS = """ + var code = document.getElementById("code"); + var access_denied = document.getElementById("access_denied"); + var result; + + if (code) { + result = {authorized: true, code: code.value}; + } else if (access_denied) { + result = {authorized: false, message: access_denied.innerText}; + } else { + result = {}; + } + result; +""" + +def _on_qt_page_load_finished(dialog, webview): + to_s = lambda x: (str(x.toUtf8()) if hasattr(x,'toUtf8') else x) + frame = webview.page().currentFrame() + try: #PySide does not QStrings + from QtCore import QString + jscode = QString(CHECK_AUTH_JS) + except ImportError: + jscode = CHECK_AUTH_JS + res = frame.evaluateJavaScript(jscode) + try: + authorization = dict((to_s(k), to_s(v)) for (k, v) in res.toPyObject().items()) + except AttributeError: #PySide returns the result in pure Python + authorization = dict((to_s(k), to_s(v)) for (k, v) in res.items()) + if "authorized" in authorization: + dialog.authorization_code = authorization.get("code") + dialog.close() + +def get_code(url, size=(640, 480), title="Google authentication"): + """Open a QT webkit window and return the access code.""" + try: + from PyQt4 import QtCore, QtGui, QtWebKit + except ImportError: + from PySide import QtCore, QtGui, QtWebKit + app = QtGui.QApplication([]) + dialog = QtGui.QDialog() + dialog.setWindowTitle(title) + dialog.resize(*size) + webview = QtWebKit.QWebView() + webpage = QtWebKit.QWebPage() + webview.setPage(webpage) + webpage.loadFinished.connect(lambda: _on_qt_page_load_finished(dialog, webview)) + webview.setUrl(QtCore.QUrl.fromEncoded(url)) + layout = QtGui.QGridLayout() + layout.addWidget(webview) + dialog.setLayout(layout) + dialog.authorization_code = None + dialog.show() + app.exec_() + return dialog.authorization_code diff --git a/mods/youtube-upload/youtube_upload/categories.py b/mods/youtube-upload/youtube_upload/categories.py @@ -0,0 +1,51 @@ +try: + #import urllib2 + from urllib2 import urlopen + import urllib +except ImportError: + from urllib.request import urlopen +import json + +URL = "https://www.googleapis.com/youtube/v3/videoCategories" + +IDS = { + "Film & Animation": 1, + "Autos & Vehicles": 2, + "Music": 10, + "Pets & Animals": 15, + "Sports": 17, + "Short Movies": 18, + "Travel & Events": 19, + "Gaming": 20, + "Videoblogging": 21, + "People & Blogs": 22, + "Comedy": 23, + "Entertainment": 24, + "News & Politics": 25, + "Howto & Style": 26, + "Education": 27, + "Science & Technology": 28, + "Nonprofits & Activism": 29, + "Movies": 30, + "Anime/Animation": 31, + "Action/Adventure": 32, + "Classics": 33, + "Documentary": 35, + "Drama": 36, + "Family": 37, + "Foreign": 38, + "Horror": 39, + "Sci-Fi/Fantasy": 40, + "Thriller": 41, + "Shorts": 42, + "Shows": 43, + "Trailers": 44, +} + +def get(region_code="us", api_key=None): + params = dict(part="snippet", regionCode=region_code, key=api_key) + full_url = URL + "?" + urllib.urlencode(params) + response = urlopen(full_url) + categories_info = json.loads(response.read()) + items = categories_info["items"] + return dict((item["snippet"]["title"], item["id"]) for item in items) diff --git a/mods/youtube-upload/youtube_upload/lib.py b/mods/youtube-upload/youtube_upload/lib.py @@ -0,0 +1,94 @@ +from __future__ import print_function +import os +import sys +import locale +import random +import time +import signal +from contextlib import contextmanager + +import googleapiclient.errors + +@contextmanager +def default_sigint(): + original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, signal.SIG_DFL) + try: + yield + finally: + signal.signal(signal.SIGINT, original_sigint_handler) + +def get_encoding(): + return locale.getpreferredencoding() + +def to_utf8(s): + """Re-encode string from the default system encoding to UTF-8.""" + current = locale.getpreferredencoding() + if hasattr(s, 'decode'): #Python 3 workaround + return (s.decode(current).encode("UTF-8") if s and current != "UTF-8" else s) + elif isinstance(s, bytes): + return bytes.decode(s) + else: + return s + +def debug(obj, fd=sys.stderr): + """Write obj to standard error.""" + print(obj, file=fd) + +def catch_exceptions(exit_codes, fun, *args, **kwargs): + """ + Catch exceptions on fun(*args, **kwargs) and return the exit code specified + in the exit_codes dictionary. Return 0 if no exception is raised. + """ + try: + fun(*args, **kwargs) + return 0 + except tuple(exit_codes.keys()) as exc: + debug("[{0}] {1}".format(exc.__class__.__name__, exc)) + return exit_codes[exc.__class__] + +def first(it): + """Return first element in iterable.""" + return it.next() + +def string_to_dict(string): + """Return dictionary from string "key1=value1, key2=value2".""" + if string: + pairs = [s.strip() for s in string.split(",")] + return dict(pair.split("=") for pair in pairs) + +def get_first_existing_filename(prefixes, relative_path): + """Get the first existing filename of relative_path seeking on prefixes directories.""" + for prefix in prefixes: + path = os.path.join(prefix, relative_path) + if os.path.exists(path): + return path + +def retriable_exceptions(fun, retriable_exceptions, max_retries=None): + """Run function and retry on some exceptions (with exponential backoff).""" + retry = 0 + while 1: + try: + return fun() + except tuple(retriable_exceptions) as exc: + retry += 1 + if type(exc) not in retriable_exceptions: + raise exc + # we want to retry 5xx errors only + elif type(exc) == googleapiclient.errors.HttpError and exc.resp.status < 500: + raise exc + elif max_retries is not None and retry > max_retries: + debug("[Retryable errors] Retry limit reached") + raise exc + else: + seconds = random.uniform(0, 2**retry) + message = ("[Retryable error {current_retry}/{total_retries}] " + + "{error_type} ({error_msg}). Wait {wait_time} seconds").format( + current_retry=retry, + total_retries=max_retries or "-", + error_type=type(exc).__name__, + error_msg=str(exc) or "-", + wait_time="%.1f" % seconds, + ) + debug(message) + time.sleep(seconds) diff --git a/mods/youtube-upload/youtube_upload/main.py b/mods/youtube-upload/youtube_upload/main.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +# +# Upload videos to Youtube from the command-line using APIv3. +# +# Author: Arnau Sanchez <pyarnau@gmail.com> +# Project: https://github.com/tokland/youtube-upload +""" +Upload a video to Youtube from the command-line. + + $ youtube-upload --title="A.S. Mutter playing" \ + --description="Anne Sophie Mutter plays Beethoven" \ + --category=Music \ + --tags="mutter, beethoven" \ + anne_sophie_mutter.flv + pxzZ-fYjeYs +""" + +import os +import sys +import optparse +import collections +import webbrowser +from io import open + +import googleapiclient.errors +import oauth2client +from oauth2client import file + +from . import auth +from . import upload_video +from . import categories +from . import lib +from . import playlists + +# http://code.google.com/p/python-progressbar (>= 2.3) +try: + import progressbar +except ImportError: + progressbar = None + +class InvalidCategory(Exception): pass +class OptionsError(Exception): pass +class AuthenticationError(Exception): pass +class RequestError(Exception): pass + +EXIT_CODES = { + OptionsError: 2, + InvalidCategory: 3, + RequestError: 3, + AuthenticationError: 4, + oauth2client.client.FlowExchangeError: 4, + NotImplementedError: 5, +} + +WATCH_VIDEO_URL = "https://www.youtube.com/watch?v={id}" + +debug = lib.debug +struct = collections.namedtuple + +def open_link(url): + """Opens a URL link in the client's browser.""" + webbrowser.open(url) + +def get_progress_info(): + """Return a function callback to update the progressbar.""" + progressinfo = struct("ProgressInfo", ["callback", "finish"]) + + if progressbar: + bar = progressbar.ProgressBar(widgets=[ + progressbar.Percentage(), + ' ', progressbar.Bar(), + ' ', progressbar.FileTransferSpeed(), + ' ', progressbar.DataSize(), '/', progressbar.DataSize('max_value'), + ' ', progressbar.Timer(), + ' ', progressbar.AdaptiveETA(), + ]) + def _callback(total_size, completed): + if not hasattr(bar, "next_update"): + if hasattr(bar, "maxval"): + bar.maxval = total_size + else: + bar.max_value = total_size + bar.start() + bar.update(completed) + def _finish(): + if hasattr(bar, "next_update"): + return bar.finish() + return progressinfo(callback=_callback, finish=_finish) + else: + return progressinfo(callback=None, finish=lambda: True) + +def get_category_id(category): + """Return category ID from its name.""" + if category: + if category in categories.IDS: + ncategory = categories.IDS[category] + debug("Using category: {0} (id={1})".format(category, ncategory)) + return str(categories.IDS[category]) + else: + msg = "{0} is not a valid category".format(category) + raise InvalidCategory(msg) + +def upload_youtube_video(youtube, options, video_path, total_videos, index): + """Upload video with index (for split videos).""" + u = lib.to_utf8 + title = u(options.title) + if hasattr(u('string'), 'decode'): + description = u(options.description or "").decode("string-escape") + else: + description = options.description + if options.publish_at: + debug("Your video will remain private until specified date.") + + tags = [u(s.strip()) for s in (options.tags or "").split(",")] + ns = dict(title=title, n=index+1, total=total_videos) + title_template = u(options.title_template) + complete_title = (title_template.format(**ns) if total_videos > 1 else title) + progress = get_progress_info() + category_id = get_category_id(options.category) + request_body = { + "snippet": { + "title": complete_title, + "description": description, + "categoryId": category_id, + "tags": tags, + "defaultLanguage": options.default_language, + "defaultAudioLanguage": options.default_audio_language, + + }, + "status": { + "embeddable": options.embeddable, + "privacyStatus": ("private" if options.publish_at else options.privacy), + "publishAt": options.publish_at, + "license": options.license, + + }, + "recordingDetails": { + "location": lib.string_to_dict(options.location), + "recordingDate": options.recording_date, + }, + } + + debug("Start upload: {0}".format(video_path)) + try: + video_id = upload_video.upload(youtube, video_path, + request_body, progress_callback=progress.callback, + chunksize=options.chunksize) + finally: + progress.finish() + return video_id + +def get_youtube_handler(options): + """Return the API Youtube object.""" + home = os.path.expanduser("~") + default_credentials = os.path.join(home, ".youtube-upload-credentials.json") + client_secrets = options.client_secrets or os.path.join(home, ".client_secrets.json") + credentials = options.credentials_file or default_credentials + debug("Using client secrets: {0}".format(client_secrets)) + debug("Using credentials file: {0}".format(credentials)) + get_code_callback = (auth.browser.get_code + if options.auth_browser else auth.console.get_code) + return auth.get_resource(client_secrets, credentials, + get_code_callback=get_code_callback) + +def parse_options_error(parser, options): + """Check errors in options.""" + required_options = ["title"] + missing = [opt for opt in required_options if not getattr(options, opt)] + if missing: + parser.print_usage() + msg = "Some required option are missing: {0}".format(", ".join(missing)) + raise OptionsError(msg) + +def run_main(parser, options, args, output=sys.stdout): + """Run the main scripts from the parsed options/args.""" + parse_options_error(parser, options) + youtube = get_youtube_handler(options) + + if youtube: + for index, video_path in enumerate(args): + video_id = upload_youtube_video(youtube, options, video_path, len(args), index) + video_url = WATCH_VIDEO_URL.format(id=video_id) + debug("Video URL: {0}".format(video_url)) + if options.open_link: + open_link(video_url) #Opens the Youtube Video's link in a webbrowser + + if options.thumb: + youtube.thumbnails().set(videoId=video_id, media_body=options.thumb).execute() + if options.playlist: + playlists.add_video_to_playlist(youtube, video_id, + title=lib.to_utf8(options.playlist), privacy=options.privacy) + output.write(video_id + "\n") + else: + raise AuthenticationError("Cannot get youtube resource") + +def main(arguments): + """Upload videos to Youtube.""" + usage = """Usage: %prog [OPTIONS] VIDEO [VIDEO2 ...] + + Upload videos to Youtube.""" + parser = optparse.OptionParser(usage) + + # Video metadata + parser.add_option('-t', '--title', dest='title', type="string", + help='Video title') + parser.add_option('-c', '--category', dest='category', type="string", + help='Video category') + parser.add_option('-d', '--description', dest='description', type="string", + help='Video description') + parser.add_option('', '--description-file', dest='description_file', type="string", + help='Video description file', default=None) + parser.add_option('', '--tags', dest='tags', type="string", + help='Video tags (separated by commas: "tag1, tag2,...")') + parser.add_option('', '--privacy', dest='privacy', metavar="STRING", + default="public", help='Privacy status (public | unlisted | private)') + parser.add_option('', '--publish-at', dest='publish_at', metavar="datetime", + default=None, help='Publish date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ') + parser.add_option('', '--license', dest='license', metavar="string", + choices=('youtube', 'creativeCommon'), default='youtube', + help='License for the video, either "youtube" (the default) or "creativeCommon"') + parser.add_option('', '--location', dest='location', type="string", + default=None, metavar="latitude=VAL,longitude=VAL[,altitude=VAL]", + help='Video location"') + parser.add_option('', '--recording-date', dest='recording_date', metavar="datetime", + default=None, help="Recording date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ") + parser.add_option('', '--default-language', dest='default_language', type="string", + default=None, metavar="string", + help="Default language (ISO 639-1: en | fr | de | ...)") + parser.add_option('', '--default-audio-language', dest='default_audio_language', type="string", + default=None, metavar="string", + help="Default audio language (ISO 639-1: en | fr | de | ...)") + parser.add_option('', '--thumbnail', dest='thumb', type="string", metavar="FILE", + help='Image file to use as video thumbnail (JPEG or PNG)') + parser.add_option('', '--playlist', dest='playlist', type="string", + help='Playlist title (if it does not exist, it will be created)') + parser.add_option('', '--title-template', dest='title_template', + type="string", default="{title} [{n}/{total}]", metavar="string", + help='Template for multiple videos (default: {title} [{n}/{total}])') + parser.add_option('', '--embeddable', dest='embeddable', default=True, + help='Video is embeddable') + + # Authentication + parser.add_option('', '--client-secrets', dest='client_secrets', + type="string", help='Client secrets JSON file') + parser.add_option('', '--credentials-file', dest='credentials_file', + type="string", help='Credentials JSON file') + parser.add_option('', '--auth-browser', dest='auth_browser', action='store_true', + help='Open a GUI browser to authenticate if required') + + #Additional options + parser.add_option('', '--chunksize', dest='chunksize', type="int", + default = 1024*1024*8, help='Update file chunksize') + parser.add_option('', '--open-link', dest='open_link', action='store_true', + help='Opens a url in a web browser to display the uploaded video') + + options, args = parser.parse_args(arguments) + + if options.description_file is not None and os.path.exists(options.description_file): + with open(options.description_file, encoding="utf-8") as file: + options.description = file.read() + + try: + run_main(parser, options, args) + except googleapiclient.errors.HttpError as error: + response = bytes.decode(error.content, encoding=lib.get_encoding()).strip() + raise RequestError(u"Server response: {0}".format(response)) + +def run(): + sys.exit(lib.catch_exceptions(EXIT_CODES, main, sys.argv[1:])) + +if __name__ == '__main__': + run() diff --git a/mods/youtube-upload/youtube_upload/playlists.py b/mods/youtube-upload/youtube_upload/playlists.py @@ -0,0 +1,53 @@ +import locale + +from .lib import debug + +def get_playlist(youtube, title): + """Return users's playlist ID by title (None if not found)""" + playlists = youtube.playlists() + request = playlists.list(mine=True, part="id,snippet") + current_encoding = locale.getpreferredencoding() + + while request: + results = request.execute() + for item in results["items"]: + t = item.get("snippet", {}).get("title") + existing_playlist_title = (t.encode(current_encoding) if hasattr(t, 'decode') else t) + if existing_playlist_title == title: + return item.get("id") + request = playlists.list_next(request, results) + +def create_playlist(youtube, title, privacy): + """Create a playlist by title and return its ID""" + debug("Creating playlist: {0}".format(title)) + response = youtube.playlists().insert(part="snippet,status", body={ + "snippet": { + "title": title, + }, + "status": { + "privacyStatus": privacy, + } + }).execute() + return response.get("id") + +def add_video_to_existing_playlist(youtube, playlist_id, video_id): + """Add video to playlist (by identifier) and return the playlist ID.""" + debug("Adding video to playlist: {0}".format(playlist_id)) + return youtube.playlistItems().insert(part="snippet", body={ + "snippet": { + "playlistId": playlist_id, + "resourceId": { + "kind": "youtube#video", + "videoId": video_id, + } + } + }).execute() + +def add_video_to_playlist(youtube, video_id, title, privacy="public"): + """Add video to playlist (by title) and return the full response.""" + playlist_id = get_playlist(youtube, title) or \ + create_playlist(youtube, title, privacy) + if playlist_id: + return add_video_to_existing_playlist(youtube, playlist_id, video_id) + else: + debug("Error adding video to playlist") diff --git a/mods/youtube-upload/youtube_upload/upload_video.py b/mods/youtube-upload/youtube_upload/upload_video.py @@ -0,0 +1,43 @@ +import socket + +try: + import httplib +except ImportError: + import http.client as httplib + +import googleapiclient.errors +import apiclient.http +import httplib2 + +from . import lib + +RETRIABLE_EXCEPTIONS = [ + socket.error, IOError, httplib2.HttpLib2Error, httplib.NotConnected, + httplib.IncompleteRead, httplib.ImproperConnectionState, + httplib.CannotSendRequest, httplib.CannotSendHeader, + httplib.ResponseNotReady, httplib.BadStatusLine, + googleapiclient.errors.HttpError, +] + +def _upload_to_request(request, progress_callback): + """Upload a video to a Youtube request. Return video ID.""" + while 1: + status, response = request.next_chunk() + if status and progress_callback: + progress_callback(status.total_size, status.resumable_progress) + if response: + if "id" in response: + return response['id'] + else: + raise KeyError("Expected field 'id' not found in response") + +def upload(resource, path, body, chunksize=4*1024*1024, + progress_callback=None, max_retries=10): + """Upload video to Youtube. Return video ID.""" + body_keys = ",".join(body.keys()) + media = apiclient.http.MediaFileUpload(path, chunksize=chunksize, + resumable=True, mimetype="application/octet-stream") + request = resource.videos().insert(part=body_keys, body=body, media_body=media) + upload_fun = lambda: _upload_to_request(request, progress_callback) + return lib.retriable_exceptions(upload_fun, + RETRIABLE_EXCEPTIONS, max_retries=max_retries) diff --git a/srv/static/Videos b/srv/static/Videos @@ -0,0 +1 @@ +/home/pi/Videos/ +\ No newline at end of file diff --git a/srv/static/jquery-3.7.1.min.js b/srv/static/jquery-3.7.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}function fe(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}ce.fn=ce.prototype={jquery:t,constructor:ce,length:0,toArray:function(){return ae.call(this)},get:function(e){return null==e?ae.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=ce.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return ce.each(this,e)},map:function(n){return this.pushStack(ce.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(ae.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(ce.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(ce.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:oe.sort,splice:oe.splice},ce.extend=ce.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||v(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(ce.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||ce.isPlainObject(n)?n:{},i=!1,a[t]=ce.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},ce.extend({expando:"jQuery"+(t+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==i.call(e))&&(!(t=r(e))||"function"==typeof(n=ue.call(t,"constructor")&&t.constructor)&&o.call(n)===a)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){m(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(c(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},text:function(e){var t,n="",r=0,i=e.nodeType;if(!i)while(t=e[r++])n+=ce.text(t);return 1===i||11===i?e.textContent:9===i?e.documentElement.textContent:3===i||4===i?e.nodeValue:n},makeArray:function(e,t){var n=t||[];return null!=e&&(c(Object(e))?ce.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:se.call(t,e,n)},isXMLDoc:function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!l.test(t||n&&n.nodeName||"HTML")},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(c(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:le}),"function"==typeof Symbol&&(ce.fn[Symbol.iterator]=oe[Symbol.iterator]),ce.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var pe=oe.pop,de=oe.sort,he=oe.splice,ge="[\\x20\\t\\r\\n\\f]",ve=new RegExp("^"+ge+"+|((?:^|[^\\\\])(?:\\\\.)*)"+ge+"+$","g");ce.contains=function(e,t){var n=t&&t.parentNode;return e===n||!(!n||1!==n.nodeType||!(e.contains?e.contains(n):e.compareDocumentPosition&&16&e.compareDocumentPosition(n)))};var f=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function p(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}ce.escapeSelector=function(e){return(e+"").replace(f,p)};var ye=C,me=s;!function(){var e,b,w,o,a,T,r,C,d,i,k=me,S=ce.expando,E=0,n=0,s=W(),c=W(),u=W(),h=W(),l=function(e,t){return e===t&&(a=!0),0},f="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",t="(?:\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",p="\\["+ge+"*("+t+")(?:"+ge+"*([*^$|!~]?=)"+ge+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+t+"))|)"+ge+"*\\]",g=":("+t+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+p+")*)|.*)\\)|)",v=new RegExp(ge+"+","g"),y=new RegExp("^"+ge+"*,"+ge+"*"),m=new RegExp("^"+ge+"*([>+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="<a id='"+S+"' href='' disabled='disabled'></a><select id='"+S+"-\r\\' disabled='disabled'><option selected=''></option></select>",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0<I(t,T,null,[e]).length},I.contains=function(e,t){return(e.ownerDocument||e)!=T&&V(e),ce.contains(e,t)},I.attr=function(e,t){(e.ownerDocument||e)!=T&&V(e);var n=b.attrHandle[t.toLowerCase()],r=n&&ue.call(b.attrHandle,t.toLowerCase())?n(e,t,!C):void 0;return void 0!==r?r:e.getAttribute(t)},I.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},ce.uniqueSort=function(e){var t,n=[],r=0,i=0;if(a=!le.sortStable,o=!le.sortStable&&ae.call(e,0),de.call(e,l),a){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)he.call(e,n[r],1)}return o=null,e},ce.fn.uniqueSort=function(){return this.pushStack(ce.uniqueSort(ae.apply(this)))},(b=ce.expr={cacheLength:50,createPseudo:F,match:D,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(v," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(d,e,t,h,g){var v="nth"!==d.slice(0,3),y="last"!==d.slice(-4),m="of-type"===e;return 1===h&&0===g?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u=v!==y?"nextSibling":"previousSibling",l=e.parentNode,c=m&&e.nodeName.toLowerCase(),f=!n&&!m,p=!1;if(l){if(v){while(u){o=e;while(o=o[u])if(m?fe(o,c):1===o.nodeType)return!1;s=u="only"===d&&!s&&"nextSibling"}return!0}if(s=[y?l.firstChild:l.lastChild],y&&f){p=(a=(r=(i=l[S]||(l[S]={}))[d]||[])[0]===E&&r[1])&&r[2],o=a&&l.childNodes[a];while(o=++a&&o&&o[u]||(p=a=0)||s.pop())if(1===o.nodeType&&++p&&o===e){i[d]=[E,a,p];break}}else if(f&&(p=a=(r=(i=e[S]||(e[S]={}))[d]||[])[0]===E&&r[1]),!1===p)while(o=++a&&o&&o[u]||(p=a=0)||s.pop())if((m?fe(o,c):1===o.nodeType)&&++p&&(f&&((i=o[S]||(o[S]={}))[d]=[E,p]),o===e))break;return(p-=g)===h||p%h==0&&0<=p/h}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||I.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?F(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=se.call(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:F(function(e){var r=[],i=[],s=ne(e.replace(ve,"$1"));return s[S]?F(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:F(function(t){return function(e){return 0<I(t,e).length}}),contains:F(function(t){return t=t.replace(O,P),function(e){return-1<(e.textContent||ce.text(e)).indexOf(t)}}),lang:F(function(n){return A.test(n||"")||I.error("unsupported lang: "+n),n=n.replace(O,P).toLowerCase(),function(e){var t;do{if(t=C?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=ie.location&&ie.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===r},focus:function(e){return e===function(){try{return T.activeElement}catch(e){}}()&&T.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:z(!1),disabled:z(!0),checked:function(e){return fe(e,"input")&&!!e.checked||fe(e,"option")&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return q.test(e.nodeName)},input:function(e){return N.test(e.nodeName)},button:function(e){return fe(e,"input")&&"button"===e.type||fe(e,"button")},text:function(e){var t;return fe(e,"input")&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:X(function(){return[0]}),last:X(function(e,t){return[t-1]}),eq:X(function(e,t,n){return[n<0?n+t:n]}),even:X(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:X(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:X(function(e,t,n){var r;for(r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:X(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=B(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=_(e);function G(){}function Y(e,t){var n,r,i,o,a,s,u,l=c[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=y.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=m.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace(ve," ")}),a=a.slice(n.length)),b.filter)!(r=D[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?I.error(e):c(e,s).slice(0)}function Q(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function J(a,e,t){var s=e.dir,u=e.next,l=u||s,c=t&&"parentNode"===l,f=n++;return e.first?function(e,t,n){while(e=e[s])if(1===e.nodeType||c)return a(e,t,n);return!1}:function(e,t,n){var r,i,o=[E,f];if(n){while(e=e[s])if((1===e.nodeType||c)&&a(e,t,n))return!0}else while(e=e[s])if(1===e.nodeType||c)if(i=e[S]||(e[S]={}),u&&fe(e,u))e=e[s]||e;else{if((r=i[l])&&r[0]===E&&r[1]===f)return o[2]=r[2];if((i[l]=o)[2]=a(e,t,n))return!0}return!1}}function K(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Z(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function ee(d,h,g,v,y,e){return v&&!v[S]&&(v=ee(v)),y&&!y[S]&&(y=ee(y,e)),F(function(e,t,n,r){var i,o,a,s,u=[],l=[],c=t.length,f=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)I(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),p=!d||!e&&h?f:Z(f,u,d,n,r);if(g?g(p,s=y||(e?d:c||v)?[]:t,n,r):s=p,v){i=Z(s,l),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(s[l[o]]=!(p[l[o]]=a))}if(e){if(y||d){if(y){i=[],o=s.length;while(o--)(a=s[o])&&i.push(p[o]=a);y(null,s=[],i,r)}o=s.length;while(o--)(a=s[o])&&-1<(i=y?se.call(e,a):u[o])&&(e[i]=!(t[i]=a))}}else s=Z(s===t?s.splice(c,s.length):s),y?y(null,t,s,r):k.apply(t,s)})}function te(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=J(function(e){return e===i},a,!0),l=J(function(e){return-1<se.call(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!=w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[J(K(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return ee(1<s&&K(c),1<s&&Q(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(ve,"$1"),t,s<n&&te(e.slice(s,n)),n<r&&te(e=e.slice(n)),n<r&&Q(e))}c.push(t)}return K(c)}function ne(e,t){var n,v,y,m,x,r,i=[],o=[],a=u[e+" "];if(!a){t||(t=Y(e)),n=t.length;while(n--)(a=te(t[n]))[S]?i.push(a):o.push(a);(a=u(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=E+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==T||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==T||(V(o),n=!C);while(s=v[a++])if(s(o,t||T,n)){k.call(r,o);break}i&&(E=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=pe.call(r));f=Z(f)}k.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&ce.uniqueSort(r)}return i&&(E=h,w=p),c},m?F(r):r))).selector=e}return a}function re(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&Y(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&C&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(O,P),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=D.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(O,P),H.test(o[0].type)&&U(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&Q(o)))return k.apply(n,r),n;break}}}return(l||ne(e,c))(r,t,!C,n,!t||H.test(e)&&U(t.parentNode)||t),n}G.prototype=b.filters=b.pseudos,b.setFilters=new G,le.sortStable=S.split("").sort(l).join("")===S,V(),le.sortDetached=$(function(e){return 1&e.compareDocumentPosition(T.createElement("fieldset"))}),ce.find=I,ce.expr[":"]=ce.expr.pseudos,ce.unique=ce.uniqueSort,I.compile=ne,I.select=re,I.setDocument=V,I.tokenize=Y,I.escape=ce.escapeSelector,I.getText=ce.text,I.isXML=ce.isXMLDoc,I.selectors=ce.expr,I.support=ce.support,I.uniqueSort=ce.uniqueSort}();var d=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&ce(e).is(n))break;r.push(e)}return r},h=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},b=ce.expr.match.needsContext,w=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1<se.call(n,e)!==r}):ce.filter(n,e,r)}ce.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?ce.find.matchesSelector(r,e)?[r]:[]:ce.find.matches(e,ce.grep(t,function(e){return 1===e.nodeType}))},ce.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(ce(e).filter(function(){for(t=0;t<r;t++)if(ce.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)ce.find(e,i[t],n);return 1<r?ce.uniqueSort(n):n},filter:function(e){return this.pushStack(T(this,e||[],!1))},not:function(e){return this.pushStack(T(this,e||[],!0))},is:function(e){return!!T(this,"string"==typeof e&&b.test(e)?ce(e):e||[],!1).length}});var k,S=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(ce.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&ce(e);if(!b.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&ce.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?ce.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?se.call(ce(e),this[0]):se.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(ce.uniqueSort(ce.merge(this.get(),ce(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),ce.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return d(e,"parentNode")},parentsUntil:function(e,t,n){return d(e,"parentNode",n)},next:function(e){return A(e,"nextSibling")},prev:function(e){return A(e,"previousSibling")},nextAll:function(e){return d(e,"nextSibling")},prevAll:function(e){return d(e,"previousSibling")},nextUntil:function(e,t,n){return d(e,"nextSibling",n)},prevUntil:function(e,t,n){return d(e,"previousSibling",n)},siblings:function(e){return h((e.parentNode||{}).firstChild,e)},children:function(e){return h(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(fe(e,"template")&&(e=e.content||e),ce.merge([],e.childNodes))}},function(r,i){ce.fn[r]=function(e,t){var n=ce.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=ce.filter(t,n)),1<this.length&&(j[r]||ce.uniqueSort(n),E.test(r)&&n.reverse()),this.pushStack(n)}});var D=/[^\x20\t\r\n\f]+/g;function N(e){return e}function q(e){throw e}function L(e,t,n,r){var i;try{e&&v(i=e.promise)?i.call(e).done(t).fail(n):e&&v(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}ce.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},ce.each(e.match(D)||[],function(e,t){n[t]=!0}),n):ce.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){ce.each(e,function(e,t){v(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==x(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return ce.each(arguments,function(e,t){var n;while(-1<(n=ce.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<ce.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},ce.extend({Deferred:function(e){var o=[["notify","progress",ce.Callbacks("memory"),ce.Callbacks("memory"),2],["resolve","done",ce.Callbacks("once memory"),ce.Callbacks("once memory"),0,"resolved"],["reject","fail",ce.Callbacks("once memory"),ce.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return ce.Deferred(function(r){ce.each(o,function(e,t){var n=v(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&v(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,v(t)?s?t.call(e,l(u,o,N,s),l(u,o,q,s)):(u++,t.call(e,l(u,o,N,s),l(u,o,q,s),l(u,o,N,o.notifyWith))):(a!==N&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){ce.Deferred.exceptionHook&&ce.Deferred.exceptionHook(e,t.error),u<=i+1&&(a!==q&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(ce.Deferred.getErrorHook?t.error=ce.Deferred.getErrorHook():ce.Deferred.getStackHook&&(t.error=ce.Deferred.getStackHook()),ie.setTimeout(t))}}return ce.Deferred(function(e){o[0][3].add(l(0,e,v(r)?r:N,e.notifyWith)),o[1][3].add(l(0,e,v(t)?t:N)),o[2][3].add(l(0,e,v(n)?n:q))}).promise()},promise:function(e){return null!=e?ce.extend(e,a):a}},s={};return ce.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=ae.call(arguments),o=ce.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?ae.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(L(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||v(i[t]&&i[t].then)))return o.then();while(t--)L(i[t],a(t),o.reject);return o.promise()}});var H=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;ce.Deferred.exceptionHook=function(e,t){ie.console&&ie.console.warn&&e&&H.test(e.name)&&ie.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},ce.readyException=function(e){ie.setTimeout(function(){throw e})};var O=ce.Deferred();function P(){C.removeEventListener("DOMContentLoaded",P),ie.removeEventListener("load",P),ce.ready()}ce.fn.ready=function(e){return O.then(e)["catch"](function(e){ce.readyException(e)}),this},ce.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--ce.readyWait:ce.isReady)||(ce.isReady=!0)!==e&&0<--ce.readyWait||O.resolveWith(C,[ce])}}),ce.ready.then=O.then,"complete"===C.readyState||"loading"!==C.readyState&&!C.documentElement.doScroll?ie.setTimeout(ce.ready):(C.addEventListener("DOMContentLoaded",P),ie.addEventListener("load",P));var M=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n))for(s in i=!0,n)M(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,v(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(ce(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},R=/^-ms-/,I=/-([a-z])/g;function W(e,t){return t.toUpperCase()}function F(e){return e.replace(R,"ms-").replace(I,W)}var $=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function B(){this.expando=ce.expando+B.uid++}B.uid=1,B.prototype={cache:function(e){var t=e[this.expando];return t||(t={},$(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[F(t)]=n;else for(r in t)i[F(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][F(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(F):(t=F(t))in r?[t]:t.match(D)||[]).length;while(n--)delete r[t[n]]}(void 0===t||ce.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!ce.isEmptyObject(t)}};var _=new B,z=new B,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,U=/[A-Z]/g;function V(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(U,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:X.test(i)?JSON.parse(i):i)}catch(e){}z.set(e,t,n)}else n=void 0;return n}ce.extend({hasData:function(e){return z.hasData(e)||_.hasData(e)},data:function(e,t,n){return z.access(e,t,n)},removeData:function(e,t){z.remove(e,t)},_data:function(e,t,n){return _.access(e,t,n)},_removeData:function(e,t){_.remove(e,t)}}),ce.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=z.get(o),1===o.nodeType&&!_.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=F(r.slice(5)),V(o,r,i[r]));_.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){z.set(this,n)}):M(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=z.get(o,n))?t:void 0!==(t=V(o,n))?t:void 0;this.each(function(){z.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){z.remove(this,e)})}}),ce.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=_.get(e,t),n&&(!r||Array.isArray(n)?r=_.access(e,t,ce.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=ce.queue(e,t),r=n.length,i=n.shift(),o=ce._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){ce.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return _.get(e,n)||_.access(e,n,{empty:ce.Callbacks("once memory").add(function(){_.remove(e,[t+"queue",n])})})}}),ce.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?ce.queue(this[0],t):void 0===n?this:this.each(function(){var e=ce.queue(this,t,n);ce._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&ce.dequeue(this,t)})},dequeue:function(e){return this.each(function(){ce.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=ce.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=_.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var G=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,Y=new RegExp("^(?:([+-])=|)("+G+")([a-z%]*)$","i"),Q=["Top","Right","Bottom","Left"],J=C.documentElement,K=function(e){return ce.contains(e.ownerDocument,e)},Z={composed:!0};J.getRootNode&&(K=function(e){return ce.contains(e.ownerDocument,e)||e.getRootNode(Z)===e.ownerDocument});var ee=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&K(e)&&"none"===ce.css(e,"display")};function te(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return ce.css(e,t,"")},u=s(),l=n&&n[3]||(ce.cssNumber[t]?"":"px"),c=e.nodeType&&(ce.cssNumber[t]||"px"!==l&&+u)&&Y.exec(ce.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)ce.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,ce.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ne={};function re(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=_.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ee(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ne[s])||(o=a.body.appendChild(a.createElement(s)),u=ce.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ne[s]=u)))):"none"!==n&&(l[c]="none",_.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}ce.fn.extend({show:function(){return re(this,!0)},hide:function(){return re(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ee(this)?ce(this).show():ce(this).hide()})}});var xe,be,we=/^(?:checkbox|radio)$/i,Te=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="<textarea>x</textarea>",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="<option></option>",le.option=!!xe.lastChild;var ke={thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n<r;n++)_.set(e[n],"globalEval",!t||_.get(t[n],"globalEval"))}ke.tbody=ke.tfoot=ke.colgroup=ke.caption=ke.thead,ke.th=ke.td,le.option||(ke.optgroup=ke.option=[1,"<select multiple='multiple'>","</select>"]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===x(o))ce.merge(p,o.nodeType?[o]:o);else if(je.test(o)){a=a||f.appendChild(t.createElement("div")),s=(Te.exec(o)||["",""])[1].toLowerCase(),u=ke[s]||ke._default,a.innerHTML=u[1]+ce.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;ce.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<ce.inArray(o,r))i&&i.push(o);else if(l=K(o),a=Se(f.appendChild(o),"script"),l&&Ee(a),n){c=0;while(o=a[c++])Ce.test(o.type||"")&&n.push(o)}return f}var De=/^([^.]*)(?:\.(.+)|)/;function Ne(){return!0}function qe(){return!1}function Le(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Le(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=qe;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return ce().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=ce.guid++)),e.each(function(){ce.event.add(this,t,i,r,n)})}function He(e,r,t){t?(_.set(e,r,!1),ce.event.add(e,r,{namespace:!1,handler:function(e){var t,n=_.get(this,r);if(1&e.isTrigger&&this[r]){if(n)(ce.event.special[r]||{}).delegateType&&e.stopPropagation();else if(n=ae.call(arguments),_.set(this,r,n),this[r](),t=_.get(this,r),_.set(this,r,!1),n!==t)return e.stopImmediatePropagation(),e.preventDefault(),t}else n&&(_.set(this,r,ce.event.trigger(n[0],n.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Ne)}})):void 0===_.get(e,r)&&ce.event.add(e,r,Ne)}ce.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=_.get(t);if($(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&ce.find.matchesSelector(J,i),n.guid||(n.guid=ce.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof ce&&ce.event.triggered!==e.type?ce.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(D)||[""]).length;while(l--)d=g=(s=De.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=ce.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=ce.event.special[d]||{},c=ce.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&ce.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),ce.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=_.hasData(e)&&_.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(D)||[""]).length;while(l--)if(d=g=(s=De.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=ce.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||ce.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)ce.event.remove(e,d+t[l],n,r,!0);ce.isEmptyObject(u)&&_.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=ce.event.fix(e),l=(_.get(this,"events")||Object.create(null))[u.type]||[],c=ce.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=ce.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((ce.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<ce(i,this).index(l):ce.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(ce.Event.prototype,t,{enumerable:!0,configurable:!0,get:v(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[ce.expando]?e:new ce.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return we.test(t.type)&&t.click&&fe(t,"input")&&He(t,"click",!0),!1},trigger:function(e){var t=this||e;return we.test(t.type)&&t.click&&fe(t,"input")&&He(t,"click"),!0},_default:function(e){var t=e.target;return we.test(t.type)&&t.click&&fe(t,"input")&&_.get(t,"click")||fe(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},ce.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},ce.Event=function(e,t){if(!(this instanceof ce.Event))return new ce.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ne:qe,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&ce.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[ce.expando]=!0},ce.Event.prototype={constructor:ce.Event,isDefaultPrevented:qe,isPropagationStopped:qe,isImmediatePropagationStopped:qe,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ne,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ne,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ne,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},ce.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},ce.event.addProp),ce.each({focus:"focusin",blur:"focusout"},function(r,i){function o(e){if(C.documentMode){var t=_.get(this,"handle"),n=ce.event.fix(e);n.type="focusin"===e.type?"focus":"blur",n.isSimulated=!0,t(e),n.target===n.currentTarget&&t(n)}else ce.event.simulate(i,e.target,ce.event.fix(e))}ce.event.special[r]={setup:function(){var e;if(He(this,r,!0),!C.documentMode)return!1;(e=_.get(this,i))||this.addEventListener(i,o),_.set(this,i,(e||0)+1)},trigger:function(){return He(this,r),!0},teardown:function(){var e;if(!C.documentMode)return!1;(e=_.get(this,i)-1)?_.set(this,i,e):(this.removeEventListener(i,o),_.remove(this,i))},_default:function(e){return _.get(e.target,r)},delegateType:i},ce.event.special[i]={setup:function(){var e=this.ownerDocument||this.document||this,t=C.documentMode?this:e,n=_.get(t,i);n||(C.documentMode?this.addEventListener(i,o):e.addEventListener(r,o,!0)),_.set(t,i,(n||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=C.documentMode?this:e,n=_.get(t,i)-1;n?_.set(t,i,n):(C.documentMode?this.removeEventListener(i,o):e.removeEventListener(r,o,!0),_.remove(t,i))}}}),ce.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){ce.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||ce.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),ce.fn.extend({on:function(e,t,n,r){return Le(this,e,t,n,r)},one:function(e,t,n,r){return Le(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,ce(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=qe),this.each(function(){ce.event.remove(this,e,n,t)})}});var Oe=/<script|<style|<link/i,Pe=/checked\s*(?:[^=]|=\s*.checked.)/i,Me=/^\s*<!\[CDATA\[|\]\]>\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)ce.event.add(t,i,s[i][n]);z.hasData(e)&&(o=z.access(e),a=ce.extend({},o),z.set(t,a))}}function $e(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=v(d);if(h||1<f&&"string"==typeof d&&!le.checkClone&&Pe.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),$e(t,r,i,o)});if(f&&(t=(e=Ae(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=ce.map(Se(e,"script"),Ie)).length;c<f;c++)u=e,c!==p&&(u=ce.clone(u,!0,!0),s&&ce.merge(a,Se(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,ce.map(a,We),c=0;c<s;c++)u=a[c],Ce.test(u.type||"")&&!_.access(u,"globalEval")&&ce.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?ce._evalUrl&&!u.noModule&&ce._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):m(u.textContent.replace(Me,""),u,l))}return n}function Be(e,t,n){for(var r,i=t?ce.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||ce.cleanData(Se(r)),r.parentNode&&(n&&K(r)&&Ee(Se(r,"script")),r.parentNode.removeChild(r));return e}ce.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=K(e);if(!(le.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||ce.isXMLDoc(e)))for(a=Se(c),r=0,i=(o=Se(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&we.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||Se(e),a=a||Se(c),r=0,i=o.length;r<i;r++)Fe(o[r],a[r]);else Fe(e,c);return 0<(a=Se(c,"script")).length&&Ee(a,!f&&Se(e,"script")),c},cleanData:function(e){for(var t,n,r,i=ce.event.special,o=0;void 0!==(n=e[o]);o++)if($(n)){if(t=n[_.expando]){if(t.events)for(r in t.events)i[r]?ce.event.remove(n,r):ce.removeEvent(n,r,t.handle);n[_.expando]=void 0}n[z.expando]&&(n[z.expando]=void 0)}}}),ce.fn.extend({detach:function(e){return Be(this,e,!0)},remove:function(e){return Be(this,e)},text:function(e){return M(this,function(e){return void 0===e?ce.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return $e(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Re(this,e).appendChild(e)})},prepend:function(){return $e(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Re(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return $e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return $e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(ce.cleanData(Se(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return ce.clone(this,e,t)})},html:function(e){return M(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Oe.test(e)&&!ke[(Te.exec(e)||["",""])[1].toLowerCase()]){e=ce.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(ce.cleanData(Se(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return $e(this,arguments,function(e){var t=this.parentNode;ce.inArray(this,n)<0&&(ce.cleanData(Se(this)),t&&t.replaceChild(e,this))},n)}}),ce.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){ce.fn[e]=function(e){for(var t,n=[],r=ce(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),ce(r[o])[a](t),s.apply(n,t.get());return this.pushStack(n)}});var _e=new RegExp("^("+G+")(?!px)[a-z%]+$","i"),ze=/^--/,Xe=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=ie),t.getComputedStyle(e)},Ue=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Ve=new RegExp(Q.join("|"),"i");function Ge(e,t,n){var r,i,o,a,s=ze.test(t),u=e.style;return(n=n||Xe(e))&&(a=n.getPropertyValue(t)||n[t],s&&a&&(a=a.replace(ve,"$1")||void 0),""!==a||K(e)||(a=ce.style(e,t)),!le.pixelBoxStyles()&&_e.test(a)&&Ve.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=n.width,u.width=r,u.minWidth=i,u.maxWidth=o)),void 0!==a?a+"":a}function Ye(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",J.appendChild(u).appendChild(l);var e=ie.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),J.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=C.createElement("div"),l=C.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",le.clearCloneStyle="content-box"===l.style.backgroundClip,ce.extend(le,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=C.createElement("table"),t=C.createElement("tr"),n=C.createElement("div"),e.style.cssText="position:absolute;left:-11111px;border-collapse:separate",t.style.cssText="box-sizing:content-box;border:1px solid",t.style.height="1px",n.style.height="9px",n.style.display="block",J.appendChild(e).appendChild(t).appendChild(n),r=ie.getComputedStyle(t),a=parseInt(r.height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===t.offsetHeight,J.removeChild(e)),a}}))}();var Qe=["Webkit","Moz","ms"],Je=C.createElement("div").style,Ke={};function Ze(e){var t=ce.cssProps[e]||Ke[e];return t||(e in Je?e:Ke[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=Qe.length;while(n--)if((e=Qe[n]+t)in Je)return e}(e)||e)}var et=/^(none|table(?!-c[ea]).+)/,tt={position:"absolute",visibility:"hidden",display:"block"},nt={letterSpacing:"0",fontWeight:"400"};function rt(e,t,n){var r=Y.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function it(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0,l=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(l+=ce.css(e,n+Q[a],!0,i)),r?("content"===n&&(u-=ce.css(e,"padding"+Q[a],!0,i)),"margin"!==n&&(u-=ce.css(e,"border"+Q[a]+"Width",!0,i))):(u+=ce.css(e,"padding"+Q[a],!0,i),"padding"!==n?u+=ce.css(e,"border"+Q[a]+"Width",!0,i):s+=ce.css(e,"border"+Q[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u+l}function ot(e,t,n){var r=Xe(e),i=(!le.boxSizingReliable()||n)&&"border-box"===ce.css(e,"boxSizing",!1,r),o=i,a=Ge(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(_e.test(a)){if(!n)return a;a="auto"}return(!le.boxSizingReliable()&&i||!le.reliableTrDimensions()&&fe(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===ce.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===ce.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+it(e,t,n||(i?"border":"content"),o,r,a)+"px"}function at(e,t,n,r,i){return new at.prototype.init(e,t,n,r,i)}ce.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Ge(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=F(t),u=ze.test(t),l=e.style;if(u||(t=Ze(s)),a=ce.cssHooks[t]||ce.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=Y.exec(n))&&i[1]&&(n=te(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(ce.cssNumber[s]?"":"px")),le.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=F(t);return ze.test(t)||(t=Ze(s)),(a=ce.cssHooks[t]||ce.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Ge(e,t,r)),"normal"===i&&t in nt&&(i=nt[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),ce.each(["height","width"],function(e,u){ce.cssHooks[u]={get:function(e,t,n){if(t)return!et.test(ce.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?ot(e,u,n):Ue(e,tt,function(){return ot(e,u,n)})},set:function(e,t,n){var r,i=Xe(e),o=!le.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===ce.css(e,"boxSizing",!1,i),s=n?it(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-it(e,u,"border",!1,i)-.5)),s&&(r=Y.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=ce.css(e,u)),rt(0,t,s)}}}),ce.cssHooks.marginLeft=Ye(le.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Ge(e,"marginLeft"))||e.getBoundingClientRect().left-Ue(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),ce.each({margin:"",padding:"",border:"Width"},function(i,o){ce.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+Q[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(ce.cssHooks[i+o].set=rt)}),ce.fn.extend({css:function(e,t){return M(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Xe(e),i=t.length;a<i;a++)o[t[a]]=ce.css(e,t[a],!1,r);return o}return void 0!==n?ce.style(e,t,n):ce.css(e,t)},e,t,1<arguments.length)}}),((ce.Tween=at).prototype={constructor:at,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||ce.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(ce.cssNumber[n]?"":"px")},cur:function(){var e=at.propHooks[this.prop];return e&&e.get?e.get(this):at.propHooks._default.get(this)},run:function(e){var t,n=at.propHooks[this.prop];return this.options.duration?this.pos=t=ce.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):at.propHooks._default.set(this),this}}).init.prototype=at.prototype,(at.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=ce.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){ce.fx.step[e.prop]?ce.fx.step[e.prop](e):1!==e.elem.nodeType||!ce.cssHooks[e.prop]&&null==e.elem.style[Ze(e.prop)]?e.elem[e.prop]=e.now:ce.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=at.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},ce.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},ce.fx=at.prototype.init,ce.fx.step={};var st,ut,lt,ct,ft=/^(?:toggle|show|hide)$/,pt=/queueHooks$/;function dt(){ut&&(!1===C.hidden&&ie.requestAnimationFrame?ie.requestAnimationFrame(dt):ie.setTimeout(dt,ce.fx.interval),ce.fx.tick())}function ht(){return ie.setTimeout(function(){st=void 0}),st=Date.now()}function gt(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=Q[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function vt(e,t,n){for(var r,i=(yt.tweeners[t]||[]).concat(yt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function yt(o,e,t){var n,a,r=0,i=yt.prefilters.length,s=ce.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=st||ht(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:ce.extend({},e),opts:ce.extend(!0,{specialEasing:{},easing:ce.easing._default},t),originalProperties:e,originalOptions:t,startTime:st||ht(),duration:t.duration,tweens:[],createTween:function(e,t){var n=ce.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=F(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=ce.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=yt.prefilters[r].call(l,o,c,l.opts))return v(n.stop)&&(ce._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return ce.map(c,vt,l),v(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),ce.fx.timer(ce.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}ce.Animation=ce.extend(yt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return te(n.elem,e,Y.exec(t),n),n}]},tweener:function(e,t){v(e)?(t=e,e=["*"]):e=e.match(D);for(var n,r=0,i=e.length;r<i;r++)n=e[r],yt.tweeners[n]=yt.tweeners[n]||[],yt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ee(e),v=_.get(e,"fxshow");for(r in n.queue||(null==(a=ce._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,ce.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],ft.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||ce.style(e,r)}if((u=!ce.isEmptyObject(t))||!ce.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=_.get(e,"display")),"none"===(c=ce.css(e,"display"))&&(l?c=l:(re([e],!0),l=e.style.display||l,c=ce.css(e,"display"),re([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===ce.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=_.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&re([e],!0),p.done(function(){for(r in g||re([e]),_.remove(e,"fxshow"),d)ce.style(e,r,d[r])})),u=vt(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?yt.prefilters.unshift(e):yt.prefilters.push(e)}}),ce.speed=function(e,t,n){var r=e&&"object"==typeof e?ce.extend({},e):{complete:n||!n&&t||v(e)&&e,duration:e,easing:n&&t||t&&!v(t)&&t};return ce.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in ce.fx.speeds?r.duration=ce.fx.speeds[r.duration]:r.duration=ce.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){v(r.old)&&r.old.call(this),r.queue&&ce.dequeue(this,r.queue)},r},ce.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ee).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=ce.isEmptyObject(t),o=ce.speed(e,n,r),a=function(){var e=yt(this,ce.extend({},t),o);(i||_.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=ce.timers,r=_.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&pt.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||ce.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=_.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=ce.timers,o=n?n.length:0;for(t.finish=!0,ce.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),ce.each(["toggle","show","hide"],function(e,r){var i=ce.fn[r];ce.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(gt(r,!0),e,t,n)}}),ce.each({slideDown:gt("show"),slideUp:gt("hide"),slideToggle:gt("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){ce.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),ce.timers=[],ce.fx.tick=function(){var e,t=0,n=ce.timers;for(st=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||ce.fx.stop(),st=void 0},ce.fx.timer=function(e){ce.timers.push(e),ce.fx.start()},ce.fx.interval=13,ce.fx.start=function(){ut||(ut=!0,dt())},ce.fx.stop=function(){ut=null},ce.fx.speeds={slow:600,fast:200,_default:400},ce.fn.delay=function(r,e){return r=ce.fx&&ce.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=ie.setTimeout(e,r);t.stop=function(){ie.clearTimeout(n)}})},lt=C.createElement("input"),ct=C.createElement("select").appendChild(C.createElement("option")),lt.type="checkbox",le.checkOn=""!==lt.value,le.optSelected=ct.selected,(lt=C.createElement("input")).value="t",lt.type="radio",le.radioValue="t"===lt.value;var mt,xt=ce.expr.attrHandle;ce.fn.extend({attr:function(e,t){return M(this,ce.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){ce.removeAttr(this,e)})}}),ce.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?ce.prop(e,t,n):(1===o&&ce.isXMLDoc(e)||(i=ce.attrHooks[t.toLowerCase()]||(ce.expr.match.bool.test(t)?mt:void 0)),void 0!==n?null===n?void ce.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=ce.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!le.radioValue&&"radio"===t&&fe(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(D);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),mt={set:function(e,t,n){return!1===t?ce.removeAttr(e,n):e.setAttribute(n,n),n}},ce.each(ce.expr.match.bool.source.match(/\w+/g),function(e,t){var a=xt[t]||ce.find.attr;xt[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=xt[o],xt[o]=r,r=null!=a(e,t,n)?o:null,xt[o]=i),r}});var bt=/^(?:input|select|textarea|button)$/i,wt=/^(?:a|area)$/i;function Tt(e){return(e.match(D)||[]).join(" ")}function Ct(e){return e.getAttribute&&e.getAttribute("class")||""}function kt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(D)||[]}ce.fn.extend({prop:function(e,t){return M(this,ce.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[ce.propFix[e]||e]})}}),ce.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&ce.isXMLDoc(e)||(t=ce.propFix[t]||t,i=ce.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=ce.find.attr(e,"tabindex");return t?parseInt(t,10):bt.test(e.nodeName)||wt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),le.optSelected||(ce.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),ce.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){ce.propFix[this.toLowerCase()]=this}),ce.fn.extend({addClass:function(t){var e,n,r,i,o,a;return v(t)?this.each(function(e){ce(this).addClass(t.call(this,e,Ct(this)))}):(e=kt(t)).length?this.each(function(){if(r=Ct(this),n=1===this.nodeType&&" "+Tt(r)+" "){for(o=0;o<e.length;o++)i=e[o],n.indexOf(" "+i+" ")<0&&(n+=i+" ");a=Tt(n),r!==a&&this.setAttribute("class",a)}}):this},removeClass:function(t){var e,n,r,i,o,a;return v(t)?this.each(function(e){ce(this).removeClass(t.call(this,e,Ct(this)))}):arguments.length?(e=kt(t)).length?this.each(function(){if(r=Ct(this),n=1===this.nodeType&&" "+Tt(r)+" "){for(o=0;o<e.length;o++){i=e[o];while(-1<n.indexOf(" "+i+" "))n=n.replace(" "+i+" "," ")}a=Tt(n),r!==a&&this.setAttribute("class",a)}}):this:this.attr("class","")},toggleClass:function(t,n){var e,r,i,o,a=typeof t,s="string"===a||Array.isArray(t);return v(t)?this.each(function(e){ce(this).toggleClass(t.call(this,e,Ct(this),n),n)}):"boolean"==typeof n&&s?n?this.addClass(t):this.removeClass(t):(e=kt(t),this.each(function(){if(s)for(o=ce(this),i=0;i<e.length;i++)r=e[i],o.hasClass(r)?o.removeClass(r):o.addClass(r);else void 0!==t&&"boolean"!==a||((r=Ct(this))&&_.set(this,"__className__",r),this.setAttribute&&this.setAttribute("class",r||!1===t?"":_.get(this,"__className__")||""))}))},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+Tt(Ct(n))+" ").indexOf(t))return!0;return!1}});var St=/\r/g;ce.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=v(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,ce(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=ce.map(t,function(e){return null==e?"":e+""})),(r=ce.valHooks[this.type]||ce.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=ce.valHooks[t.type]||ce.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(St,""):null==e?"":e:void 0}}),ce.extend({valHooks:{option:{get:function(e){var t=ce.find.attr(e,"value");return null!=t?t:Tt(ce.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!fe(n.parentNode,"optgroup"))){if(t=ce(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=ce.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<ce.inArray(ce.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),ce.each(["radio","checkbox"],function(){ce.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<ce.inArray(ce(e).val(),t)}},le.checkOn||(ce.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Et=ie.location,jt={guid:Date.now()},At=/\?/;ce.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new ie.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||ce.error("Invalid XML: "+(n?ce.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var Dt=/^(?:focusinfocus|focusoutblur)$/,Nt=function(e){e.stopPropagation()};ce.extend(ce.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||C],d=ue.call(e,"type")?e.type:e,h=ue.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||C,3!==n.nodeType&&8!==n.nodeType&&!Dt.test(d+ce.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[ce.expando]?e:new ce.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:ce.makeArray(t,[e]),c=ce.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!y(n)){for(s=c.delegateType||d,Dt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||C)&&p.push(a.defaultView||a.parentWindow||ie)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(_.get(o,"events")||Object.create(null))[e.type]&&_.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&$(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!$(n)||u&&v(n[d])&&!y(n)&&((a=n[u])&&(n[u]=null),ce.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,Nt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,Nt),ce.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=ce.extend(new ce.Event,n,{type:e,isSimulated:!0});ce.event.trigger(r,null,t)}}),ce.fn.extend({trigger:function(e,t){return this.each(function(){ce.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return ce.event.trigger(e,t,n,!0)}});var qt=/\[\]$/,Lt=/\r?\n/g,Ht=/^(?:submit|button|image|reset|file)$/i,Ot=/^(?:input|select|textarea|keygen)/i;function Pt(n,e,r,i){var t;if(Array.isArray(e))ce.each(e,function(e,t){r||qt.test(n)?i(n,t):Pt(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==x(e))i(n,e);else for(t in e)Pt(n+"["+t+"]",e[t],r,i)}ce.param=function(e,t){var n,r=[],i=function(e,t){var n=v(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!ce.isPlainObject(e))ce.each(e,function(){i(this.name,this.value)});else for(n in e)Pt(n,e[n],t,i);return r.join("&")},ce.fn.extend({serialize:function(){return ce.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=ce.prop(this,"elements");return e?ce.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!ce(this).is(":disabled")&&Ot.test(this.nodeName)&&!Ht.test(e)&&(this.checked||!we.test(e))}).map(function(e,t){var n=ce(this).val();return null==n?null:Array.isArray(n)?ce.map(n,function(e){return{name:t.name,value:e.replace(Lt,"\r\n")}}):{name:t.name,value:n.replace(Lt,"\r\n")}}).get()}});var Mt=/%20/g,Rt=/#.*$/,It=/([?&])_=[^&]*/,Wt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ft=/^(?:GET|HEAD)$/,$t=/^\/\//,Bt={},_t={},zt="*/".concat("*"),Xt=C.createElement("a");function Ut(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(D)||[];if(v(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Vt(t,i,o,a){var s={},u=t===_t;function l(e){var r;return s[e]=!0,ce.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function Gt(e,t){var n,r,i=ce.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&ce.extend(!0,e,r),e}Xt.href=Et.href,ce.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Et.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Et.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":zt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":ce.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Gt(Gt(e,ce.ajaxSettings),t):Gt(ce.ajaxSettings,e)},ajaxPrefilter:Ut(Bt),ajaxTransport:Ut(_t),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=ce.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?ce(y):ce.event,x=ce.Deferred(),b=ce.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=Wt.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||Et.href)+"").replace($t,Et.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(D)||[""],null==v.crossDomain){r=C.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Xt.protocol+"//"+Xt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=ce.param(v.data,v.traditional)),Vt(Bt,v,t,T),h)return T;for(i in(g=ce.event&&v.global)&&0==ce.active++&&ce.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Ft.test(v.type),f=v.url.replace(Rt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Mt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(At.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(It,"$1"),o=(At.test(f)?"&":"?")+"_="+jt.guid+++o),v.url=f+o),v.ifModified&&(ce.lastModified[f]&&T.setRequestHeader("If-Modified-Since",ce.lastModified[f]),ce.etag[f]&&T.setRequestHeader("If-None-Match",ce.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+zt+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Vt(_t,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=ie.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&ie.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),!i&&-1<ce.inArray("script",v.dataTypes)&&ce.inArray("json",v.dataTypes)<0&&(v.converters["text script"]=function(){}),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(ce.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(ce.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--ce.active||ce.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return ce.get(e,t,n,"json")},getScript:function(e,t){return ce.get(e,void 0,t,"script")}}),ce.each(["get","post"],function(e,i){ce[i]=function(e,t,n,r){return v(t)&&(r=r||n,n=t,t=void 0),ce.ajax(ce.extend({url:e,type:i,dataType:r,data:t,success:n},ce.isPlainObject(e)&&e))}}),ce.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),ce._evalUrl=function(e,t,n){return ce.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){ce.globalEval(e,t,n)}})},ce.fn.extend({wrapAll:function(e){var t;return this[0]&&(v(e)&&(e=e.call(this[0])),t=ce(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return v(n)?this.each(function(e){ce(this).wrapInner(n.call(this,e))}):this.each(function(){var e=ce(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=v(t);return this.each(function(e){ce(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){ce(this).replaceWith(this.childNodes)}),this}}),ce.expr.pseudos.hidden=function(e){return!ce.expr.pseudos.visible(e)},ce.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},ce.ajaxSettings.xhr=function(){try{return new ie.XMLHttpRequest}catch(e){}};var Yt={0:200,1223:204},Qt=ce.ajaxSettings.xhr();le.cors=!!Qt&&"withCredentials"in Qt,le.ajax=Qt=!!Qt,ce.ajaxTransport(function(i){var o,a;if(le.cors||Qt&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(Yt[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&ie.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),ce.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),ce.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return ce.globalEval(e),e}}}),ce.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),ce.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=ce("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=Tt(e.slice(s)),e=e.slice(0,s)),v(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&ce.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?ce("<div>").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var en=/^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g;ce.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),v(e))return r=ae.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(ae.call(arguments)))}).guid=e.guid=e.guid||ce.guid++,i},ce.holdReady=function(e){e?ce.readyWait++:ce.ready(!0)},ce.isArray=Array.isArray,ce.parseJSON=JSON.parse,ce.nodeName=fe,ce.isFunction=v,ce.isWindow=y,ce.camelCase=F,ce.type=x,ce.now=Date.now,ce.isNumeric=function(e){var t=ce.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},ce.trim=function(e){return null==e?"":(e+"").replace(en,"$1")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return ce});var tn=ie.jQuery,nn=ie.$;return ce.noConflict=function(e){return ie.$===ce&&(ie.$=nn),e&&ie.jQuery===ce&&(ie.jQuery=tn),ce},"undefined"==typeof e&&(ie.jQuery=ie.$=ce),ce}); diff --git a/srv/static/style.css b/srv/static/style.css @@ -0,0 +1,22 @@ +body +{ + margin: 0px auto; + text-align: center; + background-color:#000; + color: #f4f4f4; + font-family: monospace; +} + +pre +{ + margin: 5px auto; + padding: 3px; + display: inline-block; + background-color: #555; + color: #fff; +} + +a +{ + color: #FCD612; +} diff --git a/srv/tarinaserver.py b/srv/tarinaserver.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 + +import web +import os +import socket +import ifaddr +import sys +import time +import random +import hashlib + +# Get path of the current dir, then use it as working directory: +rundir = os.path.dirname(__file__) +if rundir != '': + os.chdir(rundir) + +filmfolder = '/home/pi/Videos/' + +basedir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(basedir) + +# Link video directory to static dir +if os.path.isfile('static/Videos') == False: + os.system("ln -s -t static/ " + filmfolder) + +films = [] + +#NETWORKS + +networks=[] +adapters = ifaddr.get_adapters() +for adapter in adapters: + print("IPs of network adapter " + adapter.nice_name) + for ip in adapter.ips: + if '::' not in ip.ip[0] and '127.0.0.1' != ip.ip: + print(ip.ip) + networks.append(ip.ip) +network=networks[0] + +urls = ( + '/?', 'index', + '/f/(.*)?', 'films' +) + +app = web.application(urls, globals()) +render = web.template.render('templates/', base="base") +web.config.debug=False +os.system('rm '+basedir+'/sessions/*') +store = web.session.DiskStore(basedir + '/sessions/') +session = web.session.Session(app,store,initializer={'login': 0, 'user': '', 'backurl': '', 'bildsida': 0, 'cameras': [], 'reload': 0, 'randhash':''}) + +port=55555 +ip='0.0.0.0' +cameras=[] +recording = False + +session.randhash = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest() + +##---------------Connection---------------------------------------------- + +def pingtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + if host not in cameras and host not in networks: + session.cameras.append(host) + print("Found camera! "+host) + print("Sent to server..") + break + except: + ('did not connect') + s.close() + +def sendtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.1) + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + print("Sent to server..") + break + except: + ('did not connect') + s.close() + + +def getfilms(filmfolder): + #get a list of films, in order of settings.p file last modified + films_sorted = [] + films = next(os.walk(filmfolder))[1] + for i in films: + if os.path.isfile(filmfolder + i + '/settings.p') == True: + lastupdate = os.path.getmtime(filmfolder + i + '/' + 'settings.p') + films_sorted.append((i,lastupdate)) + else: + films_sorted.append((i,0)) + films_sorted = sorted(films_sorted, key=lambda tup: tup[1], reverse=True) + print(films_sorted) + return films_sorted + +#------------Count scenes-------- + +def countscenes(filmfolder, filmname): + scenes = 0 + try: + allfiles = os.listdir(filmfolder + filmname) + except: + allfiles = [] + scenes = 0 + for a in allfiles: + if 'scene' in a: + scenes = scenes + 1 + return scenes + +#------------Count shots-------- + +def countshots(filmname, filmfolder, scene): + shots = 0 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3)) + except: + allfiles = [] + shots = 0 + for a in allfiles: + if 'shot' in a: + shots = shots + 1 + return shots + +#------------Count takes-------- + +def counttakes(filmname, filmfolder, scene, shot): + takes = 0 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3)) + except: + allfiles = [] + return takes + for a in allfiles: + if '.mp4' in a or '.h264' in a: + takes = takes + 1 + return takes + +class index: + def GET(self): + global recording + films = getfilms(filmfolder) + renderedfilms = [] + unrenderedfilms = [] + for f in films: + if os.path.isfile('static/Videos/' + f[0] + '/' + f[0] + '.mp4') == True: + renderedfilms.append(f[0]) + else: + unrenderedfilms.append(f[0]) + i=web.input(func=None,selected=None) + if i.selected != None: + sendtocamera(ip,port,'SELECTED:'+i.selected) + if i.func == 'search': + session.cameras=[] + # ping ip every 10 sec while not recording to connect cameras + pingip=0 + while pingip < 255 : + pingip+=1 + pingtocamera(network[:-3]+str(pingip),port,'PING') + elif i.func == 'record': + sendtocamera(ip,port,'REC') + elif i.func == 'retake': + print('retake') + elif i.func == 'up': + sendtocamera(ip,port,'UP') + elif i.func == 'down': + sendtocamera(ip,port,'DOWN') + elif i.func == 'left': + sendtocamera(ip,port,'LEFT') + elif i.func == 'right': + sendtocamera(ip,port,'RIGHT') + elif i.func == 'view': + sendtocamera(ip,port,'VIEW') + elif i.func == 'middle': + sendtocamera(ip,port,'MIDDLE') + elif i.func == 'delete': + sendtocamera(ip,port,'DELETE') + elif i.func == 'picture': + sendtocamera(ip,port,'PICTURE') + session.randhash = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest() + session.reload = 1 + interface=open('/dev/shm/interface','r') + vumeter=open('/dev/shm/vumeter','r') + menu=interface.readlines() + vumetermessage=vumeter.readlines()[0].rstrip('\n') + try: + selected=int(menu[0]) + except: + selected=0 + try: + name=menu[3].split(':')[1] + name=name.rstrip('\n') + except: + name='' + try: + scene=menu[4].split(':')[1].split('/')[0] + except: + scene=1 + try: + shot=menu[5].split(':')[1].split('/')[0] + except: + shot=1 + try: + take=menu[6].split(':')[1].split('/')[0] + except: + take=1 + session.reload = 0 + if i.func == 'retake': + print(i.func+'fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuiccccccccccccccccccccccccccccccccccccccccckkkkkkkkkkkkkkkkkkkkkkkkkk') + if recording == False: + sendtocamera(ip,port,'RETAKE:'+shot) + recording = True + else: + sendtocamera(ip,port,'STOPRETAKE') + recording = False + if i.func != None: + time.sleep(1) + session.reload = 1 + raise web.seeother('/') + thumb="/static/Videos/"+name+"/scene"+str(scene).zfill(3)+"/shot"+str(shot).zfill(3)+"/picture"+str(take).zfill(3)+".jpeg" + print(thumb) + if os.path.isfile(basedir+thumb) == False: + print(basedir+thumb) + thumb='' + return render.index(renderedfilms, unrenderedfilms, session.cameras, menu, selected,name,scene,shot,take,str,session.randhash,thumb,vumetermessage,i.func) + +class films: + def GET(self, film): + shots = 0 + takes = 0 + i = web.input(page=None, scene=None, shot=None, take=None) + if i.scene != None: + shots = countshots(film, filmfolder, i.scene) + takes = counttakes(film, filmfolder, i.scene, i.shot) + if i.scene != None and i.shot != None: + shots = countshots(film, filmfolder, i.scene) + scenes = countscenes(filmfolder, film) + return render.filmpage(film, scenes, str, filmfolder, counttakes, countshots, shots, i.scene, takes, i.shot, i.take) + +application = app.wsgifunc() + diff --git a/srv/templates/base.html b/srv/templates/base.html @@ -0,0 +1,14 @@ +$def with (content) +<!doctype html> +<HEAD> + <meta charset="utf-8"> + <title>Tarina | video & audio recorder with glue</title> + <link rel="stylesheet" href="/static/style.css?v=35" type="text/css" rel="stylesheet"/> + <script src="/static/jquery-3.7.1.min.js" type="text/javascript"></script> +</HEAD> + +<BODY> + + $:content + +</BODY> diff --git a/srv/templates/filmpage.html b/srv/templates/filmpage.html @@ -0,0 +1,39 @@ +$def with (film, scenes, str, filmfolder, counttakes, countshots, shots, scene, takes, shot, take) +$ video = '' +$if take != None: + <h1>$film | scene $scene | shot $shot | take $take</h1> + <h2><a href='?scene=$scene&shot=$shot'>go back</a></h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(take).zfill(3) + '.mp4' +$elif shot != None: + <h1>$film | scene $scene | shot $shot</h1> + <h2><a href='?scene=$scene'>go back</a></h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(takes).zfill(3) + '.mp4' +$elif scene != None: + <h1>$film | scene $scene</h1> + <h2><a href='/f/$film'>go back</a><h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/scene.mp4' +$elif scene == None: + <h1>$film</h1> + $ video = '/static/Videos/' + film + '/' + film + '.mp4' +<video width="80%" controls> +<source src="$video" type="video/mp4"> +Your brower is caput +</video> +<p>Copy project to your destination:</p> +<pre>scp -r pi@tarina.local:~/Videos/$film ~/films/$film </pre> + +$if shot != None: + $for t in range(takes): + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(t+1).zfill(3) + '.jpeg' + <a href="?scene=$scene&shot=$shot&take=${str(t+1)}"><img width="80%" src="$thumbnail_url"/></a><br> +$elif scene != None: + $for s in range(shots) + $ t = countshots(film, filmfolder, scene) + $ p = counttakes(film, filmfolder, scene, s+1) + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(s+1).zfill(3) + '/take' + str(p).zfill(3) + '.jpeg' + <a href="?scene=$scene&shot=${str(s+1)}"><img width="80%" src="$thumbnail_url"/></a><br> +$else: + $for s in range(scenes): + $ t = counttakes(film, filmfolder, s+1, 1) + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(s+1).zfill(3) + '/shot001/take' + str(t).zfill(3) + '.jpeg' + <a href="?scene=${str(s+1)}"><img width="80%" src="$thumbnail_url"/></a><br> diff --git a/srv/templates/index.html b/srv/templates/index.html @@ -0,0 +1,95 @@ +$def with (renderedfilms, unrenderedfilms, cameras, menu, selected,name,scene,shot,take,str,randhash,thumb,vumetermessage,func) +$var renderedfilms = renderedfilms +$var unrenderedfilms = unrenderedfilms +<script> +$$(document).on('keydown', function (event) { + if (event.which == 37) { + $$('#LEFT').trigger('click'); + } else if (event.which == 39) { + $$('#RIGHT').trigger('click'); + } else if (event.which == 38) { + $$('#UP').trigger('click'); + } else if (event.which == 40) { + $$('#DOWN').trigger('click'); + } else if (event.which == 33) { + $$('#RECORD').trigger('click'); + } else if (event.which == 34) { + $$('#RETAKE').trigger('click'); + } else if (event.which == 9) { + $$('#VIEW').trigger('click'); + } else if (event.which == 46) { + $$('#DELETE').trigger('click'); + } else if (event.which == 13) { + $$('#MIDDLE').trigger('click'); + } +}); +</script> +connected +<pre>use keyboard to control</pre> +$for i in cameras: + $i +<br> +<div id="controls" style="display:none"> > +<a id="VIEW" href="/?func=view">VIEW</a> <a id="UP" href="/?func=up">__UP__</a> <a id="RECORD" href="/?func=record">RECORD</a><br> +<a id="LEFT" href="/?func=left">LEFT</a> <a id="MIDDLE" href="/?func=middle">MIDDLE</a> <a id="RIGHT" href="/?func=right">RIGHT</a><br> +<a id="DELETE" href="/?func=delete">DELETE</a> <a id="DOWN" href="/?func=down">DOWN</a> <a id="RETAKE" href="/?func=retake">RETAKE</a><br> +<a href="/?func=picture">PICTURE</a> +<a href="/?func=search">SEARCH</a> +</div> + +<div id="menu" style="margin:0 auto; width:99%"> +$vumetermessage +<br> +$ y=0 +$for m in menu[3:]: + $if selected == y: + <b>$m[:-1]</b> + $else: + <a href="?selected=$y">$m[:-1]</a> + $ y+=1 +<br> +</div> +$if thumb != '': + $ picture="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/picture" + str(take).zfill(3) + ".jpeg" +$else: + $ picture="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/take" + str(take).zfill(3) + ".jpeg" + + +$ take_link="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/take" + str(take).zfill(3) + ".mp4" +$ scene_link="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/scene.mp4" +$ film_link="static/Videos/" + name + "/" +name+ ".mp4" +$if selected == 0: + <a href='$film_link'><img width="80%" src="$picture?$randhash"/></a><br> +$elif selected == 1: + <a href='$scene_link'><img width="80%" src="$picture?$randhash"/></a><br> +$elif selected > 1: + <a href='$take_link'><img width="80%" src="$picture?$randhash"/></a><br> +<br> +$if func=='show_all_films': + <h1>FILMS</h1> + + $for i in renderedfilms: + <p>--------------------------------------------------------------</p> + <h2>$i</h2> + <a href="static/Videos/$i/${i}.mp4"><img width="80%" src="static/Videos/$i/scene001/shot001/take001.jpeg?$randhash"/></a><br> + <p>Copy project to your destination:</p> + <pre>scp -r pi@tarina.local:~/Videos/$i ~/films/$i</pre> + <h1>Films unrendered</h1> + + $for i in unrenderedfilms: + <h2>$i </h2> + <p>Copy project to your destination:</p> + <pre>scp -r pi@tarina.local:~/Videos/$i ~/films/$i</pre> + +<script> +$$('#LEFT').on('click', function () { window.open('?func=left',"_self");return false; }); +$$('#RIGHT').on('click', function () { window.open('?func=right',"_self");return false; }); +$$('#DOWN').on('click', function () { window.open('?func=down',"_self");return false; }); +$$('#UP').on('click', function () { window.open('?func=up',"_self");return false; }); +$$('#RECORD').on('click', function () { window.open('?func=record',"_self");return false; }); +$$('#RETAKE').on('click', function () { window.open('?func=retake',"_self");return false; }); +$$('#DELETE').on('click', function () { window.open('?func=delete',"_self");return false; }); +$$('#VIEW').on('click', function () { window.open('?func=view',"_self");return false; }); +$$('#MIDDLE').on('click', function () { window.open('?func=middle',"_self");return false; }); +</script> + diff --git a/startinterface.sh b/startinterface.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "Have fun!" > /dev/shm/vumeter +echo "For the lulz" > /dev/shm/interface +cd ./gui +./tarinagui.bin