119 changed files with 15614 additions and 2267 deletions
@ -0,0 +1,172 @@
|
||||
### 0.5.7 |
||||
* Video messages and Telescope |
||||
* Notifications about new logins |
||||
* Changelog is now received as a message |
||||
|
||||
### 0.5.6 |
||||
* Edit the text of your messages after sending them. This works across all Telegram chats, including groups and one-on-one conversations. Select a message and click 'Edit' or just press the up arrow button to edit your last message. |
||||
* Unsend Messages: retract any messages within 48 hours of sending them. Check out the [Telegram Blog](https://telegram.org/blog/unsend-and-usage) for more info. |
||||
* Pinned chats. Check out the [Telegram Blog](https://telegram.org/blog/pin-and-ifttt) for more info. |
||||
* Sticker suggestions by emoji. |
||||
* Search for messages in specific chats. |
||||
* Background notifications in Chrome and Firefox (can be disabled in Settings) |
||||
|
||||
### 0.5.5 |
||||
* Introducing Drafts: Seamless syncing for unsent messages on all your devices. Drafts are now visible in your chats list. |
||||
* Mention people in groups by typing @ and selecting them from the list — even if they don't have a username. |
||||
* Share links to specific posts in channels via quick forwarding menu (click on the date in a message to try this out). |
||||
|
||||
### 0.5.4 |
||||
* Introducing Bot API 2.0, the biggest update to our bot platform since June 2015. |
||||
* New inline keyboards with callback, 'open URL' or 'switch to inline mode' buttons help create seamless interfaces. |
||||
* Bots can now update existing messages on the fly as you interact with them. |
||||
* Prepare for the rise of location-based bots: all bots can now ask users to share their location. |
||||
* Inline bots can now send all attachments supported in Telegram (videos, music, stickers, files, etc.). |
||||
* Try out these sample bots to see what's coming your way soon: @music, @sticker, @youtube, @foursquare |
||||
* Check out the [Telegram Blog](https://telegram.org/blog/bots-2-0) for more info. |
||||
* New quick forwarding in channels (click on the date in a message to try this out). |
||||
* Improved performance. |
||||
|
||||
### 0.5.3 |
||||
* Inline bots: A new way to add bot content to any chat. Type a bot's username and your query in the text field to get instant results and send them to your chat partner. Try typing `@gif dog` in your next chat. Sample bots: @gif, @wiki, @bing, @vid, @bold. |
||||
* Check out the [Telegram Blog](https://telegram.org/blog/inline-bots) for more info. |
||||
* Improved GIFs: 20x faster sending and downloading, nice animated progress |
||||
* Click on message date to reply (or to forward from channels). |
||||
* Preview images before sending when pasting from clipboard. |
||||
* Improved formatting for copy-pasted history fragments (date, time and sender names inserted automatically). |
||||
|
||||
### 0.5.2 |
||||
* Unread counters for muted chats now colored in gray. |
||||
* Improved previews for sticker sets: Click on a sticker to view the whole set, click on stickers in a set to send right away, added a 'Share' button. |
||||
* Improved performance. |
||||
|
||||
### 0.5.1 |
||||
* Groups that have reached their capacity of 200 users can be upgraded to supergroups of up to 1,000 members. |
||||
* Check out the [Telegram Blog](https://telegram.org/blog/supergroups) for more info |
||||
|
||||
### 0.5.0 |
||||
* Removed annoying "multiple tabs open" error. |
||||
* Improved message forwarding. |
||||
* Added view counter to messages from channels. |
||||
* Improved image loading in Safari and Firefox. |
||||
|
||||
### 0.4.9 |
||||
* New emoji and sticker menu, tabs for sticker packs. |
||||
|
||||
### 0.4.8 |
||||
* Introducing Channels – a great new way to broadcast your messages to unlimited audiences. |
||||
* Check out the [Telegram Blog](https://telegram.org/blog/channels) for more info |
||||
* Improved performance in Safari on OS X El Capitan. |
||||
* Added formatting for fixed-width code, surround text with `single backticks` for inline text and ```triple backticks``` for blocks of pre-formatted text. |
||||
|
||||
### 0.4.7 |
||||
* New bot API, free for everyone. If you're an engineer, create your own bots for games, services or integrations. |
||||
|
||||
* Check out [Telegram Blog](https://telegram.org/blog/bot-revolution) for more info |
||||
* Improved Stickers support: now stickers are loading much faster. |
||||
* Click on any custom stickers in chats to view and add sticker sets. |
||||
* [Mobile] Reply to a message easily: tap on any message and select "Reply". |
||||
|
||||
### 0.4.6 |
||||
* Install and share custom sticker sets like this one: telegram.me/addstickers/Animals |
||||
* If you're an artist, create custom sticker sets using our @Stickers bot. |
||||
* Check out [Telegram Blog](https://telegram.org/blog/stickers-revolution) for more info |
||||
|
||||
### 0.4.5 |
||||
* Invite links for group chats: |
||||
Check out [Telegram Blog](https://telegram.org/blog/invite-links) for more info |
||||
* Smart notifications |
||||
* 'Listened' status for voice messages |
||||
* Places in locations (venues, landmarks) |
||||
|
||||
### 0.4.4 |
||||
* Link Previews: |
||||
Get rich link summaries for tweets, YouTube videos, Instagram photos and other content. |
||||
* Check out [Telegram Blog](https://telegram.org/blog/link-preview) for more info. |
||||
|
||||
### 0.4.3 |
||||
* Sessions List: |
||||
View your active Telegram sessions (on desktop, tablet and mobile devices) and close specific sessions remotely. |
||||
|
||||
* Two-step verification: |
||||
Set up an additional password that will be required to log into your Telegram account. |
||||
* Check out [Telegram Blog](https://telegram.org/blog/sessions-and-2-step-verification) for more info. |
||||
|
||||
### 0.4.2 |
||||
* Optimization for screens with smaller Y-resolutions. |
||||
* Supported Spotify URL embeds. |
||||
* Mentions of the current user in group chats are now highlighted. |
||||
|
||||
### 0.4.1 |
||||
* Reply to specific messages in groups. |
||||
* Mention @usernames in groups to notify multiple users. |
||||
* Revised notifications in groups: mentioned users and people you reply to will be notified (private chat notification settings apply in this case instead of group settings). Check out <a href="https://telegram.org/blog/replies-mentions-hashtags" target="_blank">Telegram Blog</a> for more info. |
||||
* Setting to disable message preview |
||||
|
||||
### 0.4.0 |
||||
* Full stickers support |
||||
* Multisearch box: instantly find chats, usernames and messages |
||||
* Emoji autocomplete: e.g., type **:kiss<** in the message field to see the list. [Full cheat sheet](http://www.emoji-cheat-sheet.com/) |
||||
* Added 'typing' notification in chats list |
||||
* Online members counter in group headers |
||||
|
||||
### 0.3.9 |
||||
* [Desktop] Material design completed |
||||
|
||||
### 0.3.8 |
||||
* Telegram.me links open right in Telegram Web when authorized |
||||
* @username mentions in messages are clickable and open a conversation with the user |
||||
|
||||
### 0.3.7 |
||||
* [Desktop] New material design for modal windows |
||||
* [Desktop] Forward messages to multiple recipients |
||||
|
||||
### 0.3.6 |
||||
* New viewer for photos, videos, documents. |
||||
* [FirefoxOS] Improved media downloads. |
||||
|
||||
### 0.3.5 |
||||
* Added embedded Soundcloud tracks and playlists. |
||||
* Added global user search to contacts list. |
||||
* Added switch to mobile version on window resize. |
||||
* Migrate to HTTPS notification |
||||
* Bugfixes. |
||||
|
||||
### 0.3.4 |
||||
* Added embedded Facebook posts and Vimeo videos. |
||||
* Improved IE10+ support: downloading files and style fixes. |
||||
* Added unsupported media playback warning. |
||||
* Bugfixes. |
||||
|
||||
### 0.3.3 |
||||
* Added Native Client module: dramatically improved encryption speed; Download and upload speed is now as high as in native applications. |
||||
* HTTPS. We recommend you to use https://web.telegram.org. |
||||
* Added multiple open tabs warning. Please note, that only one tab with Telegram Web will work. |
||||
* Added embedded Instagram, Twitter, Vine, YouTube links. |
||||
* Jump to selected spot when playing back audio. |
||||
* Bugfixes |
||||
|
||||
### 0.3.2 |
||||
* Usernames support. |
||||
* Search can now find public users by username. |
||||
* Most popular emoticons shown in 'recent' when empty |
||||
* [ChromeApp] Added saving window position and size |
||||
* Bugfixes |
||||
|
||||
### 0.3.1 |
||||
* New languages: Spanish, German and Italian are now available |
||||
* New custom-made audio player |
||||
* Bad browser page for IE 6-9 |
||||
* Perfomance improvements and bugfixes |
||||
|
||||
### 0.3.0 |
||||
* Log in codes may be received in other Telegram apps |
||||
* Partner's online status updates automatically |
||||
* Added support for non-english hashtags in messages |
||||
* Fixed invalid scrollbar width bug |
||||
* [Desktop] Added automatic country code detection |
||||
* [FirefoxOS] Improved PUSH-notifications for <= 1.1 |
||||
* [FirefoxOS] Fixed emoji in notifications |
||||
* [FirefoxOS] Fixed attachment bug for <= 1.1 |
||||
* [FirefoxOS] Added phonebook permissions handling |
||||
* [FirefoxOS] Added ability to share Gallery photos in Telegram |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,27 @@
|
||||
<a class="img_round_with_progress_wrap" ng-click="toggle($event)"> |
||||
|
||||
<div class="img_round_image_wrap" ng-style="::{width: document.thumb.width + 'px'}"> |
||||
|
||||
<div class="img_round_meta" ng-show="!isActive" ng-switch="document.progress.enabled"> |
||||
<div ng-switch-when="true" class="img_round_meta_contents"> |
||||
<i class="icon icon-cancel"> |
||||
<i class="icon icon-bar"></i> |
||||
<i class="icon icon-bar"></i> |
||||
</i> |
||||
<div my-arc-progress="document.progress.percent"></div> |
||||
</div> |
||||
|
||||
<div ng-switch-default class="img_round_meta_contents"><i class="icon icon-videoplay"></i></div> |
||||
</div> |
||||
|
||||
<div ng-if="document.url" ng-show="document.downloaded && isActive" ng-switch="document.mime_type == 'video/mp4'"> |
||||
<video ng-switch-when="true" width="{{document.thumb.width}}" height="{{document.thumb.height}}" autoplay class="img_round_video"> |
||||
<source ng-src="{{document.url}}" type="video/mp4"> |
||||
</video> |
||||
<img ng-switch-default class="img_round_image" ng-src="{{document.url}}" width="{{document.thumb.width}}" height="{{document.thumb.height}}" /> |
||||
</div> |
||||
<img ng-hide="document.downloaded && isActive" class="img_round_thumb" my-load-thumb thumb="document.thumb" /> |
||||
|
||||
</div> |
||||
|
||||
</a> |
@ -0,0 +1,68 @@
|
||||
<form class="im_send_form" ng-class="{im_send_form_empty: !draftMessage.text.length, composer_progress_enabled: draftMessage.inlineProgress, im_voice_recording: voiceRecorder.recording, im_processing_recording: voiceRecorder.processing}"> |
||||
|
||||
<div class="im_send_form_inline_results" my-inline-results="inlineResults"></div> |
||||
|
||||
<div class="im_send_reply_wrap" ng-if="draftMessage.replyToMsgID > 0"> |
||||
<a class="im_send_reply_cancel" ng-mousedown="draftMessage.replyClear(true)"><i class="icon icon-reply-bar"></i><i class="icon icon-reply-bar"></i></a> |
||||
<a class="im_message_reply_wrap" my-reply-message="draftMessage.replyToMsgID" watch="true" edit="{{draftMessage.type == 'edit'}}"></a> |
||||
</div> |
||||
|
||||
<div class="im_send_reply_wrap im_send_fwds_wrap" ng-if="draftMessage.fwdMessages.length > 0"> |
||||
<a class="im_send_reply_cancel" ng-mousedown="draftMessage.fwdsClear()"><i class="icon icon-reply-bar"></i><i class="icon icon-reply-bar"></i></a> |
||||
<div class="im_message_reply_wrap" my-forwarded-messages="draftMessage.fwdMessages"></div> |
||||
</div> |
||||
|
||||
<div class="im_send_field_wrap hasselect" ng-class="replyKeyboard._ == 'replyKeyboardMarkup' ? 'im_send_field_wrap_2ndbtn' : ''"> |
||||
<a class="composer_emoji_insert_btn"><i class="icon icon-emoji"></i></a> |
||||
<div class="composer_progress_icon_wrap"> |
||||
<div class="composer_progress_icon" my-arc-progress width="22" stroke="2.5"></div> |
||||
</div> |
||||
<a class="composer_command_btn" ng-show="!replyKeyboard && commands.list.length > 0 && (!draftMessage.text.length || draftMessage.text == '/')" ng-mousedown="draftMessage.toggleSlash($event)" ng-class="draftMessage.text[0] == '/' ? 'active' : ''"><i class="icon icon-slash"></i></a> |
||||
<a class="composer_keyboard_btn" ng-show="replyKeyboard._ == 'replyKeyboardMarkup'" ng-mousedown="draftMessage.replyKeyboardToggle($event)" ng-class="!replyKeyboard.pFlags.hidden ? 'active' : ''"><i class="icon icon-keyboard"></i></a> |
||||
|
||||
<div class="im_send_dropbox_wrap" my-i18n="im_photos_drop_text"></div> |
||||
<div class="im_voice_recorder_wrap"> |
||||
<div class="im_recorder_indicator"><i></i></div> |
||||
<div class="im_recorder_time" ng-bind="voiceRecorder.duration | duration"></div> |
||||
<div class="im_recorder_label" ng-switch="voiceRecorder.processing"> |
||||
<span ng-switch-when="true" my-i18n="im_voice_processing_label"> |
||||
<my-i18n-param name="dots"></my-i18n-param> |
||||
</span> |
||||
<span ng-switch-default> |
||||
<span class="im_recorder_label_hover" my-i18n="im_voice_recording_label"></span> |
||||
<span class="im_recorder_label_hout" my-i18n="im_voice_recording_cancel_label"></span> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<textarea ng-model="draftMessage.text" class="form-control im_message_field no_outline" dir="auto" ng-trim="false"></textarea> |
||||
</div> |
||||
|
||||
<div class="im_send_buttons_wrap clearfix"> |
||||
<button type="submit" class="btn btn-md im_submit" ng-class="draftMessage.type == 'edit' ? 'im_submit_edit' : 'im_submit_send'"> |
||||
<span class="im_submit_send_label nocopy" my-i18n="im_submit_message"></span> |
||||
<span class="im_submit_edit_label nocopy" my-i18n="im_submit_edit_message"></span> |
||||
</button> |
||||
|
||||
<div class="im_attach pull-left"> |
||||
<input type="file" class="im_attach_input" size="28" multiple="multiple" title="{{'im_attach_file_title' | i18n}}" /> |
||||
<i class="icon icon-paperclip"></i> |
||||
</div> |
||||
|
||||
<div class="im_media_attach pull-left"> |
||||
<input type="file" class="im_media_attach_input" size="28" multiple="multiple" accept="image/*, video/*, audio/*" title="{{'im_media_attach_title' | i18n}}"/> |
||||
<i class="icon icon-camera"></i> |
||||
</div> |
||||
|
||||
<a class="im_record pull-left"> |
||||
<i class="icon icon-mic"></i> |
||||
</a> |
||||
|
||||
<div class="composer_emoji_panel"></div> |
||||
</div> |
||||
|
||||
<div class="im_send_keyboard_wrap" ng-if="replyKeyboard._ == 'replyKeyboardMarkup'" ng-show="!replyKeyboard.pFlags.hidden"> |
||||
<div my-reply-markup="replyKeyboard"></div> |
||||
</div> |
||||
|
||||
</form> |
@ -0,0 +1,59 @@
|
||||
<form class="im_send_form" ng-class="{im_send_form_empty: !draftMessage.text.length && draftMessage.type != 'edit', composer_progress_enabled: draftMessage.inlineProgress, im_voice_recording: voiceRecorder.recording, im_processing_recording: voiceRecorder.processing}"> |
||||
|
||||
<div class="im_send_reply_wrap" ng-if="draftMessage.replyToMsgID > 0"> |
||||
<a class="im_send_reply_cancel" ng-mousedown="draftMessage.replyClear(true)"><i class="icon icon-reply-bar"></i><i class="icon icon-reply-bar"></i></a> |
||||
<a class="im_message_reply_wrap" my-reply-message="draftMessage.replyToMsgID" watch="true" edit="{{draftMessage.type == 'edit'}}"></a> |
||||
</div> |
||||
|
||||
<div class="im_send_reply_wrap im_send_fwds_wrap" ng-if="draftMessage.fwdMessages.length > 0"> |
||||
<a class="im_send_reply_cancel" ng-mousedown="draftMessage.fwdsClear()"><i class="icon icon-reply-bar"></i><i class="icon icon-reply-bar"></i></a> |
||||
<div class="im_message_reply_wrap" my-forwarded-messages="draftMessage.fwdMessages"></div> |
||||
</div> |
||||
|
||||
<div class="im_send_field_panel"> |
||||
|
||||
<div class="im_voice_recorder_wrap"> |
||||
<div class="im_recorder_indicator"><i></i></div> |
||||
<div class="im_recorder_time" ng-bind="voiceRecorder.duration | duration"></div> |
||||
<div class="im_recorder_label" ng-switch="voiceRecorder.processing"> |
||||
<span ng-switch-when="true" my-i18n="im_voice_processing_label"> |
||||
<my-i18n-param name="dots"></my-i18n-param> |
||||
</span> |
||||
<span ng-switch-default> |
||||
<span class="im_recorder_label_hover" my-i18n="im_voice_recording_label"></span> |
||||
<span class="im_recorder_label_hout" my-i18n="im_voice_recording_cancel_label"></span> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="im_send_field_wrap" ng-class="replyKeyboard._ == 'replyKeyboardMarkup' ? 'im_send_field_wrap_2ndbtn' : ''"> |
||||
<a class="composer_emoji_insert_btn pull-right"><i class="icon icon-emoji"></i></a> |
||||
<a class="composer_command_btn" ng-show="!replyKeyboard && commands.list.length > 0 && (!draftMessage.text.length || draftMessage.text[0] == '/')" ng-mousedown="draftMessage.toggleSlash($event)" ng-class="draftMessage.text[0] == '/' ? 'active' : ''"><i class="icon icon-slash"></i></a> |
||||
<a class="composer_keyboard_btn" ng-show="replyKeyboard._ == 'replyKeyboardMarkup'" ng-mousedown="draftMessage.replyKeyboardToggle($event)" ng-class="!replyKeyboard.pFlags.hidden ? 'active' : ''"><i class="icon icon-keyboard"></i></a> |
||||
|
||||
<div class="composer_progress_icon_wrap"> |
||||
<div class="composer_progress_icon" my-arc-progress width="22" stroke="2.5"></div> |
||||
</div> |
||||
|
||||
<div class="im_send_dropbox_wrap" my-i18n="im_photos_drop_text"></div> |
||||
<textarea ng-model="draftMessage.text" class="form-control im_message_field no_outline" dir="auto" ng-trim="false"></textarea> |
||||
</div> |
||||
|
||||
<div class="im_attach pull-left"> |
||||
<input type="file" class="im_attach_input" size="28" multiple="true" title="{{'im_media_attach_title' | i18n}}" /> |
||||
<i class="icon icon-paperclip"></i> |
||||
</div> |
||||
|
||||
<div class="im_record pull-right"> |
||||
<i class="icon icon-mic"></i> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-success im_submit"></button> |
||||
</div> |
||||
|
||||
|
||||
<div class="im_send_keyboard_wrap" ng-if="replyKeyboard._ == 'replyKeyboardMarkup'" ng-show="!replyKeyboard.pFlags.hidden"> |
||||
<div my-reply-markup="replyKeyboard"></div> |
||||
</div> |
||||
|
||||
</form> |
@ -0,0 +1,21 @@
|
||||
ogv.js & ogv.swf wrapper and player code |
||||
|
||||
Copyright (c) 2013-2014 Brion Vibber and other contributors |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
@ -0,0 +1,28 @@
|
||||
Copyright (c) 2002, Xiph.org Foundation |
||||
|
||||
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 Xiph.org Foundation 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 FOUNDATION |
||||
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. |
@ -0,0 +1,44 @@
|
||||
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, |
||||
Jean-Marc Valin, Timothy B. Terriberry, |
||||
CSIRO, Gregory Maxwell, Mark Borgerding, |
||||
Erik de Castro Lopo |
||||
|
||||
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 Internet Society, IETF or IETF Trust, nor the |
||||
names of specific 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 OWNER |
||||
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. |
||||
|
||||
Opus is subject to the royalty-free patent licenses which are |
||||
specified at: |
||||
|
||||
Xiph.Org Foundation: |
||||
https://datatracker.ietf.org/ipr/1524/ |
||||
|
||||
Microsoft Corporation: |
||||
https://datatracker.ietf.org/ipr/1914/ |
||||
|
||||
Broadcom Corporation: |
||||
https://datatracker.ietf.org/ipr/1526/ |
@ -0,0 +1,28 @@
|
||||
Copyright (C) 2002-2009 Xiph.org Foundation |
||||
|
||||
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 Xiph.org Foundation 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 FOUNDATION |
||||
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. |
@ -0,0 +1,28 @@
|
||||
Copyright (c) 2002-2008 Xiph.org Foundation |
||||
|
||||
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 Xiph.org Foundation 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 FOUNDATION |
||||
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. |
@ -0,0 +1,13 @@
|
||||
Copyright © 2010 Mozilla Foundation |
||||
|
||||
Permission to use, copy, modify, and distribute this software for any |
||||
purpose with or without fee is hereby granted, provided that the above |
||||
copyright notice and this permission notice appear in all copies. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
@ -0,0 +1,31 @@
|
||||
Copyright (c) 2010, The WebM Project authors. 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 Google, nor the WebM Project, 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. |
||||
|
@ -0,0 +1,23 @@
|
||||
Additional IP Rights Grant (Patents) |
||||
------------------------------------ |
||||
|
||||
"These implementations" means the copyrightable works that implement the WebM |
||||
codecs distributed by Google as part of the WebM Project. |
||||
|
||||
Google 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, transfer, and otherwise |
||||
run, modify and propagate the contents of these implementations of WebM, where |
||||
such license applies only to those patent claims, both currently owned by |
||||
Google and acquired in the future, licensable by Google that are necessarily |
||||
infringed by these implementations of WebM. This grant does not include claims |
||||
that would be infringed only as a consequence of further modification of these |
||||
implementations. If you or your agent or exclusive licensee institute or order |
||||
or agree to the institution of patent litigation or any other patent |
||||
enforcement activity against any entity (including a cross-claim or |
||||
counterclaim in a lawsuit) alleging that any of these implementations of WebM |
||||
or any code incorporated within any of these implementations of WebM |
||||
constitute direct or contributory patent infringement, or inducement of |
||||
patent infringement, then any patent rights granted to you under this License |
||||
for these implementations of WebM shall terminate as of the date such |
||||
litigation is filed. |
@ -0,0 +1,371 @@
|
||||
ogv.js |
||||
====== |
||||
|
||||
Media decoder and player for Ogg Vorbis/Opus/Theora and (experimentally) WebM video. |
||||
|
||||
Based around libogg, libvorbis, libtheora, libopus, libvpx, and libnestegg compiled to JavaScript with Emscripten. |
||||
|
||||
## Updates |
||||
|
||||
1.4.2 - 2017-04-24 |
||||
* support 8-bit 4:2:2 and 4:4:4 subsampling in VP9 |
||||
|
||||
1.4.1 - 2017-04-07 |
||||
* fix for seek shortly after initialization |
||||
* fix for some missing instance constants |
||||
|
||||
1.4.0 - 2017-04-06 |
||||
* fastSeek() is now fast; seeks to first keyframe found. |
||||
* VP9 base profile support in WebM container (8-bit 4:2:0 only). |
||||
* Safari no longer complains about missing es6-promise.map source map |
||||
* Smoother playback on low-end machines prone to lag spikes: when A/V sync lags, keep audio running smoothly and resync video at the next keyframe. To restore previous behavior, set `sync: 'delay-audio'` in options. |
||||
* Experimental Web Assembly builds of all modules; set `wasm: true` in options to force on. |
||||
* `error` property now returns an `OGVMediaError` object instead of string. |
||||
* Decode pipeline up to 3 frames deep to aid in momentary spikes. |
||||
* Experimental multithreaded JS builds for VP8 and VP9; set `threading: true` in options to force on. |
||||
* Fixed bad autodetection of files in root dir |
||||
|
||||
1.3.1 - 2017-02-24 |
||||
* Fix for seeking before load completes |
||||
* Fix for bisection seeking in very short Ogg files |
||||
|
||||
1.3.0 - 2017-02-08 |
||||
* Separated XHR and caching out to stream-file package |
||||
* more aggressive in-memory buffering should improve audio seek performance |
||||
* improved seek precision on audio files |
||||
* fix for Ogg files with stream id of 0 |
||||
|
||||
1.2.1 - 2016-09-24 |
||||
* Performance fixed for playback of Ogg Theora with many duplicate frames ("1000fps" files from ffmpeg) |
||||
* Report actual fps (ignoring dupe frames) for Ogg Theora |
||||
* Delay loading when using preload="none" |
||||
* Fix regression in IE 10 network layer |
||||
|
||||
1.2.0 - 2016-09-19 |
||||
* Separated software and WebGL paths to yuv-canvas package |
||||
* fixed regression in WebM frame rate handling |
||||
* buffer up to 3 decoded frames for smoother playback |
||||
* smoother audio in the face of short delays (drop late frame if next one is already decoded) |
||||
* fixed regression in seeking non-indexed Ogg files |
||||
* updated libvpx |
||||
|
||||
1.1.3 - 2016-06-27 |
||||
* fix play-during-seek bug that interacted with video.js badly |
||||
|
||||
1.1.2 - 2016-06-27 |
||||
* better a/v sync |
||||
* muted autoplay works on iOS |
||||
* numerous seeking-related race-condition fixes |
||||
* more consistent performance on low-end machines |
||||
* supports cross-domain hosting of worker and Flash audio shim |
||||
* seeking now works in WebM as well as Ogg |
||||
* cleaner multithreading |
||||
* lots of little fixes |
||||
|
||||
See more details and history in [CHANGES.md](https://github.com/brion/ogv.js/blob/master/CHANGES.md) |
||||
|
||||
## Current status |
||||
|
||||
Since August 2015, ogv.js can be seen in action [on Wikipedia and Wikimedia Commons](https://commons.wikimedia.org/wiki/Commons:Video) in Safari and IE/Edge where native Ogg and WebM playback is not available. (See [technical details on MediaWiki integration](https://www.mediawiki.org/wiki/Extension:TimedMediaHandler/ogv.js).) |
||||
|
||||
See also a standalone demo with performance metrics at https://brionv.com/misc/ogv.js/demo/ |
||||
|
||||
* streaming: yes (with Range header) |
||||
* seeking: yes for Ogg and WebM (with Range header) |
||||
* color: yes |
||||
* audio: yes, with a/v sync (requires Web Audio or Flash) |
||||
* background threading: yes (video, audio decoders in Workers) |
||||
* [GPU accelerated drawing: yes (WebGL)](https://github.com/brion/ogv.js/wiki/GPU-acceleration) |
||||
* GPU accelerated decoding: no |
||||
* SIMD acceleration: no |
||||
* Web Assembly: yes (experimental; set `options.wasm` to `true`) |
||||
* multithreaded VP8, VP9: yes (experimental; set `options.threading` to `true`; requires `SharedArrayBuffer`) |
||||
* controls: no (currently provided by demo or other UI harness) |
||||
|
||||
Ogg files are fairly well supported, but WebM is still experimental and is disabled by default. |
||||
|
||||
|
||||
## Goals |
||||
|
||||
Long-form goal is to create a drop-in replacement for the HTML5 video and audio tags which can be used for basic playback of Ogg Theora and Vorbis or WebM media on browsers that don't support Ogg or WebM natively. |
||||
|
||||
The API isn't quite complete, but works pretty well. |
||||
|
||||
|
||||
## Compatibility |
||||
|
||||
ogv.js requires a fast JS engine with typed arrays, and either Web Audio or Flash for audio playback. |
||||
|
||||
The primary target browsers are (testing 360p/30fps and up): |
||||
* Safari 6.1/7/8/9/10 on Mac OS X 10.7-10.11 |
||||
* Safari on iOS 8/9/10 64-bit |
||||
* Edge on Windows 10 desktop/tablet |
||||
* Internet Explorer 10/11 on Windows 7/8/8.1/10 (desktop/tablet) |
||||
|
||||
And for lower-resolution files (testing 160p/15fps): |
||||
* Safari on iOS 8/9/10 32-bit |
||||
* Edge on Windows 10 Mobile |
||||
* Internet Explorer 10/11 on Windows RT |
||||
|
||||
Older versions of Safari have flaky JIT compilers. IE 9 and below lack typed arrays. |
||||
|
||||
(Note that Windows and Mac OS X can support Ogg and WebM by installing codecs or alternate browsers with built-in support, but this is not possible on iOS, Windows RT, or Windows 10 Mobile.) |
||||
|
||||
Testing browsers (these support .ogv and .webm natively): |
||||
* Firefox 52 |
||||
* Chrome 57 |
||||
|
||||
|
||||
## Package installation |
||||
|
||||
Pre-built releases of ogv.js are available as [.zip downloads from the GitHub releases page](https://github.com/brion/ogv.js/releases) and through the npm package manager. |
||||
|
||||
You can load the `ogv.js` main entry point directly in a script tag, or bundle it through whatever build process you like. The other .js files and the .swf file (for audio in IE) must be made available for runtime loading, together in the same directory. |
||||
|
||||
ogv.js will try to auto-detect the path to its resources based on the script element that loads ogv.js or ogv-support.js. If you load ogv.js through another bundler (such as browserify or MediaWiki's ResourceLoader) you may need to override this manually before instantiating players: |
||||
|
||||
``` |
||||
// Path to ogv-demuxer-ogg.js, ogv-worker-audio.js, dynamicaudio.swf etc |
||||
OGVLoader.base = '/path/to/resources'; |
||||
``` |
||||
|
||||
To fetch from npm: |
||||
|
||||
``` |
||||
npm install ogv |
||||
``` |
||||
|
||||
The distribution-ready files will appear in 'node_modules/ogv/dist'. |
||||
|
||||
To load the player library into your browserify or webpack project: |
||||
|
||||
``` |
||||
var ogv = require('ogv'); |
||||
|
||||
// Access public classes either as ogv.OGVPlayer or just OGVPlayer. |
||||
// Your build/lint tools may be happier with ogv.OGVPlayer! |
||||
ogv.OGVLoader.base = '/path/to/resources'; |
||||
var player = new ogv.OGVPlayer(); |
||||
``` |
||||
|
||||
## Usage |
||||
|
||||
The `OGVPlayer` class implements a player, and supports a subset of the events, properties and methods from [HTMLMediaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) and [HTMLVideoElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement). |
||||
|
||||
``` |
||||
// Create a new player with the constructor |
||||
var player = new OGVPlayer(); |
||||
|
||||
// Or with options |
||||
var player = new OGVPlayer({ |
||||
enableWebM: true |
||||
}); |
||||
|
||||
// Now treat it just like a video or audio element |
||||
containerElement.appendChild(player); |
||||
player.src = 'path/to/media.ogv'; |
||||
player.play(); |
||||
player.addEventListener('ended', function() { |
||||
// ta-da! |
||||
}); |
||||
``` |
||||
|
||||
To check for compatibility before creating a player, include `ogv-support.js` and use the `OGVCompat` API: |
||||
|
||||
``` |
||||
if (OGVCompat.supported('OGVPlayer')) { |
||||
// go load the full player from ogv.js and instantiate stuff |
||||
} |
||||
``` |
||||
|
||||
This will check for typed arrays, audio/Flash, blacklisted iOS versions, and super-slow/broken JIT compilers. |
||||
|
||||
If you need a URL versioning/cache-buster parameter for dynamic loading of `ogv.js`, you can use the `OGVVersion` symbol provided by `ogv-support.js` or the even tinier `ogv-version.js`: |
||||
|
||||
``` |
||||
var script = document.createElement('script'); |
||||
script.src = 'ogv.js?version=' + encodeURIComponent(OGVVersion); |
||||
document.querySelector('head').appendChild(script); |
||||
``` |
||||
|
||||
## Distribution notes |
||||
|
||||
Entry points: |
||||
* `ogv.js` contains the main runtime classes, including OGVPlayer, OGVLoader, and OGVCompat. |
||||
* `ogv-support.js` contains the OGVCompat class and OGVVersion symbol, useful for checking for runtime support before loading the main `ogv.js`. |
||||
* `ogv-version.js` contains only the OGVVersion symbol. |
||||
|
||||
These entry points may be loaded directly from a script element, or concatenated into a larger project, or otherwise loaded as you like. |
||||
|
||||
Further code modules are loaded at runtime, which must be available with their defined names together in a directory. If the files are not hosted same-origin to the web page that includes them, you will need to set up appropriate CORS headers to allow loading of the worker JS modules. |
||||
|
||||
Dynamically loaded assets: |
||||
* `ogv-worker-audio.js`, `ogv-worker-video.js`, and `pthread-main.js` are Worker entry points, used to run video and audio decoders in the background. |
||||
* `ogv-demuxer-ogg.js` is used in playing .ogg, .oga, and .ogv files. |
||||
* `ogv-demuxer-webm.js` is used in playing .webm files. |
||||
* `ogv-decoder-audio-vorbis.js` and `ogv-decoder-audio-opus.js` are used in playing both Ogg and WebM files containing audio. |
||||
* `ogv-decoder-video-theora.js` is used in playing .ogg and .ogv video files. |
||||
* `ogv-decoder-video-vp8.js` and `ogv-decoder-video-vp9.js` are used in playing .webm video files. |
||||
* `*-wasm.js` and `*-wasm.wasm` files are the Web Assembly versions of the above modules. |
||||
* `*-mt.js` are the multithreaded versions of some of the above modules. |
||||
* `dynamicaudio.swf` is the Flash audio shim, used for Internet Explorer 10/11. |
||||
|
||||
If you know you will never use particular formats or codecs you can skip bundling them; for instance if you only need to play Ogg files you don't need `ogv-demuxer-webm.js` or `ogv-decoder-video-vp8.js` which are only used for WebM. Web Assembly and multithreaded modules are experimental and can be left out if not enabled in your runtime options. |
||||
|
||||
|
||||
## Performance |
||||
|
||||
As of 2015, for SD-or-less resolution basic Ogg Theora decoding speed is reliable on desktop and newer high-end mobile devices; current high-end desktops and laptops can even reach HD resolutions. Older and low-end mobile devices may have difficulty on any but audio and the lowest-resolution video files. |
||||
|
||||
WebM is much slower, and remains experimental. |
||||
|
||||
*Low-res targets* |
||||
|
||||
I've gotten acceptable performance for Vorbis audio and 160p/15fps Theora files on 32-bit iOS devices: iPhone 4s, iPod Touch 5th-gen and iPad 3. These have difficulty at 240p and above, and just won't keep up with higher resolutions. |
||||
|
||||
Meanwhile, newer 64-bit iPhones and iPads are comparable to low-end laptops, and videos at 360p and often 480p play acceptably. Since 32-bit and 64-bit iOS devices have the same user-agent, a benchmark must be used to approximately test minimum CPU speed. |
||||
|
||||
(On iOS, Safari performs significantly better than some alternative browsers that are unable to enable the JIT due to use of the old UIWebView API. Chrome 49 and Firefox for iOS are known to work using the newer WKWebView API internally. Again, a benchmark must be used to detect slow performance, as the browser remains otherwise compatible.) |
||||
|
||||
|
||||
Windows on 32-bit ARM platforms is similar... IE 11 on Windows RT 8.1 on a Surface tablet (NVidia Tegra 3), and Edge on Windows 10 Mobile build 10166 on a Lumia 635, perform acceptably with audio and with 160p/15fps videos but have trouble starting around 240p. |
||||
|
||||
|
||||
In both cases, a native application looms as a possibly better alternative. See [OGVKit ](https://github.com/brion/OGVKit) and [OgvRt](https://github.com/brion/OgvRT) projects for experiments in those directions. |
||||
|
||||
|
||||
Note that at these lower resolutions, Vorbis audio and Theora video decoding are about equally expensive operations -- dual-core phones and tablets should be able to eek out a little parallelism here thanks to audio and video being in separate Worker threads. |
||||
|
||||
|
||||
*WebGL drawing acceleration* |
||||
|
||||
Accelerated YCbCr->RGB conversion and drawing is done using WebGL on supporting browsers, or through software CPU conversion if not. This is abstracted in the [yuv-canvas](https://github.com/brion/yuv-canvas) package, now separately installable. |
||||
|
||||
It may be possible to do further acceleration of actual decoding operations using WebGL shaders, but this could be ... tricky. WebGL is also only available on the main thread, and there are no compute shaders yet so would have to use fragment shaders. |
||||
|
||||
|
||||
## Difficulties |
||||
|
||||
*Threading* |
||||
|
||||
Currently the video and audio codecs run in worker threads by default, while the demuxer |
||||
and player logic run on the UI thread. This seems to work pretty well. |
||||
|
||||
There is some overhead in extracting data out of each emscripten module's heap and in the thread-to-thread communications, but the parallelism and smoother main thread makes up for it. |
||||
|
||||
*Streaming download* |
||||
|
||||
In Firefox, the 'moz-chunked-array' responseType on XHR is used to read data as ArrayBuffer chunks during download. Safari and Chrome use a 'binary string' read which requires manually converting input to ArrayBuffer chunks. In IE and Edge have an (MS-prefixed) Stream/StreamReader interface which can be used to read data on demand into ArrayBuffer objects, but it has proved problematic especially with intermediate proxies; as of 1.1.2 IE and Edge use the same chunked binary-string method as Safari and Chrome. |
||||
|
||||
The Firefox and Safari/Chrome cases have been hacked up to do streaming buffering by chunking the requests at up to a megabyte each, using the HTTP Range header. For cross-site playback, this requires CORS setup to whitelist the Range header! |
||||
|
||||
[Safari has a bug with Range headers](https://bugs.webkit.org/show_bug.cgi?id=82672) which is worked around as necessary with a 'cache-busting' URL string parameter. Hopefully this will be fixed in future versions of Mac OS X and iOS. |
||||
|
||||
|
||||
*Seeking* |
||||
|
||||
Seeking is implemented via the HTTP Range: header. |
||||
|
||||
For Ogg files with keyframe indices in a skeleton index, seeking is very fast. Otherwise, a bisection search is used to locate the target frame or audio position, which is very slow over the internet as it creates a lot of short-lived HTTP requests. |
||||
|
||||
For WebM files with cues, efficient seeking is supported as well as of 1.1.2. |
||||
|
||||
As with chunked streaming, cross-site playback requires CORS support for the Range header. |
||||
|
||||
|
||||
*Audio output* |
||||
|
||||
Audio output is handled through the [AudioFeeder](https://github.com/brion/audio-feeder) library, which encapsulates use of Web Audio API or Flash depending on browser support: |
||||
|
||||
Firefox, Safari, Chrome, and Edge support the W3C Web Audio API. |
||||
|
||||
IE doesn't support Web Audio, but does bundle the Flash player in Windows 8/8.1/RT. A small Flash shim is included here and used as a fallback -- thanks to Maik Merten for hacking some pieces together and getting this working! |
||||
|
||||
A/V synchronization is performed on files with both audio and video, and seems to |
||||
actually work. Yay! |
||||
|
||||
Note that autoplay with audio doesn't work on iOS Safari due to limitations with starting audio playback from event handlers; if playback is started outside an event handler, the player will hang due to broken audio. |
||||
|
||||
As of 1.1.1, muting before script-triggered playback allows things to work: |
||||
|
||||
``` |
||||
player = new OGVPlayer(); |
||||
player.muted = true; |
||||
player.src = 'path/to/file-with-audio.ogv'; |
||||
player.play(); |
||||
``` |
||||
|
||||
You can then unmute the video in response to a touch or click handler. Alternately if audio is not required, do not include an audio track in the file. |
||||
|
||||
|
||||
*WebM* |
||||
|
||||
WebM support was added in June 2015, with some major issues finally worked out in May 2016. Initial VP9 support was added in February 2017. It remains experimental, but should be fully enabled in the future once a few more bugs are worked out. |
||||
|
||||
To enable, set `enableWebM: true` in your `options` array. |
||||
|
||||
Beware that performance of WebM VP8 is much slower than Ogg Theora, and VP9 is slower still. |
||||
|
||||
For best WebM decode speed, consider encoding VP8 with "profile 1" (simple deblocking filter) which will sacrifice quality modestly, mainly in high-motion scenes. When encoding with ffmpeg, this is the `-profile:v 1` option to the `libvpx` codec. |
||||
|
||||
It is also recommended to use the `-slices` option for VP8, or `-tile-columns` for VP9, to maximize ability to use multithreaded decoding when available. |
||||
|
||||
|
||||
## Upstream library notes |
||||
|
||||
We've experimented with tremor (libivorbis), an integer-only variant of libvorbis. This actually does *not* decode faster, but does save about 200kb off our generated JavaScript, presumably thanks to not including an encoder in the library. However on slow devices like iPod Touch 5th-generation, it makes a significant negative impact on the decode time so we've gone back to libvorbis. |
||||
|
||||
The Ogg Skeleton library (libskeleton) is a bit ... unfinished and is slightly modified here. |
||||
|
||||
libvpx is slightly modified to work around emscripten threading limitations in the VP8 decoder. |
||||
|
||||
|
||||
## Web Assembly |
||||
|
||||
Experimental Web Assembly (WASM) versions of the emscripten cross-compiled modules are also included, used if `options.wasm` is true. |
||||
|
||||
The WASM versions of the modules are more compact than the cross-compiled asm.js-style JavaScript, and should download and parse faster. Some browsers may also compile the module differently, providing more consistent performance at the beginning of playback. |
||||
|
||||
Currently Firefox and Chrome are the only release versions of browsers that support Web Assembly, but it's available in Safari Technical Preview and behind the 'experimental JS options' flag in Edge in Windows 10 version 1703. |
||||
|
||||
If you are making a slim build and will not use the `wasm` option, you can leave out the `*-wasm.js` and `*-wasm.wasm` files. |
||||
|
||||
|
||||
## Multithreading |
||||
|
||||
Experimental multithreaded VP8 and VP9 decoding up to 4 cores is available for VP8 and VP9 video, used if `options.threading` is true. This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, currently available in Safari 10.1 / iOS 10.3 and in Firefox developer & nightly builds, and in Chrome behind a flag. |
||||
|
||||
Threading is not currently compatible with Web Assembly. |
||||
|
||||
Speedups will only be noticeable when using the "slices" or "token partitions" option for VP8 encoding, or the "tile columns" option for VP9 encoding. |
||||
|
||||
Currently, getting a successful multithreaded build requires a [patch to the emscripten compiler](https://github.com/kripken/emscripten/pull/5016); without this patch, the resulting multithreaded modules will build but fail to initialize correctly. |
||||
|
||||
If you are making a slim build and will not use the `threading` option, you can leave out the `*-mt.js` files, as well as `pthread-main.js`. |
||||
|
||||
|
||||
## Building JS components |
||||
|
||||
Building ogv.js is known to work on Mac OS X and Linux (tested Ubuntu 15.04). |
||||
|
||||
1. You will need autoconf, automake, libtool, pkg-config, and node (nodejs). These can be installed through Homebrew on Mac OS X, or through distribution-specific methods on Linux. |
||||
2. Install [Emscripten](http://kripken.github.io/emscripten-site/docs/getting_started/Tutorial.html); currently using the incoming branch (of what will be 1.38) for distribution builds for latest WASM support, plus [multithreading patch](https://github.com/kripken/emscripten/pull/5016) |
||||
3. `git submodule update --init` |
||||
4. Run `npm install` to install build utilities |
||||
5. Run `make js` to configure and build the libraries and the C wrapper |
||||
|
||||
|
||||
## Building the demo |
||||
|
||||
If you did all the setup above, just run `make demo` or `make`. Look in build/demo/ and enjoy! |
||||
|
||||
|
||||
## License |
||||
|
||||
libogg, libvorbis, libtheora, libopus, nestegg, and libvpx are available under their respective licenses, and the JavaScript and C wrapper code in this repo is licensed under MIT. |
||||
|
||||
Based on build scripts from https://github.com/devongovett/ogg.js |
||||
|
||||
[AudioFeeder](https://github.com/brion/audio-feeder)'s dynamicaudio.as and other Flash-related bits are based on code under BSD license, (c) 2010 Ben Firshman. |
||||
|
||||
See [AUTHORS.md](https://github.com/brion/ogv.js/blob/master/AUTHORS.md) and/or the git history for a list of contributors. |
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -0,0 +1,272 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {}; |
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) { |
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) |
||||
/******/ return installedModules[moduleId].exports; |
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = { |
||||
/******/ exports: {}, |
||||
/******/ id: moduleId, |
||||
/******/ loaded: false |
||||
/******/ }; |
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true; |
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports; |
||||
/******/ } |
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules; |
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules; |
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = ""; |
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0); |
||||
/******/ }) |
||||
/************************************************************************/ |
||||
/******/ ([ |
||||
/* 0 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
//
|
||||
// -- ogv-support.js
|
||||
// https://github.com/brion/ogv.js
|
||||
// Copyright (c) 2013-2016 Brion Vibber
|
||||
//
|
||||
|
||||
(function() { |
||||
|
||||
var OGVCompat = __webpack_require__(1), |
||||
OGVVersion = ("1.4.2-20170425024925-504d7197"); |
||||
|
||||
if (window) { |
||||
// 1.0-compat globals
|
||||
window.OGVCompat = OGVCompat; |
||||
window.OGVVersion = OGVVersion; |
||||
} |
||||
|
||||
module.exports = { |
||||
OGVCompat: OGVCompat, |
||||
OGVVersion: OGVVersion |
||||
}; |
||||
|
||||
})(); |
||||
|
||||
|
||||
/***/ }), |
||||
/* 1 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var BogoSlow = __webpack_require__(2); |
||||
|
||||
var OGVCompat = { |
||||
benchmark: new BogoSlow(), |
||||
|
||||
hasTypedArrays: function() { |
||||
// emscripten-compiled code requires typed arrays
|
||||
return !!window.Uint32Array; |
||||
}, |
||||
|
||||
hasWebAudio: function() { |
||||
return !!(window.AudioContext || window.webkitAudioContext); |
||||
}, |
||||
|
||||
hasFlash: function() { |
||||
if (navigator.userAgent.indexOf('Trident') !== -1) { |
||||
// We only do the ActiveX test because we only need Flash in
|
||||
// Internet Explorer 10/11. Other browsers use Web Audio directly
|
||||
// (Edge, Safari) or native playback, so there's no need to test
|
||||
// other ways of loading Flash.
|
||||
try { |
||||
var obj = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); |
||||
return true; |
||||
} catch(e) { |
||||
return false; |
||||
} |
||||
} |
||||
return false; |
||||
}, |
||||
|
||||
hasAudio: function() { |
||||
return this.hasWebAudio() || this.hasFlash(); |
||||
}, |
||||
|
||||
isBlacklisted: function(userAgent) { |
||||
// JIT bugs in old Safari versions
|
||||
var blacklist = [ |
||||
/\(i.* OS [67]_.* like Mac OS X\).* Mobile\/.* Safari\//, |
||||
/\(Macintosh.* Version\/6\..* Safari\/\d/ |
||||
]; |
||||
var blacklisted = false; |
||||
blacklist.forEach(function(regex) { |
||||
if (userAgent.match(regex)) { |
||||
blacklisted = true; |
||||
} |
||||
}); |
||||
return blacklisted; |
||||
}, |
||||
|
||||
isSlow: function() { |
||||
return this.benchmark.slow; |
||||
}, |
||||
|
||||
isTooSlow: function() { |
||||
return this.benchmark.tooSlow; |
||||
}, |
||||
|
||||
supported: function(component) { |
||||
if (component === 'OGVDecoder') { |
||||
return (this.hasTypedArrays() && !this.isBlacklisted(navigator.userAgent)); |
||||
} |
||||
if (component === 'OGVPlayer') { |
||||
return (this.supported('OGVDecoder') && this.hasAudio() && !this.isTooSlow()); |
||||
} |
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
module.exports = OGVCompat; |
||||
|
||||
|
||||
/***/ }), |
||||
/* 2 */ |
||||
/***/ (function(module, exports) { |
||||
|
||||
/** |
||||
* A quick CPU/JS engine benchmark to guesstimate whether we're |
||||
* fast enough to handle 360p video in JavaScript. |
||||
*/ |
||||
function BogoSlow() { |
||||
var self = this; |
||||
|
||||
var timer; |
||||
// FIXME: avoid to use window scope here
|
||||
if (window.performance && window.performance.now) { |
||||
timer = function() { |
||||
return window.performance.now(); |
||||
}; |
||||
} else { |
||||
timer = function() { |
||||
return Date.now(); |
||||
}; |
||||
} |
||||
|
||||
var savedSpeed = null; |
||||
function run() { |
||||
var ops = 0; |
||||
var fibonacci = function(n) { |
||||
ops++; |
||||
if (n < 2) { |
||||
return n; |
||||
} else { |
||||
return fibonacci(n - 2) + fibonacci(n - 1); |
||||
} |
||||
}; |
||||
|
||||
var start = timer(); |
||||
|
||||
fibonacci(30); |
||||
|
||||
var delta = timer() - start; |
||||
savedSpeed = (ops / delta); |
||||
} |
||||
|
||||
/** |
||||
* Return a scale value of operations/sec from the benchmark. |
||||
* If the benchmark has already been run, uses a memoized result. |
||||
* |
||||
* @property {number} |
||||
*/ |
||||
Object.defineProperty(self, 'speed', { |
||||
get: function() { |
||||
if (savedSpeed === null) { |
||||
run(); |
||||
} |
||||
return savedSpeed; |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* Return the defined cutoff speed value for 'slow' devices, |
||||
* based on results measured from some test devices. |
||||
* |
||||
* @property {number} |
||||
*/ |
||||
Object.defineProperty(self, 'slowCutoff', { |
||||
get: function() { |
||||
// 2012 Retina MacBook Pro (Safari 7) ~150,000
|
||||
// 2009 Dell T5500 (IE 11) ~100,000
|
||||
// iPad Air (iOS 7) ~65,000
|
||||
// 2010 MBP / OS X 10.9 (Safari 7) ~62,500
|
||||
// 2010 MBP / Win7 VM (IE 11) ~50,000+-
|
||||
// ^ these play 360p ok
|
||||
// ----------- line of moderate doom ----------
|
||||
return 50000; |
||||
// v these play 160p ok
|
||||
// iPad Mini non-Retina (iOS 8 beta) ~25,000
|
||||
// Dell Inspiron Duo (IE 11) ~25,000
|
||||
// Surface RT (IE 11) ~18,000
|
||||
// iPod Touch 5th-gen (iOS 8 beta) ~16,000
|
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* Return the defined cutoff speed value for 'too slow' devices, |
||||
* based on results measured from some test devices. |
||||
* |
||||
* No longer used. |
||||
* |
||||
* @property {number} |
||||
* @deprecated |
||||
*/ |
||||
Object.defineProperty(self, 'tooSlowCutoff', { |
||||
get: function() { |
||||
return 0; |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* 'Slow' devices can play audio and should sorta play our |
||||
* extra-tiny Wikimedia 160p15 transcodes |
||||
* |
||||
* @property {boolean} |
||||
*/ |
||||
Object.defineProperty(self, 'slow', { |
||||
get: function() { |
||||
return (self.speed < self.slowCutoff); |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* 'Too slow' devices aren't reliable at all |
||||
* |
||||
* @property {boolean} |
||||
*/ |
||||
Object.defineProperty(self, 'tooSlow', { |
||||
get: function() { |
||||
return (self.speed < self.tooSlowCutoff); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
module.exports = BogoSlow; |
||||
|
||||
|
||||
/***/ }) |
||||
/******/ ]); |
@ -0,0 +1,70 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {}; |
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) { |
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) |
||||
/******/ return installedModules[moduleId].exports; |
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = { |
||||
/******/ exports: {}, |
||||
/******/ id: moduleId, |
||||
/******/ loaded: false |
||||
/******/ }; |
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true; |
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports; |
||||
/******/ } |
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules; |
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules; |
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = ""; |
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0); |
||||
/******/ }) |
||||
/************************************************************************/ |
||||
/******/ ([ |
||||
/* 0 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
//
|
||||
// -- ogv-support.js
|
||||
// https://github.com/brion/ogv.js
|
||||
// Copyright (c) 2013-2016 Brion Vibber
|
||||
//
|
||||
|
||||
(function() { |
||||
|
||||
var OGVVersion = ("1.4.2-20170425024925-504d7197"); |
||||
|
||||
if (window) { |
||||
// 1.0-compat globals
|
||||
window.OGVVersion = OGVVersion; |
||||
} |
||||
|
||||
module.exports = { |
||||
OGVVersion: OGVVersion |
||||
}; |
||||
|
||||
})(); |
||||
|
||||
|
||||
/***/ }) |
||||
/******/ ]); |
@ -0,0 +1,701 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {}; |
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) { |
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) |
||||
/******/ return installedModules[moduleId].exports; |
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = { |
||||
/******/ exports: {}, |
||||
/******/ id: moduleId, |
||||
/******/ loaded: false |
||||
/******/ }; |
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true; |
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports; |
||||
/******/ } |
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules; |
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules; |
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = ""; |
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0); |
||||
/******/ }) |
||||
/************************************************************************/ |
||||
/******/ ([ |
||||
/* 0 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
module.exports = __webpack_require__(1); |
||||
|
||||
|
||||
/***/ }), |
||||
/* 1 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVWorkerSupport = __webpack_require__(2); |
||||
|
||||
var proxy = new OGVWorkerSupport([ |
||||
'loadedMetadata', |
||||
'audioFormat', |
||||
'audioBuffer', |
||||
'cpuTime' |
||||
], { |
||||
init: function(args, callback) { |
||||
this.target.init(callback); |
||||
}, |
||||
|
||||
processHeader: function(args, callback) { |
||||
this.target.processHeader(args[0], function(ok) { |
||||
callback([ok]); |
||||
}); |
||||
}, |
||||
|
||||
processAudio: function(args, callback) { |
||||
this.target.processAudio(args[0], function(ok) { |
||||
callback([ok]); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
module.exports = proxy; |
||||
|
||||
/***/ }), |
||||
/* 2 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVLoader = __webpack_require__(3); |
||||
|
||||
/** |
||||
* Web Worker wrapper for codec fun |
||||
*/ |
||||
function OGVWorkerSupport(propList, handlers) { |
||||
|
||||
var transferables = (function() { |
||||
var buffer = new ArrayBuffer(1024), |
||||
bytes = new Uint8Array(buffer); |
||||
try { |
||||
postMessage({ |
||||
action: 'transferTest', |
||||
bytes: bytes |
||||
}, [buffer]); |
||||
if (buffer.byteLength) { |
||||
// No transferable support
|
||||
return false; |
||||
} else { |
||||
return true; |
||||
} |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
})(); |
||||
|
||||
var self = this; |
||||
self.target = null; |
||||
|
||||
var sentProps = {}, |
||||
pendingEvents = []; |
||||
|
||||
function copyObject(obj) { |
||||
var copy = {}; |
||||
for (var prop in obj) { |
||||
if (obj.hasOwnProperty(prop)) { |
||||
copy[prop] = obj[prop]; |
||||
} |
||||
} |
||||
return copy; |
||||
} |
||||
|
||||
function copyAudioBuffer(data) { |
||||
if (data == null) { |
||||
return null; |
||||
} else { |
||||
// Array of Float32Arrays
|
||||
var copy = []; |
||||
for (var i = 0; i < data.length; i++) { |
||||
copy[i] = new Float32Array(data[i]); |
||||
} |
||||
return copy; |
||||
} |
||||
} |
||||
|
||||
function handleEvent(data) { |
||||
handlers[data.action].call(self, data.args, function(args) { |
||||
args = args || []; |
||||
|
||||
// Collect and send any changed properties...
|
||||
var props = {}, |
||||
transfers = []; |
||||
propList.forEach(function(propName) { |
||||
var propVal = self.target[propName]; |
||||
|
||||
if (sentProps[propName] !== propVal) { |
||||
// Save this value for later reference...
|
||||
sentProps[propName] = propVal; |
||||
|
||||
if (propName == 'duration' && isNaN(propVal) && isNaN(sentProps[propName])) { |
||||
// NaN is not === itself. Nice!
|
||||
// no need to update it here.
|
||||
} else if (propName == 'audioBuffer') { |
||||
// Don't send the entire emscripten heap!
|
||||
propVal = copyAudioBuffer(propVal); |
||||
props[propName] = propVal; |
||||
if (propVal) { |
||||
for (var i = 0; i < propVal.length; i++) { |
||||
transfers.push(propVal[i].buffer); |
||||
} |
||||
} |
||||
} else if (propName == 'frameBuffer') { |
||||
// We already extract ahead of time now,
|
||||
// so transfer the small buffers.
|
||||
props[propName] = propVal; |
||||
if (propVal) { |
||||
transfers.push(propVal.y.bytes.buffer); |
||||
transfers.push(propVal.u.bytes.buffer); |
||||
transfers.push(propVal.v.bytes.buffer); |
||||
} |
||||
} else { |
||||
props[propName] = propVal; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
var out = { |
||||
action: 'callback', |
||||
callbackId: data.callbackId, |
||||
args: args, |
||||
props: props |
||||
}; |
||||
if (transferables) { |
||||
postMessage(out, transfers); |
||||
} else { |
||||
postMessage(out); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
handlers.construct = function(args, callback) { |
||||
var className = args[0], |
||||
options = args[1]; |
||||
|
||||
OGVLoader.loadClass(className, function(classObj) { |
||||
self.target = new classObj(options); |
||||
callback(); |
||||
while (pendingEvents.length) { |
||||
handleEvent(pendingEvents.shift()); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
addEventListener('message', function workerOnMessage(event) { |
||||
var data = event.data; |
||||
if (!data || typeof data !== 'object') { |
||||
// invalid
|
||||
return; |
||||
} else if (data.action == 'transferTest') { |
||||
// ignore
|
||||
} else if (typeof data.action !== 'string' || typeof data.callbackId !== 'string' || typeof data.args !== 'object') { |
||||
console.log('invalid message data', data); |
||||
} else if (!(data.action in handlers)) { |
||||
console.log('invalid message action', data.action); |
||||
} else if (data.action == 'construct') { |
||||
// always handle constructor
|
||||
handleEvent(data); |
||||
} else if (!self.target) { |
||||
// queue until constructed
|
||||
pendingEvents.push(data); |
||||
} else { |
||||
handleEvent(data); |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
module.exports = OGVWorkerSupport; |
||||
|
||||
|
||||
/***/ }), |
||||
/* 3 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVVersion = ("1.4.2-20170425024925-504d7197"); |
||||
|
||||
(function() { |
||||
var global = this; |
||||
|
||||
var scriptMap = { |
||||
OGVDemuxerOgg: 'ogv-demuxer-ogg.js', |
||||
OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js', |
||||
OGVDemuxerWebM: 'ogv-demuxer-webm.js', |
||||
OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js', |
||||
OGVDecoderAudioOpus: 'ogv-decoder-audio-opus.js', |
||||
OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js', |
||||
OGVDecoderAudioVorbis: 'ogv-decoder-audio-vorbis.js', |
||||
OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js', |
||||
OGVDecoderVideoTheora: 'ogv-decoder-video-theora.js', |
||||
OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js', |
||||
OGVDecoderVideoVP8: 'ogv-decoder-video-vp8.js', |
||||
OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js', |
||||
OGVDecoderVideoVP8MT: 'ogv-decoder-video-vp8-mt.js', |
||||
OGVDecoderVideoVP9: 'ogv-decoder-video-vp9.js', |
||||
OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js', |
||||
OGVDecoderVideoVP9MT: 'ogv-decoder-video-vp9-mt.js' |
||||
}; |
||||
|
||||
// @fixme make this less awful
|
||||
var proxyTypes = { |
||||
OGVDecoderAudioOpus: 'audio', |
||||
OGVDecoderAudioOpusW: 'audio', |
||||
OGVDecoderAudioVorbis: 'audio', |
||||
OGVDecoderAudioVorbisW: 'audio', |
||||
OGVDecoderVideoTheora: 'video', |
||||
OGVDecoderVideoTheoraW: 'video', |
||||
OGVDecoderVideoVP8: 'video', |
||||
OGVDecoderVideoVP8W: 'video', |
||||
OGVDecoderVideoVP9: 'video', |
||||
OGVDecoderVideoVP9W: 'video' |
||||
}; |
||||
var proxyInfo = { |
||||
audio: { |
||||
proxy: __webpack_require__(4), |
||||
worker: 'ogv-worker-audio.js', |
||||
}, |
||||
video: { |
||||
proxy: __webpack_require__(6), |
||||
worker: 'ogv-worker-video.js' |
||||
} |
||||
} |
||||
|
||||
function urlForClass(className) { |
||||
var scriptName = scriptMap[className]; |
||||
if (scriptName) { |
||||
return urlForScript(scriptName); |
||||
} else { |
||||
throw new Error('asked for URL for unknown class ' + className); |
||||
} |
||||
}; |
||||
|
||||
function urlForScript(scriptName) { |
||||
if (scriptName) { |
||||
var base = OGVLoader.base; |
||||
if (base === undefined) { |
||||
base = ''; |
||||
} else { |
||||
base += '/'; |
||||
} |
||||
return base + scriptName + '?version=' + encodeURIComponent(OGVVersion); |
||||
} else { |
||||
throw new Error('asked for URL for unknown script ' + scriptName); |
||||
} |
||||
}; |
||||
|
||||
var scriptStatus = {}, |
||||
scriptCallbacks = {}; |
||||
function loadWebScript(src, callback) { |
||||
if (scriptStatus[src] == 'done') { |
||||
callback(); |
||||
} else if (scriptStatus[src] == 'loading') { |
||||
scriptCallbacks[src].push(callback); |
||||
} else { |
||||
scriptStatus[src] = 'loading'; |
||||
scriptCallbacks[src] = [callback]; |
||||
|
||||
var scriptNode = document.createElement('script'); |
||||
function done(event) { |
||||
var callbacks = scriptCallbacks[src]; |
||||
delete scriptCallbacks[src]; |
||||
scriptStatus[src] = 'done'; |
||||
|
||||
callbacks.forEach(function(cb) { |
||||
cb(); |
||||
}); |
||||
} |
||||
scriptNode.addEventListener('load', done); |
||||
scriptNode.addEventListener('error', done); |
||||
scriptNode.src = src; |
||||
document.querySelector('head').appendChild(scriptNode); |
||||
} |
||||
} |
||||
|
||||
function loadWebAssembly(src, callback) { |
||||
if (!src.match(/-wasm\.js/)) { |
||||
callback(null); |
||||
} else { |
||||
var wasmSrc = src.replace(/-wasm\.js/, '-wasm.wasm'); |
||||
var xhr = new XMLHttpRequest(); |
||||
xhr.responseType = 'arraybuffer'; |
||||
xhr.onload = function() { |
||||
callback(xhr.response); |
||||
}; |
||||
xhr.onerror = function() { |
||||
callback(null); |
||||
}; |
||||
xhr.open('GET', wasmSrc); |
||||
xhr.send(); |
||||
} |
||||
} |
||||
|
||||
function defaultBase() { |
||||
if (typeof global.window === 'object') { |
||||
|
||||
// for browser, try to autodetect
|
||||
var scriptNodes = document.querySelectorAll('script'), |
||||
regex = /^(?:|(.*)\/)ogv(?:-support)?\.js(?:\?|#|$)/, |
||||
path, |
||||
matches; |
||||
for (var i = 0; i < scriptNodes.length; i++) { |
||||
path = scriptNodes[i].getAttribute('src'); |
||||
if (path) { |
||||
matches = path.match(regex); |
||||
if (matches) { |
||||
return matches[1]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return undefined; // current dir
|
||||
|
||||
} else { |
||||
|
||||
// for workers, assume current directory
|
||||
// if not a worker, too bad.
|
||||
return undefined; |
||||
|
||||
} |
||||
} |
||||
|
||||
var OGVLoader = { |
||||
base: defaultBase(), |
||||
|
||||
loadClass: function(className, callback, options) { |
||||
options = options || {}; |
||||
if (options.worker) { |
||||
this.workerProxy(className, callback); |
||||
return; |
||||
} |
||||
var url = urlForClass(className); |
||||
loadWebAssembly(url, function(wasmBinary) { |
||||
function wasmClassWrapper(options) { |
||||
options = options || {}; |
||||
if (wasmBinary !== null) { |
||||
options.wasmBinary = wasmBinary; |
||||
} |
||||
return new global[className](options); |
||||
} |
||||
if (typeof global[className] === 'function') { |
||||
// already loaded!
|
||||
callback(wasmClassWrapper); |
||||
} else if (typeof global.window === 'object') { |
||||
loadWebScript(url, function() { |
||||
callback(wasmClassWrapper); |
||||
}); |
||||
} else if (typeof global.importScripts === 'function') { |
||||
// worker has convenient sync importScripts
|
||||
global.importScripts(url); |
||||
callback(wasmClassWrapper); |
||||
} |
||||
}); |
||||
}, |
||||
|
||||
workerProxy: function(className, callback) { |
||||
var proxyType = proxyTypes[className], |
||||
info = proxyInfo[proxyType]; |
||||
|
||||
if (!info) { |
||||
throw new Error('Requested worker for class with no proxy: ' + className); |
||||
} |
||||
|
||||
var proxyClass = info.proxy, |
||||
workerScript = info.worker, |
||||
codecUrl = urlForScript(scriptMap[className]), |
||||
workerUrl = urlForScript(workerScript), |
||||
worker; |
||||
|
||||
var construct = function(options) { |
||||
return new proxyClass(worker, className, options); |
||||
}; |
||||
|
||||
if (workerUrl.match(/^https?:|\/\//i)) { |
||||
// Can't load workers natively cross-domain, but if CORS
|
||||
// is set up we can fetch the worker stub and the desired
|
||||
// class and load them from a blob.
|
||||
var getCodec, |
||||
getWorker, |
||||
codecResponse, |
||||
workerResponse, |
||||
codecLoaded = false, |
||||
workerLoaded = false, |
||||
blob; |
||||
|
||||
function completionCheck() { |
||||
if ((codecLoaded == true) && (workerLoaded == true)) { |
||||
try { |
||||
blob = new Blob([codecResponse + " " + workerResponse], {type: 'application/javascript'}); |
||||
} catch (e) { // Backwards-compatibility
|
||||
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; |
||||
blob = new BlobBuilder(); |
||||
blob.append(codecResponse + " " + workerResponse); |
||||
blob = blob.getBlob(); |
||||
} |
||||
// Create the web worker
|
||||
worker = new Worker(URL.createObjectURL(blob)); |
||||
callback(construct); |
||||
} |
||||
} |
||||
|
||||
// Load the codec
|
||||
getCodec = new XMLHttpRequest(); |
||||
getCodec.open("GET", codecUrl, true); |
||||
getCodec.onreadystatechange = function() { |
||||
if(getCodec.readyState == 4 && getCodec.status == 200) { |
||||
codecResponse = getCodec.responseText; |
||||
// Update the codec response loaded flag
|
||||
codecLoaded = true; |
||||
completionCheck(); |
||||
} |
||||
}; |
||||
getCodec.send(); |
||||
|
||||
// Load the worker
|
||||
getWorker = new XMLHttpRequest(); |
||||
getWorker.open("GET", workerUrl, true); |
||||
getWorker.onreadystatechange = function() { |
||||
if(getWorker.readyState == 4 && getWorker.status == 200) { |
||||
workerResponse = getWorker.responseText; |
||||
// Update the worker response loaded flag
|
||||
workerLoaded = true; |
||||
completionCheck(); |
||||
} |
||||
}; |
||||
getWorker.send(); |
||||
} else { |
||||
// Local URL; load it directly for simplicity.
|
||||
worker = new Worker(workerUrl); |
||||
callback(construct); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
module.exports = OGVLoader; |
||||
|
||||
})(); |
||||
|
||||
|
||||
/***/ }), |
||||
/* 4 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVProxyClass = __webpack_require__(5); |
||||
|
||||
var OGVDecoderAudioProxy = OGVProxyClass({ |
||||
loadedMetadata: false, |
||||
audioFormat: null, |
||||
audioBuffer: null, |
||||
cpuTime: 0 |
||||
}, { |
||||
init: function(callback) { |
||||
this.proxy('init', [], callback); |
||||
}, |
||||
|
||||
processHeader: function(data, callback) { |
||||
this.proxy('processHeader', [data], callback, [data]); |
||||
}, |
||||
|
||||
processAudio: function(data, callback) { |
||||
this.proxy('processAudio', [data], callback, [data]); |
||||
}, |
||||
|
||||
close: function() { |
||||
this.terminate(); |
||||
} |
||||
}); |
||||
|
||||
module.exports = OGVDecoderAudioProxy; |
||||
|
||||
|
||||
/***/ }), |
||||
/* 5 */ |
||||
/***/ (function(module, exports) { |
||||
|
||||
/** |
||||
* Proxy object for web worker interface for codec classes. |
||||
* |
||||
* Used by the high-level player interface. |
||||
* |
||||
* @author Brion Vibber <brion@pobox.com> |
||||
* @copyright 2015 |
||||
* @license MIT-style |
||||
*/ |
||||
function OGVProxyClass(initialProps, methods) { |
||||
return function(worker, className, options) { |
||||
options = options || {}; |
||||
var self = this; |
||||
|
||||
var transferables = (function() { |
||||
var buffer = new ArrayBuffer(1024), |
||||
bytes = new Uint8Array(buffer); |
||||
try { |
||||
worker.postMessage({ |
||||
action: 'transferTest', |
||||
bytes: bytes |
||||
}, [buffer]); |
||||
if (buffer.byteLength) { |
||||
// No transferable support
|
||||
return false; |
||||
} else { |
||||
return true; |
||||
} |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
})(); |
||||
|
||||
// Set up proxied property getters
|
||||
var props = {}; |
||||
for (var iPropName in initialProps) { |
||||
if (initialProps.hasOwnProperty(iPropName)) { |
||||
(function(propName) { |
||||
props[propName] = initialProps[propName]; |
||||
Object.defineProperty(self, propName, { |
||||
get: function getProperty() { |
||||
return props[propName]; |
||||
} |
||||
}); |
||||
})(iPropName); |
||||
} |
||||
} |
||||
|
||||
// Current player wants to avoid async confusion.
|
||||
var processingQueue = 0; |
||||
Object.defineProperty(self, 'processing', { |
||||
get: function() { |
||||
return (processingQueue > 0); |
||||
} |
||||
}); |
||||
|
||||
// Set up proxied methods
|
||||
for (var method in methods) { |
||||
if (methods.hasOwnProperty(method)) { |
||||
self[method] = methods[method]; |
||||
} |
||||
} |
||||
|
||||
// And some infrastructure!
|
||||
var messageCount = 0, |
||||
pendingCallbacks = {}; |
||||
this.proxy = function(action, args, callback, transfers) { |
||||
if (!worker) { |
||||
throw 'Tried to call "' + action + '" method on closed proxy object'; |
||||
} |
||||
var callbackId = 'callback-' + (++messageCount) + '-' + action; |
||||
if (callback) { |
||||
pendingCallbacks[callbackId] = callback; |
||||
} |
||||
var out = { |
||||
'action': action, |
||||
'callbackId': callbackId, |
||||
'args': args || [] |
||||
}; |
||||
processingQueue++; |
||||
if (transferables) { |
||||
worker.postMessage(out, transfers || []); |
||||
} else { |
||||
worker.postMessage(out); |
||||
} |
||||
}; |
||||
this.terminate = function() { |
||||
if (worker) { |
||||
worker.terminate(); |
||||
worker = null; |
||||
processingQueue = 0; |
||||
pendingCallbacks = {}; |
||||
} |
||||
}; |
||||
|
||||
worker.addEventListener('message', function proxyOnMessage(event) { |
||||
processingQueue--; |
||||
if (event.data.action !== 'callback') { |
||||
// ignore
|
||||
return; |
||||
} |
||||
|
||||
var data = event.data, |
||||
callbackId = data.callbackId, |
||||
args = data.args, |
||||
callback = pendingCallbacks[callbackId]; |
||||
|
||||
// Save any updated properties returned to us...
|
||||
if (data.props) { |
||||
for (var propName in data.props) { |
||||
if (data.props.hasOwnProperty(propName)) { |
||||
props[propName] = data.props[propName]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (callback) { |
||||
delete pendingCallbacks[callbackId]; |
||||
callback.apply(this, args); |
||||
} |
||||
}); |
||||
|
||||
// Tell the proxy to load and initialize the appropriate class
|
||||
self.proxy('construct', [className, options], function() {}); |
||||
|
||||
return self; |
||||
}; |
||||
} |
||||
|
||||
module.exports = OGVProxyClass; |
||||
|
||||
/***/ }), |
||||
/* 6 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVProxyClass = __webpack_require__(5); |
||||
|
||||
var OGVDecoderVideoProxy = OGVProxyClass({ |
||||
loadedMetadata: false, |
||||
videoFormat: null, |
||||
frameBuffer: null, |
||||
cpuTime: 0 |
||||
}, { |
||||
init: function(callback) { |
||||
this.proxy('init', [], callback); |
||||
}, |
||||
|
||||
processHeader: function(data, callback) { |
||||
this.proxy('processHeader', [data], callback, [data]); |
||||
}, |
||||
|
||||
processFrame: function(data, callback) { |
||||
this.proxy('processFrame', [data], callback, [data]); |
||||
}, |
||||
|
||||
close: function() { |
||||
this.terminate(); |
||||
} |
||||
}); |
||||
|
||||
module.exports = OGVDecoderVideoProxy; |
||||
|
||||
/***/ }) |
||||
/******/ ]); |
@ -0,0 +1,700 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {}; |
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) { |
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) |
||||
/******/ return installedModules[moduleId].exports; |
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = { |
||||
/******/ exports: {}, |
||||
/******/ id: moduleId, |
||||
/******/ loaded: false |
||||
/******/ }; |
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true; |
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports; |
||||
/******/ } |
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules; |
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules; |
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = ""; |
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0); |
||||
/******/ }) |
||||
/************************************************************************/ |
||||
/******/ ([ |
||||
/* 0 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
module.exports = __webpack_require__(1); |
||||
|
||||
/***/ }), |
||||
/* 1 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVWorkerSupport = __webpack_require__(2); |
||||
|
||||
var proxy = new OGVWorkerSupport([ |
||||
'loadedMetadata', |
||||
'videoFormat', |
||||
'frameBuffer', |
||||
'cpuTime' |
||||
], { |
||||
init: function(args, callback) { |
||||
this.target.init(callback); |
||||
}, |
||||
|
||||
processHeader: function(args, callback) { |
||||
this.target.processHeader(args[0], function(ok) { |
||||
callback([ok]); |
||||
}); |
||||
}, |
||||
|
||||
processFrame: function(args, callback) { |
||||
this.target.processFrame(args[0], function(ok) { |
||||
callback([ok]); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
module.exports = proxy; |
||||
|
||||
/***/ }), |
||||
/* 2 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVLoader = __webpack_require__(3); |
||||
|
||||
/** |
||||
* Web Worker wrapper for codec fun |
||||
*/ |
||||
function OGVWorkerSupport(propList, handlers) { |
||||
|
||||
var transferables = (function() { |
||||
var buffer = new ArrayBuffer(1024), |
||||
bytes = new Uint8Array(buffer); |
||||
try { |
||||
postMessage({ |
||||
action: 'transferTest', |
||||
bytes: bytes |
||||
}, [buffer]); |
||||
if (buffer.byteLength) { |
||||
// No transferable support
|
||||
return false; |
||||
} else { |
||||
return true; |
||||
} |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
})(); |
||||
|
||||
var self = this; |
||||
self.target = null; |
||||
|
||||
var sentProps = {}, |
||||
pendingEvents = []; |
||||
|
||||
function copyObject(obj) { |
||||
var copy = {}; |
||||
for (var prop in obj) { |
||||
if (obj.hasOwnProperty(prop)) { |
||||
copy[prop] = obj[prop]; |
||||
} |
||||
} |
||||
return copy; |
||||
} |
||||
|
||||
function copyAudioBuffer(data) { |
||||
if (data == null) { |
||||
return null; |
||||
} else { |
||||
// Array of Float32Arrays
|
||||
var copy = []; |
||||
for (var i = 0; i < data.length; i++) { |
||||
copy[i] = new Float32Array(data[i]); |
||||
} |
||||
return copy; |
||||
} |
||||
} |
||||
|
||||
function handleEvent(data) { |
||||
handlers[data.action].call(self, data.args, function(args) { |
||||
args = args || []; |
||||
|
||||
// Collect and send any changed properties...
|
||||
var props = {}, |
||||
transfers = []; |
||||
propList.forEach(function(propName) { |
||||
var propVal = self.target[propName]; |
||||
|
||||
if (sentProps[propName] !== propVal) { |
||||
// Save this value for later reference...
|
||||
sentProps[propName] = propVal; |
||||
|
||||
if (propName == 'duration' && isNaN(propVal) && isNaN(sentProps[propName])) { |
||||
// NaN is not === itself. Nice!
|
||||
// no need to update it here.
|
||||
} else if (propName == 'audioBuffer') { |
||||
// Don't send the entire emscripten heap!
|
||||
propVal = copyAudioBuffer(propVal); |
||||
props[propName] = propVal; |
||||
if (propVal) { |
||||
for (var i = 0; i < propVal.length; i++) { |
||||
transfers.push(propVal[i].buffer); |
||||
} |
||||
} |
||||
} else if (propName == 'frameBuffer') { |
||||
// We already extract ahead of time now,
|
||||
// so transfer the small buffers.
|
||||
props[propName] = propVal; |
||||
if (propVal) { |
||||
transfers.push(propVal.y.bytes.buffer); |
||||
transfers.push(propVal.u.bytes.buffer); |
||||
transfers.push(propVal.v.bytes.buffer); |
||||
} |
||||
} else { |
||||
props[propName] = propVal; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
var out = { |
||||
action: 'callback', |
||||
callbackId: data.callbackId, |
||||
args: args, |
||||
props: props |
||||
}; |
||||
if (transferables) { |
||||
postMessage(out, transfers); |
||||
} else { |
||||
postMessage(out); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
handlers.construct = function(args, callback) { |
||||
var className = args[0], |
||||
options = args[1]; |
||||
|
||||
OGVLoader.loadClass(className, function(classObj) { |
||||
self.target = new classObj(options); |
||||
callback(); |
||||
while (pendingEvents.length) { |
||||
handleEvent(pendingEvents.shift()); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
addEventListener('message', function workerOnMessage(event) { |
||||
var data = event.data; |
||||
if (!data || typeof data !== 'object') { |
||||
// invalid
|
||||
return; |
||||
} else if (data.action == 'transferTest') { |
||||
// ignore
|
||||
} else if (typeof data.action !== 'string' || typeof data.callbackId !== 'string' || typeof data.args !== 'object') { |
||||
console.log('invalid message data', data); |
||||
} else if (!(data.action in handlers)) { |
||||
console.log('invalid message action', data.action); |
||||
} else if (data.action == 'construct') { |
||||
// always handle constructor
|
||||
handleEvent(data); |
||||
} else if (!self.target) { |
||||
// queue until constructed
|
||||
pendingEvents.push(data); |
||||
} else { |
||||
handleEvent(data); |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
module.exports = OGVWorkerSupport; |
||||
|
||||
|
||||
/***/ }), |
||||
/* 3 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVVersion = ("1.4.2-20170425024925-504d7197"); |
||||
|
||||
(function() { |
||||
var global = this; |
||||
|
||||
var scriptMap = { |
||||
OGVDemuxerOgg: 'ogv-demuxer-ogg.js', |
||||
OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js', |
||||
OGVDemuxerWebM: 'ogv-demuxer-webm.js', |
||||
OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js', |
||||
OGVDecoderAudioOpus: 'ogv-decoder-audio-opus.js', |
||||
OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js', |
||||
OGVDecoderAudioVorbis: 'ogv-decoder-audio-vorbis.js', |
||||
OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js', |
||||
OGVDecoderVideoTheora: 'ogv-decoder-video-theora.js', |
||||
OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js', |
||||
OGVDecoderVideoVP8: 'ogv-decoder-video-vp8.js', |
||||
OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js', |
||||
OGVDecoderVideoVP8MT: 'ogv-decoder-video-vp8-mt.js', |
||||
OGVDecoderVideoVP9: 'ogv-decoder-video-vp9.js', |
||||
OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js', |
||||
OGVDecoderVideoVP9MT: 'ogv-decoder-video-vp9-mt.js' |
||||
}; |
||||
|
||||
// @fixme make this less awful
|
||||
var proxyTypes = { |
||||
OGVDecoderAudioOpus: 'audio', |
||||
OGVDecoderAudioOpusW: 'audio', |
||||
OGVDecoderAudioVorbis: 'audio', |
||||
OGVDecoderAudioVorbisW: 'audio', |
||||
OGVDecoderVideoTheora: 'video', |
||||
OGVDecoderVideoTheoraW: 'video', |
||||
OGVDecoderVideoVP8: 'video', |
||||
OGVDecoderVideoVP8W: 'video', |
||||
OGVDecoderVideoVP9: 'video', |
||||
OGVDecoderVideoVP9W: 'video' |
||||
}; |
||||
var proxyInfo = { |
||||
audio: { |
||||
proxy: __webpack_require__(4), |
||||
worker: 'ogv-worker-audio.js', |
||||
}, |
||||
video: { |
||||
proxy: __webpack_require__(6), |
||||
worker: 'ogv-worker-video.js' |
||||
} |
||||
} |
||||
|
||||
function urlForClass(className) { |
||||
var scriptName = scriptMap[className]; |
||||
if (scriptName) { |
||||
return urlForScript(scriptName); |
||||
} else { |
||||
throw new Error('asked for URL for unknown class ' + className); |
||||
} |
||||
}; |
||||
|
||||
function urlForScript(scriptName) { |
||||
if (scriptName) { |
||||
var base = OGVLoader.base; |
||||
if (base === undefined) { |
||||
base = ''; |
||||
} else { |
||||
base += '/'; |
||||
} |
||||
return base + scriptName + '?version=' + encodeURIComponent(OGVVersion); |
||||
} else { |
||||
throw new Error('asked for URL for unknown script ' + scriptName); |
||||
} |
||||
}; |
||||
|
||||
var scriptStatus = {}, |
||||
scriptCallbacks = {}; |
||||
function loadWebScript(src, callback) { |
||||
if (scriptStatus[src] == 'done') { |
||||
callback(); |
||||
} else if (scriptStatus[src] == 'loading') { |
||||
scriptCallbacks[src].push(callback); |
||||
} else { |
||||
scriptStatus[src] = 'loading'; |
||||
scriptCallbacks[src] = [callback]; |
||||
|
||||
var scriptNode = document.createElement('script'); |
||||
function done(event) { |
||||
var callbacks = scriptCallbacks[src]; |
||||
delete scriptCallbacks[src]; |
||||
scriptStatus[src] = 'done'; |
||||
|
||||
callbacks.forEach(function(cb) { |
||||
cb(); |
||||
}); |
||||
} |
||||
scriptNode.addEventListener('load', done); |
||||
scriptNode.addEventListener('error', done); |
||||
scriptNode.src = src; |
||||
document.querySelector('head').appendChild(scriptNode); |
||||
} |
||||
} |
||||
|
||||
function loadWebAssembly(src, callback) { |
||||
if (!src.match(/-wasm\.js/)) { |
||||
callback(null); |
||||
} else { |
||||
var wasmSrc = src.replace(/-wasm\.js/, '-wasm.wasm'); |
||||
var xhr = new XMLHttpRequest(); |
||||
xhr.responseType = 'arraybuffer'; |
||||
xhr.onload = function() { |
||||
callback(xhr.response); |
||||
}; |
||||
xhr.onerror = function() { |
||||
callback(null); |
||||
}; |
||||
xhr.open('GET', wasmSrc); |
||||
xhr.send(); |
||||
} |
||||
} |
||||
|
||||
function defaultBase() { |
||||
if (typeof global.window === 'object') { |
||||
|
||||
// for browser, try to autodetect
|
||||
var scriptNodes = document.querySelectorAll('script'), |
||||
regex = /^(?:|(.*)\/)ogv(?:-support)?\.js(?:\?|#|$)/, |
||||
path, |
||||
matches; |
||||
for (var i = 0; i < scriptNodes.length; i++) { |
||||
path = scriptNodes[i].getAttribute('src'); |
||||
if (path) { |
||||
matches = path.match(regex); |
||||
if (matches) { |
||||
return matches[1]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return undefined; // current dir
|
||||
|
||||
} else { |
||||
|
||||
// for workers, assume current directory
|
||||
// if not a worker, too bad.
|
||||
return undefined; |
||||
|
||||
} |
||||
} |
||||
|
||||
var OGVLoader = { |
||||
base: defaultBase(), |
||||
|
||||
loadClass: function(className, callback, options) { |
||||
options = options || {}; |
||||
if (options.worker) { |
||||
this.workerProxy(className, callback); |
||||
return; |
||||
} |
||||
var url = urlForClass(className); |
||||
loadWebAssembly(url, function(wasmBinary) { |
||||
function wasmClassWrapper(options) { |
||||
options = options || {}; |
||||
if (wasmBinary !== null) { |
||||
options.wasmBinary = wasmBinary; |
||||
} |
||||
return new global[className](options); |
||||
} |
||||
if (typeof global[className] === 'function') { |
||||
// already loaded!
|
||||
callback(wasmClassWrapper); |
||||
} else if (typeof global.window === 'object') { |
||||
loadWebScript(url, function() { |
||||
callback(wasmClassWrapper); |
||||
}); |
||||
} else if (typeof global.importScripts === 'function') { |
||||
// worker has convenient sync importScripts
|
||||
global.importScripts(url); |
||||
callback(wasmClassWrapper); |
||||
} |
||||
}); |
||||
}, |
||||
|
||||
workerProxy: function(className, callback) { |
||||
var proxyType = proxyTypes[className], |
||||
info = proxyInfo[proxyType]; |
||||
|
||||
if (!info) { |
||||
throw new Error('Requested worker for class with no proxy: ' + className); |
||||
} |
||||
|
||||
var proxyClass = info.proxy, |
||||
workerScript = info.worker, |
||||
codecUrl = urlForScript(scriptMap[className]), |
||||
workerUrl = urlForScript(workerScript), |
||||
worker; |
||||
|
||||
var construct = function(options) { |
||||
return new proxyClass(worker, className, options); |
||||
}; |
||||
|
||||
if (workerUrl.match(/^https?:|\/\//i)) { |
||||
// Can't load workers natively cross-domain, but if CORS
|
||||
// is set up we can fetch the worker stub and the desired
|
||||
// class and load them from a blob.
|
||||
var getCodec, |
||||
getWorker, |
||||
codecResponse, |
||||
workerResponse, |
||||
codecLoaded = false, |
||||
workerLoaded = false, |
||||
blob; |
||||
|
||||
function completionCheck() { |
||||
if ((codecLoaded == true) && (workerLoaded == true)) { |
||||
try { |
||||
blob = new Blob([codecResponse + " " + workerResponse], {type: 'application/javascript'}); |
||||
} catch (e) { // Backwards-compatibility
|
||||
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; |
||||
blob = new BlobBuilder(); |
||||
blob.append(codecResponse + " " + workerResponse); |
||||
blob = blob.getBlob(); |
||||
} |
||||
// Create the web worker
|
||||
worker = new Worker(URL.createObjectURL(blob)); |
||||
callback(construct); |
||||
} |
||||
} |
||||
|
||||
// Load the codec
|
||||
getCodec = new XMLHttpRequest(); |
||||
getCodec.open("GET", codecUrl, true); |
||||
getCodec.onreadystatechange = function() { |
||||
if(getCodec.readyState == 4 && getCodec.status == 200) { |
||||
codecResponse = getCodec.responseText; |
||||
// Update the codec response loaded flag
|
||||
codecLoaded = true; |
||||
completionCheck(); |
||||
} |
||||
}; |
||||
getCodec.send(); |
||||
|
||||
// Load the worker
|
||||
getWorker = new XMLHttpRequest(); |
||||
getWorker.open("GET", workerUrl, true); |
||||
getWorker.onreadystatechange = function() { |
||||
if(getWorker.readyState == 4 && getWorker.status == 200) { |
||||
workerResponse = getWorker.responseText; |
||||
// Update the worker response loaded flag
|
||||
workerLoaded = true; |
||||
completionCheck(); |
||||
} |
||||
}; |
||||
getWorker.send(); |
||||
} else { |
||||
// Local URL; load it directly for simplicity.
|
||||
worker = new Worker(workerUrl); |
||||
callback(construct); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
module.exports = OGVLoader; |
||||
|
||||
})(); |
||||
|
||||
|
||||
/***/ }), |
||||
/* 4 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVProxyClass = __webpack_require__(5); |
||||
|
||||
var OGVDecoderAudioProxy = OGVProxyClass({ |
||||
loadedMetadata: false, |
||||
audioFormat: null, |
||||
audioBuffer: null, |
||||
cpuTime: 0 |
||||
}, { |
||||
init: function(callback) { |
||||
this.proxy('init', [], callback); |
||||
}, |
||||
|
||||
processHeader: function(data, callback) { |
||||
this.proxy('processHeader', [data], callback, [data]); |
||||
}, |
||||
|
||||
processAudio: function(data, callback) { |
||||
this.proxy('processAudio', [data], callback, [data]); |
||||
}, |
||||
|
||||
close: function() { |
||||
this.terminate(); |
||||
} |
||||
}); |
||||
|
||||
module.exports = OGVDecoderAudioProxy; |
||||
|
||||
|
||||
/***/ }), |
||||
/* 5 */ |
||||
/***/ (function(module, exports) { |
||||
|
||||
/** |
||||
* Proxy object for web worker interface for codec classes. |
||||
* |
||||
* Used by the high-level player interface. |
||||
* |
||||
* @author Brion Vibber <brion@pobox.com> |
||||
* @copyright 2015 |
||||
* @license MIT-style |
||||
*/ |
||||
function OGVProxyClass(initialProps, methods) { |
||||
return function(worker, className, options) { |
||||
options = options || {}; |
||||
var self = this; |
||||
|
||||
var transferables = (function() { |
||||
var buffer = new ArrayBuffer(1024), |
||||
bytes = new Uint8Array(buffer); |
||||
try { |
||||
worker.postMessage({ |
||||
action: 'transferTest', |
||||
bytes: bytes |
||||
}, [buffer]); |
||||
if (buffer.byteLength) { |
||||
// No transferable support
|
||||
return false; |
||||
} else { |
||||
return true; |
||||
} |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
})(); |
||||
|
||||
// Set up proxied property getters
|
||||
var props = {}; |
||||
for (var iPropName in initialProps) { |
||||
if (initialProps.hasOwnProperty(iPropName)) { |
||||
(function(propName) { |
||||
props[propName] = initialProps[propName]; |
||||
Object.defineProperty(self, propName, { |
||||
get: function getProperty() { |
||||
return props[propName]; |
||||
} |
||||
}); |
||||
})(iPropName); |
||||
} |
||||
} |
||||
|
||||
// Current player wants to avoid async confusion.
|
||||
var processingQueue = 0; |
||||
Object.defineProperty(self, 'processing', { |
||||
get: function() { |
||||
return (processingQueue > 0); |
||||
} |
||||
}); |
||||
|
||||
// Set up proxied methods
|
||||
for (var method in methods) { |
||||
if (methods.hasOwnProperty(method)) { |
||||
self[method] = methods[method]; |
||||
} |
||||
} |
||||
|
||||
// And some infrastructure!
|
||||
var messageCount = 0, |
||||
pendingCallbacks = {}; |
||||
this.proxy = function(action, args, callback, transfers) { |
||||
if (!worker) { |
||||
throw 'Tried to call "' + action + '" method on closed proxy object'; |
||||
} |
||||
var callbackId = 'callback-' + (++messageCount) + '-' + action; |
||||
if (callback) { |
||||
pendingCallbacks[callbackId] = callback; |
||||
} |
||||
var out = { |
||||
'action': action, |
||||
'callbackId': callbackId, |
||||
'args': args || [] |
||||
}; |
||||
processingQueue++; |
||||
if (transferables) { |
||||
worker.postMessage(out, transfers || []); |
||||
} else { |
||||
worker.postMessage(out); |
||||
} |
||||
}; |
||||
this.terminate = function() { |
||||
if (worker) { |
||||
worker.terminate(); |
||||
worker = null; |
||||
processingQueue = 0; |
||||
pendingCallbacks = {}; |
||||
} |
||||
}; |
||||
|
||||
worker.addEventListener('message', function proxyOnMessage(event) { |
||||
processingQueue--; |
||||
if (event.data.action !== 'callback') { |
||||
// ignore
|
||||
return; |
||||
} |
||||
|
||||
var data = event.data, |
||||
callbackId = data.callbackId, |
||||
args = data.args, |
||||
callback = pendingCallbacks[callbackId]; |
||||
|
||||
// Save any updated properties returned to us...
|
||||
if (data.props) { |
||||
for (var propName in data.props) { |
||||
if (data.props.hasOwnProperty(propName)) { |
||||
props[propName] = data.props[propName]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (callback) { |
||||
delete pendingCallbacks[callbackId]; |
||||
callback.apply(this, args); |
||||
} |
||||
}); |
||||
|
||||
// Tell the proxy to load and initialize the appropriate class
|
||||
self.proxy('construct', [className, options], function() {}); |
||||
|
||||
return self; |
||||
}; |
||||
} |
||||
|
||||
module.exports = OGVProxyClass; |
||||
|
||||
/***/ }), |
||||
/* 6 */ |
||||
/***/ (function(module, exports, __webpack_require__) { |
||||
|
||||
var OGVProxyClass = __webpack_require__(5); |
||||
|
||||
var OGVDecoderVideoProxy = OGVProxyClass({ |
||||
loadedMetadata: false, |
||||
videoFormat: null, |
||||
frameBuffer: null, |
||||
cpuTime: 0 |
||||
}, { |
||||
init: function(callback) { |
||||
this.proxy('init', [], callback); |
||||
}, |
||||
|
||||
processHeader: function(data, callback) { |
||||
this.proxy('processHeader', [data], callback, [data]); |
||||
}, |
||||
|
||||
processFrame: function(data, callback) { |
||||
this.proxy('processFrame', [data], callback, [data]); |
||||
}, |
||||
|
||||
close: function() { |
||||
this.terminate(); |
||||
} |
||||
}); |
||||
|
||||
module.exports = OGVDecoderVideoProxy; |
||||
|
||||
/***/ }) |
||||
/******/ ]); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,103 @@
|
||||
// Pthread Web Worker startup routine:
|
||||
// This is the entry point file that is loaded first by each Web Worker
|
||||
// that executes pthreads on the Emscripten application.
|
||||
|
||||
// Thread-local, communicated via globals:
|
||||
var threadInfoStruct = 0; // Info area for this thread in Emscripten HEAP (shared). If zero, this worker is not currently hosting an executing pthread.
|
||||
var selfThreadId = 0; // The ID of this thread. 0 if not hosting a pthread.
|
||||
var parentThreadId = 0; // The ID of the parent pthread that launched this thread.
|
||||
|
||||
// Send the pthreads mode and other params in through Module object settings
|
||||
var Module = { |
||||
ENVIRONMENT: 'PTHREAD' |
||||
}; |
||||
|
||||
// Cannot use console.log or console.error in a web worker, since that would risk a browser deadlock! https://bugzilla.mozilla.org/show_bug.cgi?id=1049091
|
||||
// Therefore implement custom logging facility for threads running in a worker, which queue the messages to main thread to print.
|
||||
function threadPrint() { |
||||
var text = Array.prototype.slice.call(arguments).join(' '); |
||||
console.log(text); |
||||
} |
||||
function threadPrintErr() { |
||||
var text = Array.prototype.slice.call(arguments).join(' '); |
||||
console.error(text); |
||||
} |
||||
function threadAlert() { |
||||
var text = Array.prototype.slice.call(arguments).join(' '); |
||||
postMessage({cmd: 'alert', text: text, threadId: selfThreadId}); |
||||
} |
||||
Module['print'] = threadPrint; |
||||
Module['printErr'] = threadPrintErr; |
||||
this.alert = threadAlert; |
||||
|
||||
// If modularized, we can't reuse the module's assert() function.
|
||||
function assert(condition, text) { |
||||
if (!condition) { |
||||
abort('Assertion failed: ' + text); |
||||
} |
||||
} |
||||
|
||||
this.onmessage = function(e) { |
||||
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
|
||||
// Initialize the thread-local field(s):
|
||||
Module['tempDoublePtr'] = e.data.tempDoublePtr; |
||||
|
||||
// Initialize the global "process"-wide fields:
|
||||
Module['buffer'] = e.data.buffer; |
||||
Module['TOTAL_MEMORY'] = e.data.TOTAL_MEMORY; |
||||
Module['STATICTOP'] = e.data.STATICTOP; |
||||
Module['DYNAMIC_BASE'] = e.data.DYNAMIC_BASE; |
||||
Module['DYNAMICTOP_PTR'] = e.data.DYNAMICTOP_PTR; |
||||
|
||||
Module['pthreadWorkerInit'] = e.data.PthreadWorkerInit; |
||||
importScripts(e.data.url); |
||||
if (e.data.modularize) { |
||||
// Feed input options into the modularized constructor...
|
||||
// 'this' is the Worker, which is also global scope.
|
||||
Module = new this[e.data.moduleExportName](Module); |
||||
} |
||||
if (typeof FS !== 'undefined') FS.createStandardStreams(); |
||||
postMessage({ cmd: 'loaded' }); |
||||
} else if (e.data.cmd === 'objectTransfer') { |
||||
Module.PThread.receiveObjectTransfer(e.data); |
||||
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
|
||||
threadInfoStruct = e.data.threadInfoStruct; |
||||
Module.PThread.registerPthreadPtr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
|
||||
assert(threadInfoStruct); |
||||
selfThreadId = e.data.selfThreadId; |
||||
parentThreadId = e.data.parentThreadId; |
||||
assert(selfThreadId); |
||||
assert(parentThreadId); |
||||
Module.PThread.setStackSpace(e.data.stackBase, e.data.stackBase + e.data.stackSize); |
||||
var result = 0; |
||||
|
||||
Module.PThread.receiveObjectTransfer(e.data); |
||||
|
||||
Module.PThread.setThreadStatus(threadInfoStruct, 1/*EM_THREAD_STATUS_RUNNING*/); |
||||
|
||||
try { |
||||
Module.PThread.runThreadFunc(e.data.start_routine, e.data.arg); |
||||
} catch(e) { |
||||
if (e === 'Canceled!') { |
||||
Module.PThread.threadCancel(); |
||||
return; |
||||
} else { |
||||
Atomics.store(Module.HEAPU32, (threadInfoStruct + 4 /*{{{ C_STRUCTS.pthread.threadExitCode }}}*/ ) >> 2, -2 /*A custom entry specific to Emscripten denoting that the thread crashed.*/); |
||||
Atomics.store(Module.HEAPU32, (threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/ ) >> 2, 1); // Mark the thread as no longer running.
|
||||
Module.PThread.wakeAllThreads(); |
||||
throw e; |
||||
} |
||||
} |
||||
// The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves.
|
||||
// (This is a no-op if explicit pthread_exit() had been called prior.)
|
||||
if (!Module['noExitRuntime']) Module.PThread.threadExit(result); |
||||
else console.log('pthread noExitRuntime: not quitting.'); |
||||
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
|
||||
if (threadInfoStruct && Module.PThread.thisThreadCancelState == 0/*PTHREAD_CANCEL_ENABLE*/) { |
||||
Module.PThread.threadCancel(); |
||||
} |
||||
} else { |
||||
// Module['printErr']('pthread-main.js received unknown command ' + e.data.cmd);
|
||||
// console.error(e.data)
|
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,245 @@
|
||||
"use strict"; |
||||
|
||||
var root = (typeof self === 'object' && self.self === self && self) || (typeof global === 'object' && global.global === global && global) || this; |
||||
|
||||
(function( global ) { |
||||
|
||||
var Recorder = function( config ){ |
||||
|
||||
var that = this; |
||||
|
||||
if ( !Recorder.isRecordingSupported() ) { |
||||
throw new Error("Recording is not supported in this browser"); |
||||
} |
||||
|
||||
this.state = "inactive"; |
||||
this.eventTarget = global.document.createDocumentFragment(); |
||||
this.audioContext = new global.AudioContext(); |
||||
this.monitorNode = this.audioContext.createGain(); |
||||
|
||||
this.config = config = config || {}; |
||||
this.config.command = "init"; |
||||
this.config.bufferLength = config.bufferLength || 4096; |
||||
this.config.monitorGain = config.monitorGain || 0; |
||||
this.config.numberOfChannels = config.numberOfChannels || 1; |
||||
this.config.originalSampleRate = this.audioContext.sampleRate; |
||||
this.config.encoderSampleRate = config.encoderSampleRate || 48000; |
||||
this.config.encoderPath = config.encoderPath || 'encoderWorker.min.js'; |
||||
this.config.streamPages = config.streamPages || false; |
||||
this.config.leaveStreamOpen = config.leaveStreamOpen || false; |
||||
this.config.maxBuffersPerPage = config.maxBuffersPerPage || 40; |
||||
this.config.encoderApplication = config.encoderApplication || 2049; |
||||
this.config.encoderFrameSize = config.encoderFrameSize || 20; |
||||
this.config.resampleQuality = config.resampleQuality || 3; |
||||
this.config.streamOptions = config.streamOptions || { |
||||
optional: [], |
||||
mandatory: { |
||||
googEchoCancellation: false, |
||||
googAutoGainControl: false, |
||||
googNoiseSuppression: false, |
||||
googHighpassFilter: false |
||||
} |
||||
}; |
||||
|
||||
this.setMonitorGain( this.config.monitorGain ); |
||||
this.scriptProcessorNode = this.audioContext.createScriptProcessor( this.config.bufferLength, this.config.numberOfChannels, this.config.numberOfChannels ); |
||||
this.scriptProcessorNode.onaudioprocess = function( e ){ |
||||
that.encodeBuffers( e.inputBuffer ); |
||||
}; |
||||
}; |
||||
|
||||
Recorder.isRecordingSupported = function(){ |
||||
return global.AudioContext && global.navigator && ( global.navigator.getUserMedia || ( global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia ) ); |
||||
}; |
||||
|
||||
Recorder.prototype.addEventListener = function( type, listener, useCapture ){ |
||||
this.eventTarget.addEventListener( type, listener, useCapture ); |
||||
}; |
||||
|
||||
Recorder.prototype.clearStream = function() { |
||||
if ( this.stream ) { |
||||
|
||||
if ( this.stream.getTracks ) { |
||||
this.stream.getTracks().forEach(function ( track ) { |
||||
track.stop(); |
||||
}); |
||||
} |
||||
|
||||
else { |
||||
this.stream.stop(); |
||||
} |
||||
|
||||
delete this.stream; |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.encodeBuffers = function( inputBuffer ){ |
||||
if ( this.state === "recording" ) { |
||||
var buffers = []; |
||||
for ( var i = 0; i < inputBuffer.numberOfChannels; i++ ) { |
||||
buffers[i] = inputBuffer.getChannelData(i); |
||||
} |
||||
|
||||
this.encoder.postMessage({ |
||||
command: "encode", |
||||
buffers: buffers |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.initStream = function(){ |
||||
var that = this; |
||||
|
||||
var onStreamInit = function( stream ){ |
||||
that.stream = stream; |
||||
that.sourceNode = that.audioContext.createMediaStreamSource( stream ); |
||||
that.sourceNode.connect( that.scriptProcessorNode ); |
||||
that.sourceNode.connect( that.monitorNode ); |
||||
that.eventTarget.dispatchEvent( new global.Event( "streamReady" ) ); |
||||
return stream; |
||||
} |
||||
|
||||
var onStreamError = function( e ){ |
||||
that.eventTarget.dispatchEvent( new global.ErrorEvent( "streamError", { error: e } ) ); |
||||
} |
||||
|
||||
var constraints = { audio : this.config.streamOptions }; |
||||
|
||||
if ( this.stream ) { |
||||
this.eventTarget.dispatchEvent( new global.Event( "streamReady" ) ); |
||||
return global.Promise.resolve( this.stream ); |
||||
} |
||||
|
||||
if ( global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia ) { |
||||
return global.navigator.mediaDevices.getUserMedia( constraints ).then( onStreamInit, onStreamError ); |
||||
} |
||||
|
||||
if ( global.navigator.getUserMedia ) { |
||||
return new global.Promise( function( resolve, reject ) { |
||||
global.navigator.getUserMedia( constraints, resolve, reject ); |
||||
}).then( onStreamInit, onStreamError ); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.pause = function(){ |
||||
if ( this.state === "recording" ){ |
||||
this.state = "paused"; |
||||
this.eventTarget.dispatchEvent( new global.Event( 'pause' ) ); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.removeEventListener = function( type, listener, useCapture ){ |
||||
this.eventTarget.removeEventListener( type, listener, useCapture ); |
||||
}; |
||||
|
||||
Recorder.prototype.resume = function() { |
||||
if ( this.state === "paused" ) { |
||||
this.state = "recording"; |
||||
this.eventTarget.dispatchEvent( new global.Event( 'resume' ) ); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.setMonitorGain = function( gain ){ |
||||
this.monitorNode.gain.value = gain; |
||||
}; |
||||
|
||||
Recorder.prototype.start = function(){ |
||||
if ( this.state === "inactive" && this.stream ) { |
||||
var that = this; |
||||
this.encoder = new global.Worker( this.config.encoderPath ); |
||||
|
||||
if (this.config.streamPages){ |
||||
this.encoder.addEventListener( "message", function( e ) { |
||||
that.streamPage( e.data ); |
||||
}); |
||||
} |
||||
|
||||
else { |
||||
this.recordedPages = []; |
||||
this.totalLength = 0; |
||||
this.encoder.addEventListener( "message", function( e ) { |
||||
that.storePage( e.data ); |
||||
}); |
||||
} |
||||
|
||||
// First buffer can contain old data. Don't encode it.
|
||||
this.encodeBuffers = function(){ |
||||
delete this.encodeBuffers; |
||||
}; |
||||
|
||||
this.state = "recording"; |
||||
this.monitorNode.connect( this.audioContext.destination ); |
||||
this.scriptProcessorNode.connect( this.audioContext.destination ); |
||||
this.eventTarget.dispatchEvent( new global.Event( 'start' ) ); |
||||
this.encoder.postMessage( this.config ); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.stop = function(){ |
||||
if ( this.state !== "inactive" ) { |
||||
this.state = "inactive"; |
||||
this.monitorNode.disconnect(); |
||||
this.scriptProcessorNode.disconnect(); |
||||
|
||||
if ( !this.config.leaveStreamOpen ) { |
||||
this.clearStream(); |
||||
} |
||||
|
||||
this.audioContext.close(); |
||||
this.audioContext = null; |
||||
|
||||
this.encoder.postMessage({ command: "done" }); |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.storePage = function( page ) { |
||||
if ( page === null ) { |
||||
var outputData = new Uint8Array( this.totalLength ); |
||||
var outputIndex = 0; |
||||
|
||||
for ( var i = 0; i < this.recordedPages.length; i++ ) { |
||||
outputData.set( this.recordedPages[i], outputIndex ); |
||||
outputIndex += this.recordedPages[i].length; |
||||
} |
||||
|
||||
this.eventTarget.dispatchEvent( new global.CustomEvent( 'dataAvailable', { |
||||
detail: outputData |
||||
})); |
||||
|
||||
this.recordedPages = []; |
||||
this.eventTarget.dispatchEvent( new global.Event( 'stop' ) ); |
||||
} |
||||
|
||||
else { |
||||
this.recordedPages.push( page ); |
||||
this.totalLength += page.length; |
||||
} |
||||
}; |
||||
|
||||
Recorder.prototype.streamPage = function( page ) { |
||||
if ( page === null ) { |
||||
this.eventTarget.dispatchEvent( new global.Event( 'stop' ) ); |
||||
} |
||||
|
||||
else { |
||||
this.eventTarget.dispatchEvent( new global.CustomEvent( 'dataAvailable', { |
||||
detail: page |
||||
})); |
||||
} |
||||
}; |
||||
|
||||
|
||||
// Exports
|
||||
global.Recorder = Recorder; |
||||
|
||||
if ( typeof define === 'function' && define.amd ) { |
||||
define( [], function() { |
||||
return Recorder; |
||||
}); |
||||
} |
||||
|
||||
else if ( typeof module == 'object' && module.exports ) { |
||||
module.exports = Recorder; |
||||
} |
||||
|
||||
})(root); |
@ -0,0 +1 @@
|
||||
"use strict";var root="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this;!function(e){var t=function(n){var i=this;if(!t.isRecordingSupported())throw new Error("Recording is not supported in this browser");this.state="inactive",this.eventTarget=e.document.createDocumentFragment(),this.audioContext=new e.AudioContext,this.monitorNode=this.audioContext.createGain(),this.config=n=n||{},this.config.command="init",this.config.bufferLength=n.bufferLength||4096,this.config.monitorGain=n.monitorGain||0,this.config.numberOfChannels=n.numberOfChannels||1,this.config.originalSampleRate=this.audioContext.sampleRate,this.config.encoderSampleRate=n.encoderSampleRate||48e3,this.config.encoderPath=n.encoderPath||"encoderWorker.min.js",this.config.streamPages=n.streamPages||!1,this.config.leaveStreamOpen=n.leaveStreamOpen||!1,this.config.maxBuffersPerPage=n.maxBuffersPerPage||40,this.config.encoderApplication=n.encoderApplication||2049,this.config.encoderFrameSize=n.encoderFrameSize||20,this.config.resampleQuality=n.resampleQuality||3,this.config.streamOptions=n.streamOptions||{optional:[],mandatory:{googEchoCancellation:!1,googAutoGainControl:!1,googNoiseSuppression:!1,googHighpassFilter:!1}},this.setMonitorGain(this.config.monitorGain),this.scriptProcessorNode=this.audioContext.createScriptProcessor(this.config.bufferLength,this.config.numberOfChannels,this.config.numberOfChannels),this.scriptProcessorNode.onaudioprocess=function(e){i.encodeBuffers(e.inputBuffer)}};t.isRecordingSupported=function(){return e.AudioContext&&e.navigator&&(e.navigator.getUserMedia||e.navigator.mediaDevices&&e.navigator.mediaDevices.getUserMedia)},t.prototype.addEventListener=function(e,t,n){this.eventTarget.addEventListener(e,t,n)},t.prototype.clearStream=function(){this.stream&&(this.stream.getTracks?this.stream.getTracks().forEach(function(e){e.stop()}):this.stream.stop(),delete this.stream)},t.prototype.encodeBuffers=function(e){if("recording"===this.state){for(var t=[],n=0;n<e.numberOfChannels;n++)t[n]=e.getChannelData(n);this.encoder.postMessage({command:"encode",buffers:t})}},t.prototype.initStream=function(){var t=this,n=function(n){return t.stream=n,t.sourceNode=t.audioContext.createMediaStreamSource(n),t.sourceNode.connect(t.scriptProcessorNode),t.sourceNode.connect(t.monitorNode),t.eventTarget.dispatchEvent(new e.Event("streamReady")),n},i=function(n){t.eventTarget.dispatchEvent(new e.ErrorEvent("streamError",{error:n}))},o={audio:this.config.streamOptions};return this.stream?(this.eventTarget.dispatchEvent(new e.Event("streamReady")),e.Promise.resolve(this.stream)):e.navigator.mediaDevices&&e.navigator.mediaDevices.getUserMedia?e.navigator.mediaDevices.getUserMedia(o).then(n,i):e.navigator.getUserMedia?new e.Promise(function(t,n){e.navigator.getUserMedia(o,t,n)}).then(n,i):void 0},t.prototype.pause=function(){"recording"===this.state&&(this.state="paused",this.eventTarget.dispatchEvent(new e.Event("pause")))},t.prototype.removeEventListener=function(e,t,n){this.eventTarget.removeEventListener(e,t,n)},t.prototype.resume=function(){"paused"===this.state&&(this.state="recording",this.eventTarget.dispatchEvent(new e.Event("resume")))},t.prototype.setMonitorGain=function(e){this.monitorNode.gain.value=e},t.prototype.start=function(){if("inactive"===this.state&&this.stream){var t=this;this.encoder=new e.Worker(this.config.encoderPath),this.config.streamPages?this.encoder.addEventListener("message",function(e){t.streamPage(e.data)}):(this.recordedPages=[],this.totalLength=0,this.encoder.addEventListener("message",function(e){t.storePage(e.data)})),this.encodeBuffers=function(){delete this.encodeBuffers},this.state="recording",this.monitorNode.connect(this.audioContext.destination),this.scriptProcessorNode.connect(this.audioContext.destination),this.eventTarget.dispatchEvent(new e.Event("start")),this.encoder.postMessage(this.config)}},t.prototype.stop=function(){"inactive"!==this.state&&(this.state="inactive",this.monitorNode.disconnect(),this.scriptProcessorNode.disconnect(),this.config.leaveStreamOpen||this.clearStream(),this.encoder.postMessage({command:"done"}))},t.prototype.storePage=function(t){if(null===t){for(var n=new Uint8Array(this.totalLength),i=0,o=0;o<this.recordedPages.length;o++)n.set(this.recordedPages[o],i),i+=this.recordedPages[o].length;this.eventTarget.dispatchEvent(new e.CustomEvent("dataAvailable",{detail:n})),this.recordedPages=[],this.eventTarget.dispatchEvent(new e.Event("stop"))}else this.recordedPages.push(t),this.totalLength+=t.length},t.prototype.streamPage=function(t){null===t?this.eventTarget.dispatchEvent(new e.Event("stop")):this.eventTarget.dispatchEvent(new e.CustomEvent("dataAvailable",{detail:t}))},e.Recorder=t,"function"==typeof define&&define.amd?define([],function(){return t}):"object"==typeof module&&module.exports&&(module.exports=t)}(root); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue