├── LICENSE ├── README.md ├── librerouter.md ├── librerouter.png ├── raspberry-pi-3b.md └── src └── sbot ├── Templates ├── Template.dwt.php └── _notes │ └── dwsync.xml ├── _notes └── dwsync.xml ├── changename.php ├── client.php ├── create.php ├── global.php ├── images ├── SSB-logo.png └── _notes │ └── dwsync.xml ├── index.php ├── login.php ├── main.css ├── video-hls.js.bak ├── ad-cue-tags.js ├── bin-utils.js ├── config.js ├── decrypter-worker.js ├── master-playlist-controller.js ├── media-groups.js ├── media-segment-request.js ├── playback-watcher.js ├── playlist-loader.js ├── playlist-selectors.js ├── playlist.js ├── ranges.js ├── reload-source-on-error.js ├── rendition-mixin.js ├── resolve-url.js ├── segment-loader.js ├── source-updater.js ├── sync-controller.js ├── util │ └── codecs.js ├── videojs-contrib-hls.js ├── vtt-segment-loader.js └── xhr.js ├── video-js.min.css ├── video.js.bak ├── alt │ ├── video-js-cdn.css │ ├── video-js-cdn.min.css │ ├── video.core.js │ ├── video.core.min.js │ ├── video.core.novtt.js │ ├── video.core.novtt.min.js │ ├── video.novtt.js │ └── video.novtt.min.js ├── font │ ├── VideoJS.svg │ ├── VideoJS.ttf │ └── VideoJS.woff ├── lang │ ├── ar.js │ ├── ba.js │ ├── bg.js │ ├── ca.js │ ├── cs.js │ ├── da.js │ ├── de.js │ ├── el.js │ ├── en.js │ ├── es.js │ ├── fa.js │ ├── fi.js │ ├── fr.js │ ├── gl.js │ ├── he.js │ ├── hr.js │ ├── hu.js │ ├── it.js │ ├── ja.js │ ├── ko.js │ ├── nb.js │ ├── nl.js │ ├── nn.js │ ├── pl.js │ ├── pt-BR.js │ ├── pt-PT.js │ ├── ru.js │ ├── sk.js │ ├── sr.js │ ├── sv.js │ ├── tr.js │ ├── uk.js │ ├── vi.js │ ├── zh-CN.js │ └── zh-TW.js ├── video-js.css ├── video-js.min.css ├── video.cjs.js ├── video.es.js ├── video.js └── video.min.js ├── video.min.js ├── view.php └── view2.php /README.md: -------------------------------------------------------------------------------- 1 | Meshstream 2 | ========== 3 | 4 | The purpose is to show what can be accomplished with peer-to-peer applications running over a wireless mesh network that is completely isolated from the Internet. 5 | 6 | Meshstream demonstrates: 7 | 8 | * Live video streaming over content addressable storage (IPFS) 9 | * Sharing of multimedia content over a peer-to-peer social network (SSB) 10 | * Mesh networking over long-range wireless links using open hardware (LibreRouter) 11 | 12 | Each physical node consists of a LibreRouter + a Raspberry Pi, running software developed by Toronto Mesh that use IPFS and SSB. One node will stream video off of a Raspberry Pi camera, publishes to the private IPFS and SSB network formed by these devices, then other nodes can view the embedded player on the SSB timeline of the video publisher. The user experience is similar to streaming a YouTube video and sharing the link on your Facebook, then your friends discover that video via their social feed and view the live stream from the embedded player. 13 | 14 | ## Set up Meshstream 15 | 16 | The current iteration is prepared for [Decentralized Web Summit 2018](https://decentralizedweb.net). Things are pieced together in a short time with limited access to hardware, so set up instructions may be incomplete and the software is quite hacky. However, browsing through the instructions will give you a good idea of how the all the pieces fit together. 17 | 18 | ### Hardware 19 | 20 | * _Two_ [LibreRouter](https://github.com/libremesh/librerouter) v1 prototype boards each with _one_ [HPM5G radio assembly](https://github.com/tomeshnet/documents/blob/master/technical/20180530_hpm5g-radio-tests.md) 21 | * _Two_ Raspberry Pi 3B devices 22 | * _One_ Raspberry Pi camera 23 | * Ethernet cables and power 24 | 25 | ### Software 26 | 27 | Follow [Configurations for the LibreRouter v1](librerouter.md) to configure each LibreRouter. 28 | 29 | Follow [Configurations for the Raspberry Pi 3B](raspberry-pi-3b.md) to configure each Raspberry Pi. -------------------------------------------------------------------------------- /librerouter.md: -------------------------------------------------------------------------------- 1 | Configurations for the LibreRouter v1 2 | ===================================== 3 | 4 | ## Compile LibreMesh or LEDE 5 | 6 | Compile requires a special SDK and specific paths to be observed. 7 | 8 | ### Prepare the build 9 | 10 | ``` 11 | apt-get install subversion 12 | mkdir -p /home/nicolas/OS_projects/lime-sdk-LR 13 | cd /home/nicolas/OS_projects 14 | wget http://blog.altermundi.net/media/uploads/lime-sdk-LR.tar.bz 15 | tar xvf lime-sdk-LR.tar.bz 16 | cd lime-sdk-LR 17 | rm -rf communities 18 | ./cooker --update-communities --update-feeds 19 | ./cooker -b ar71xx/generic 20 | ``` 21 | 22 | ### Compile LibreMesh 23 | 24 | Build image using: 25 | 26 | ``` 27 | ./cooker -c ar71xx/generic --flavor=lime_default --profile=librerouter-v1 28 | ``` 29 | 30 | ### Compile LEDE 31 | 32 | `dnsmasq` will not build. Edit `flavours.conf` and change the `led_vanilla` line to: 33 | 34 | ``` 35 | lede_vanilla="-dnsmasq" 36 | ``` 37 | 38 | Build image using: 39 | 40 | ``` 41 | ./cooker -c ar71xx/generic --flavor=lede_vanilla --profile=librerouter-v1 42 | ``` 43 | 44 | Compile with env variables `J=1 V=s` for debugging info. 45 | 46 | ### Outputs 47 | 48 | The files will be placed in `/home/nicolas/OS_projects/lime-sdk-LR/output/ar71xx/generic/librerouter-v1`. There will be a folder for each "flavour". 49 | 50 | The output folder will have these files of interest: 51 | 52 | | File ending in | Function | 53 | |:------------------------------------------------------|:------------------------| 54 | | ar71xx-generic-librerouter-v1-squashfs-sysupgrade.bin | Bin used for sysupgrade | 55 | | ar71xx-generic-root.squashfs | Root file system | 56 | | ar71xx-generic-uImage-lzma.bin | Kernel | 57 | 58 | ## Flash 59 | 60 | ### First flash from factory 61 | 62 | To be able to boot the image successfully you will need to change a parameter in U-Boot. From factory, `bootargs` are set to `bootargs=console=ttyS0,115200 root=31:02 rootfstype=squashfs init=/sbin/init mtdparts=ath-nor0:256k(u-boot),64k(u-boot-env),6336k(rootfs),1408k(uImage),8256k(mib0),64k(ART)`. 63 | 64 | To set this paramater you will need to enter the following at the U-Boot `ath>` prompt (see U-Boot section below for more information): 65 | 66 | ``` 67 | setenv bootargs "board=LIBREROUTERV1 console=ttyS0,115200" 68 | saveenv 69 | ``` 70 | 71 | ### Flash using sysupgrade 72 | 73 | Currently unable to flash with sysupgrade that will work beyond a single reboot. 74 | 75 | `sysupgrade img.bin` 76 | 77 | or 78 | 79 | `sysupgrade -F img.bin` 80 | 81 | ### Flash using U-Boot 82 | 83 | This method seems to create a stable flash. It requires you to flash two separate sections, the kernel and the rootfs. 84 | 85 | To use this method you must first prepare a computer/device that will hold the required files. Configure the computer as follows: 86 | 87 | * Configure the IP Address to be `192.168.1.10` 88 | * Install a tftpd server ([tftpd32](http://tftpd32.jounin.net/) for Windows, `tftpd-hpa` for Linux) 89 | * Place the required files in the root of the tftpd server 90 | * Connect to the LibreRouter via ethernet (can be direct of via a network switch) 91 | 92 | #### Flash rootfs 93 | 94 | ``` 95 | tftp 0x80060000 openwrt-lime-default-ar71xx-generic-root.squashfs 96 | erase 0x9f050000 +0xE30000 97 | cp.b 0x80060000 0x9f050000 $filesize 98 | ``` 99 | 100 | #### Flash kernel 101 | 102 | ``` 103 | tftp 0x80060000 openwrt-lime-default-ar71xx-generic-uImage-lzma.bin 104 | erase 0x9fE80000 +0x170000 105 | cp.b 0x80060000 0x9fE80000 $filesize 106 | ``` 107 | 108 | ### Boot 109 | 110 | To reboot from U-Boot enter `reset`, or you can just boot using `boot`. 111 | 112 | ### Explanations 113 | 114 | Flashing is done with these 3 commands: 115 | 116 | * `tftp ` 117 | * `tftp` downloads a file from a predefined tftp server and writes it to a memory address 118 | * `` is where you wish to write the file (we use `0x80060000` which is a location in RAM) 119 | * `` is the file name on the tftp server you wish to get 120 | 121 | * `erase +` 122 | * `erase` will erase information at a specific memory address for a specific length 123 | * `` is the start of the memory we wish to erase 124 | * `` is the amount of memory to erase 125 | 126 | * `cp.b ` 127 | * `cp.b` is a binary copy from one memory location to another 128 | * `` is where the data will be copied from (we use `0x80060000` which is where we stored the file from tftpd) 129 | * `` is the size of the file (we use a predefined variable `$filesize`) 130 | 131 | ## General notes 132 | 133 | ### Known issues 134 | 135 | * `eth0` does not seem to work. It may be connected directly to a WAN port that does not seem to be on the board. You want to do everything on `eth1` 136 | * Do not cold boot with serial connected. It will not work 137 | * The 2.4 GHz radio is broken. Seems to work only for a very local access point 138 | 139 | ### Fix fake MAC addresses 140 | 141 | Run the following in a console to generate new MAC addresses for all wireless and physical interfaces: 142 | 143 | ``` 144 | rand4bytes=$(head -c 256 /dev/urandom | md5sum | sed 's/\(..\)/\1:/g' | head -c 11) 145 | 146 | uci set network.lan.macaddr="02:$rand4bytes":10 147 | uci commit network 148 | 149 | for index in 0 1 2; do 150 | uci set wireless.radio$index.macaddr="02:$rand4bytes":5$index 151 | done 152 | ``` 153 | 154 | ### OpenWRT/LEDE 155 | 156 | Device will boot with the default IP address of `192.168.1.1`. 157 | 158 | Full config script found here: https://github.com/tomeshnet/meshstream/issues/9 159 | 160 | ### Connect device to Internet 161 | 162 | #### Connect using commands 163 | 164 | Set an IP address on your LAN: 165 | 166 | ``` 167 | ifconfig br-lan 192.168.x.x 168 | ``` 169 | 170 | Set the gateway of your LAN: 171 | 172 | ``` 173 | route add -net 0.0.0.0 gw 192.168.x.x 174 | ``` 175 | 176 | Set the DNS server of your LAN: 177 | 178 | ``` 179 | echo nameserver 8.8.8.8 > /etc/resolv.conf 180 | ``` 181 | 182 | #### Connect using config 183 | 184 | Edit the `/etc/config/network` file and under `lan` interface: 185 | 186 | Set your `ipaddr` and add: 187 | 188 | ``` 189 | option gateway '192.168.40.1 190 | option dns '8.8.8.8' 191 | ``` 192 | 193 | Restart `lan`: 194 | 195 | ``` 196 | ifdown lan 197 | ifup lan 198 | ``` 199 | 200 | ### Install LuCI 201 | 202 | ``` 203 | opkg update 204 | opkg install luci 205 | ``` 206 | 207 | Reboot to take effect. 208 | 209 | ### LibreMesh 210 | 211 | Default address is 10.13.0.1. Connecting to the WiFi is the easiest way to access. 212 | 213 | LuCI is pretty broken on LibreMesh. 214 | 215 | ### U-Boot 216 | 217 | #### Serial Interface 218 | 219 | To access U-Boot you need to use a TTL serial adapter running at 3.3v. This is connected to the header labeled `J4` on the board. 220 | 221 | **Header J4:** 222 | 223 | ``` 224 | 1 - VCC (optional) 225 | 2 - RX (TX on your dongle) 226 | 3 - TX (RX on your dongle) 227 | 4 - Ground 228 | ``` 229 | 230 | Set your serial port to baud rate of `115200`. 231 | 232 | ![Serial connection on LibreRouter v1](librerouter.png?raw=true) 233 | 234 | **Note:** You cannot have the dongle connected when powering up the LibreRouter. You must disconnected for the first few seconds from cold boot. 235 | 236 | #### Entering U-Boot 237 | 238 | During the boot process you will be notified to press any key. This delay is usually 3 seconds. Simply press any key. If the boot failed it will automatically go into U-Boot. 239 | 240 | #### Entering commands 241 | 242 | Few things to note when entering U-Boot commands: 243 | 244 | * Enter commands exactly. You risk bricking the board (making it impossible, or nearly impossible, to recover the device) 245 | * U-Boot is very sensitive, an extra leading whitespace will make a command fail 246 | * Do not copy and paste multiple lines, it will not work. One line at a time -------------------------------------------------------------------------------- /librerouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomeshnet/meshstream/cf65ad551c6f1dcfda6e7404cc85994f93be499b/librerouter.png -------------------------------------------------------------------------------- /raspberry-pi-3b.md: -------------------------------------------------------------------------------- 1 | Configurations for the Raspberry Pi 3B 2 | ====================================== 3 | 4 | There are two Raspberry Pis configured slightly differently, one is streaming a video from the camera and the other one is playing the video off of an embedded player in a SSB feed. Let's call them `Streamer Pi` (the one with the Raspberry Pi camera) and `Subscriber Pi`. The steps are almost identical for both: 5 | 6 | 1. Flash Raspbian 7 | 8 | 1. Install [prototype](https://github.com/tomeshnet/prototype-cjdns-pi) at commit `9cc421b9e8c9f4a7340e7a4c85208de6a251c18c` (which at the time of writing is identical as `develop`) with the following features enabled: 9 | 10 | WITH_WIFI_AP 11 | WITH_IPFS 12 | WITH_IPFS_PI_STREAM (for Streamer Pi only) 13 | WITH_SSB 14 | WITH_SSB_WEB 15 | WITH_EXTRA_TOOLS 16 | 17 | 1. Run `ipfs id` on one Pi and on the other node, use `ipfs bootstrap add` to add its cjdns-transported IPFS address so they will peer with one another 18 | 19 | 1. Assign IPv4 addresses to the `eth0` interface of each Pi by adding to `/etc/network/interfaces` with the following, with a different value of `x` for each Pi: 20 | 21 | allow-hotplug eth0 22 | iface eth0 inet static 23 | address 192.168.10.x 24 | netmask 255.255.255.0 25 | network 192.168.10.0 26 | broadcast 192.168.10.255 27 | 28 | This allows for SSB discovery because at the time of writing, discovery over IPv6 is only being worked on in sbot 29 | 30 | 1. Replace `PI_ROOT/var/www/sbot` with `REPO_ROOT/src/sbot` from this repository to get fixes that still need to be upstreamed 31 | 32 | 1. Change `M3U8_SIZE` to a smaller value in `/usr/bin/process-stream.sh` such as `2` for less latency in the live stream 33 | 34 | 1. Occassionally it is necessary to check `df` for space and `ipfs repo gc` to prevent using up all the SD card space 35 | 36 | ## Install ipfs-live-streaming player UI (optional) 37 | 38 | 1. Download the [ipfs-live-streaming video player](https://github.com/tomeshnet/ipfs-live-streaming/tree/enable-ipns/terraform/shared/video-player) to `PI_ROOT/var/www/video-player` 39 | 40 | 1. Create `/etc/nginx/site-path-enabled/ipfs-live-streaming.conf` with: 41 | 42 | location /ipfs-live-streaming { 43 | alias /var/www/video-player; 44 | index index.html index.htm index.nginx-debian.html index.php; 45 | try_files $uri $uri/ =404; 46 | location ~ \.php$ { 47 | try_files $uri =404; 48 | fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; 49 | fastcgi_index index.php; 50 | fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name; 51 | include fastcgi_params; 52 | } 53 | } 54 | 55 | 1. Edit `/var/www/video-player/js/common.js` where `` is that of Streamer Pi: 56 | 57 | var ipfs_gateway_self = 'http://10.0.0.1'; // IPFS gateway of this node 58 | var ipfs_gateway_origin = 'http://10.0.0.1'; // IPFS gateway of origin stream 59 | //var m3u8_ipfs = 'live.m3u8'; // File path to m3u8 with IPFS content via HTTP server 60 | var m3u8_ipfs = 'http://10.0.0.1/ipns/'; // URL to m3u8 via IPNS (uncomment to enable) 61 | var m3u8_http_urls = []; // Optional list of URLs to m3u8 over HTTP 62 | 63 | 1. Restart nginx -------------------------------------------------------------------------------- /src/sbot/Templates/Template.dwt.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Untitled Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 25 |
26 | Body 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/sbot/Templates/_notes/dwsync.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sbot/_notes/dwsync.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/sbot/changename.php: -------------------------------------------------------------------------------- 1 | changeName($_POST['name']); 5 | header("location: view.php"); 6 | } 7 | ?> 8 | 9 | 10 | 11 | 12 | 13 | Untitled Document 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 32 |
33 | 34 |

Change Name

35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/sbot/client.php: -------------------------------------------------------------------------------- 1 | getLogin(); 14 | shell_exec ("nodejs /var/www/backend/post.js $l \"$msg\" 2>&1"); 15 | return 1; 16 | } 17 | } 18 | function toDate($ts) { 19 | $ts=$ts/1000; 20 | return date('Y-m-d H:i:s',$ts); 21 | } 22 | function getName($r) { 23 | $l=$this->getLogin(); 24 | if (isset($this->nameRepo[$r])) { 25 | return $this->nameRepo[$r]; 26 | } else { 27 | $v=shell_exec ("nodejs /var/www/backend/getname.js $l \"$r\" 2>&1"); 28 | if (trim($v)=="") { 29 | return $r; 30 | } else { 31 | return $this->nameRepo[$r]=$v; 32 | } 33 | } 34 | } 35 | function render($msg) { 36 | $msg=str_replace("\n","
",$msg); 37 | $msg=preg_replace("#\[!videoIPFS:(.*?)\]#si",$this->renderPlayerIPFS("\\1"),$msg); 38 | $msg=preg_replace("#\[!videoIPNS:(.*?)\]#si",$this->renderPlayerIPNS("\\1"),$msg); 39 | return $msg; 40 | } 41 | var $counter=0; 42 | function renderPlayerIPFS($url) { 43 | $this->counter++; 44 | $res =''; 48 | $res.=""; 49 | return $res; 50 | } 51 | function renderPlayerIPNS($url) { 52 | $this->counter++; 53 | $res =''; 57 | $res.=""; 58 | return $res; 59 | } 60 | function changeName($newName) { 61 | $l=$this->getLogin(); 62 | $n=$newName; 63 | shell_exec ("nodejs /var/www/backend/changename.js $l \"$n\" 2>&1"); 64 | } 65 | function getPeers() { 66 | $source=shell_exec ("sbot gossip.peers"); 67 | $source=json_decode($source,true); 68 | 69 | foreach ($source as $peer) { 70 | if (!isset($peer['failure']) || $peer['failure']=='0') { 71 | $r['name']=$this->getName($peer['key']); 72 | if ($peer['source']=='local') { 73 | $local[]=$r; 74 | } else { 75 | $remote[]=$r; 76 | } 77 | } 78 | } 79 | $peers['local']=$local; 80 | $peers['remote']=$remote; 81 | 82 | return $peers; 83 | } 84 | } 85 | $sbot=new sbotClient(); 86 | ?> 87 | -------------------------------------------------------------------------------- /src/sbot/create.php: -------------------------------------------------------------------------------- 1 | $v) { 17 | $kv=explode("\t",$v); 18 | if ($kv[0]==$l) { 19 | die("user account already exists"); 20 | } 21 | } 22 | $p=GetPasswordHash($l,$p); 23 | file_put_contents ("/var/www/backend/userlist","$l\t$p\n",FILE_APPEND); 24 | header("location: login.php"); 25 | } 26 | ?> 27 | 28 | 29 | 30 | 31 | 32 | Untitled Document 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 51 |
52 | 53 |

54 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/sbot/global.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sbot/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Untitled Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 25 |
26 |

Welcome to SSB Local

27 |

28 | Please click HERE to login. 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/sbot/login.php: -------------------------------------------------------------------------------- 1 | $v) { 11 | $kv=explode("\t",$v); 12 | 13 | if ($kv[0]==$l) { 14 | if ($kv[1] == GetPasswordHash($l,$p)) { 15 | $_SESSION['login']=$l; 16 | header("location: view.php"); 17 | } 18 | } 19 | } 20 | die("incorrect l/p"); 21 | } 22 | ?> 23 | 24 | 25 | 26 | 27 | 28 | Untitled Document 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 47 |
48 |

49 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/sbot/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | maring:0; 3 | padding:0; 4 | 5 | } 6 | .TopMenu { 7 | background-color: #101935; 8 | display: block; 9 | } 10 | .TopMenu ul { 11 | list-style-type: none; 12 | margin: 0; 13 | padding: 0; 14 | display: block; 15 | } 16 | .TopMenu li { 17 | vertical-align: middle; 18 | display: inline-block; 19 | margin:0; 20 | padding:0; 21 | text-align: center; 22 | } 23 | .TopMenu li a { 24 | height:45px; 25 | display: block; 26 | padding: 10px; 27 | color: #FFF; 28 | font-family: Arial, Helvetica, sans-serif; 29 | font-weight:bold; 30 | text-decoration:none; 31 | font-size:16px; 32 | } 33 | .TopMenu li a:hover { 34 | background-color: #252d47; 35 | color: #FFF; 36 | } 37 | /*Login/Create Forms*/ 38 | .login { 39 | border: 1px solid blue; 40 | display:block; 41 | width:300px; 42 | margin:0 auto; 43 | padding:20px; 44 | background:#D9ECFF; 45 | } 46 | .login h1 { 47 | padding:0; 48 | margin-top:0; 49 | font-family:Arial, Helvetica, sans-serif; 50 | } 51 | .login .textbox { 52 | padding:5px; 53 | margin:5px; 54 | border:none; 55 | } -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/ad-cue-tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ad-cue-tags.js 3 | */ 4 | import window from 'global/window'; 5 | 6 | /** 7 | * Searches for an ad cue that overlaps with the given mediaTime 8 | */ 9 | const findAdCue = function(track, mediaTime) { 10 | let cues = track.cues; 11 | 12 | for (let i = 0; i < cues.length; i++) { 13 | let cue = cues[i]; 14 | 15 | if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 16 | return cue; 17 | } 18 | } 19 | return null; 20 | }; 21 | 22 | const updateAdCues = function(media, track, offset = 0) { 23 | if (!media.segments) { 24 | return; 25 | } 26 | 27 | let mediaTime = offset; 28 | let cue; 29 | 30 | for (let i = 0; i < media.segments.length; i++) { 31 | let segment = media.segments[i]; 32 | 33 | if (!cue) { 34 | // Since the cues will span for at least the segment duration, adding a fudge 35 | // factor of half segment duration will prevent duplicate cues from being 36 | // created when timing info is not exact (e.g. cue start time initialized 37 | // at 10.006677, but next call mediaTime is 10.003332 ) 38 | cue = findAdCue(track, mediaTime + (segment.duration / 2)); 39 | } 40 | 41 | if (cue) { 42 | if ('cueIn' in segment) { 43 | // Found a CUE-IN so end the cue 44 | cue.endTime = mediaTime; 45 | cue.adEndTime = mediaTime; 46 | mediaTime += segment.duration; 47 | cue = null; 48 | continue; 49 | } 50 | 51 | if (mediaTime < cue.endTime) { 52 | // Already processed this mediaTime for this cue 53 | mediaTime += segment.duration; 54 | continue; 55 | } 56 | 57 | // otherwise extend cue until a CUE-IN is found 58 | cue.endTime += segment.duration; 59 | 60 | } else { 61 | if ('cueOut' in segment) { 62 | cue = new window.VTTCue(mediaTime, 63 | mediaTime + segment.duration, 64 | segment.cueOut); 65 | cue.adStartTime = mediaTime; 66 | // Assumes tag format to be 67 | // #EXT-X-CUE-OUT:30 68 | cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 69 | track.addCue(cue); 70 | } 71 | 72 | if ('cueOutCont' in segment) { 73 | // Entered into the middle of an ad cue 74 | let adOffset; 75 | let adTotal; 76 | 77 | // Assumes tag formate to be 78 | // #EXT-X-CUE-OUT-CONT:10/30 79 | [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat); 80 | 81 | cue = new window.VTTCue(mediaTime, 82 | mediaTime + segment.duration, 83 | ''); 84 | cue.adStartTime = mediaTime - adOffset; 85 | cue.adEndTime = cue.adStartTime + adTotal; 86 | track.addCue(cue); 87 | } 88 | } 89 | mediaTime += segment.duration; 90 | } 91 | }; 92 | 93 | export default { 94 | updateAdCues, 95 | findAdCue 96 | }; 97 | 98 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/bin-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file bin-utils.js 3 | */ 4 | 5 | /** 6 | * convert a TimeRange to text 7 | * 8 | * @param {TimeRange} range the timerange to use for conversion 9 | * @param {Number} i the iterator on the range to convert 10 | */ 11 | const textRange = function(range, i) { 12 | return range.start(i) + '-' + range.end(i); 13 | }; 14 | 15 | /** 16 | * format a number as hex string 17 | * 18 | * @param {Number} e The number 19 | * @param {Number} i the iterator 20 | */ 21 | const formatHexString = function(e, i) { 22 | let value = e.toString(16); 23 | 24 | return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 25 | }; 26 | const formatAsciiString = function(e) { 27 | if (e >= 0x20 && e < 0x7e) { 28 | return String.fromCharCode(e); 29 | } 30 | return '.'; 31 | }; 32 | 33 | /** 34 | * Creates an object for sending to a web worker modifying properties that are TypedArrays 35 | * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 36 | * 37 | * @param {Object} message 38 | * Object of properties and values to send to the web worker 39 | * @return {Object} 40 | * Modified message with TypedArray values expanded 41 | * @function createTransferableMessage 42 | */ 43 | const createTransferableMessage = function(message) { 44 | const transferable = {}; 45 | 46 | Object.keys(message).forEach((key) => { 47 | const value = message[key]; 48 | 49 | if (ArrayBuffer.isView(value)) { 50 | transferable[key] = { 51 | bytes: value.buffer, 52 | byteOffset: value.byteOffset, 53 | byteLength: value.byteLength 54 | }; 55 | } else { 56 | transferable[key] = value; 57 | } 58 | }); 59 | 60 | return transferable; 61 | }; 62 | 63 | /** 64 | * Returns a unique string identifier for a media initialization 65 | * segment. 66 | */ 67 | const initSegmentId = function(initSegment) { 68 | let byterange = initSegment.byterange || { 69 | length: Infinity, 70 | offset: 0 71 | }; 72 | 73 | return [ 74 | byterange.length, byterange.offset, initSegment.resolvedUri 75 | ].join(','); 76 | }; 77 | 78 | /** 79 | * utils to help dump binary data to the console 80 | */ 81 | const utils = { 82 | hexDump(data) { 83 | let bytes = Array.prototype.slice.call(data); 84 | let step = 16; 85 | let result = ''; 86 | let hex; 87 | let ascii; 88 | 89 | for (let j = 0; j < bytes.length / step; j++) { 90 | hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 91 | ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 92 | result += hex + ' ' + ascii + '\n'; 93 | } 94 | return result; 95 | }, 96 | tagDump(tag) { 97 | return utils.hexDump(tag.bytes); 98 | }, 99 | textRanges(ranges) { 100 | let result = ''; 101 | let i; 102 | 103 | for (i = 0; i < ranges.length; i++) { 104 | result += textRange(ranges, i) + ' '; 105 | } 106 | return result; 107 | }, 108 | createTransferableMessage, 109 | initSegmentId 110 | }; 111 | 112 | export default utils; 113 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | GOAL_BUFFER_LENGTH: 30, 3 | MAX_GOAL_BUFFER_LENGTH: 60, 4 | GOAL_BUFFER_LENGTH_RATE: 1, 5 | // A fudge factor to apply to advertised playlist bitrates to account for 6 | // temporary flucations in client bandwidth 7 | BANDWIDTH_VARIANCE: 1.2, 8 | // How much of the buffer must be filled before we consider upswitching 9 | BUFFER_LOW_WATER_LINE: 0, 10 | MAX_BUFFER_LOW_WATER_LINE: 30, 11 | BUFFER_LOW_WATER_LINE_RATE: 1 12 | }; 13 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/decrypter-worker.js: -------------------------------------------------------------------------------- 1 | import window from 'global/window'; 2 | import {Decrypter} from 'aes-decrypter'; 3 | import { createTransferableMessage } from './bin-utils'; 4 | 5 | /** 6 | * Our web worker interface so that things can talk to aes-decrypter 7 | * that will be running in a web worker. the scope is passed to this by 8 | * webworkify. 9 | * 10 | * @param {Object} self 11 | * the scope for the web worker 12 | */ 13 | const DecrypterWorker = function(self) { 14 | self.onmessage = function(event) { 15 | const data = event.data; 16 | const encrypted = new Uint8Array(data.encrypted.bytes, 17 | data.encrypted.byteOffset, 18 | data.encrypted.byteLength); 19 | const key = new Uint32Array(data.key.bytes, 20 | data.key.byteOffset, 21 | data.key.byteLength / 4); 22 | const iv = new Uint32Array(data.iv.bytes, 23 | data.iv.byteOffset, 24 | data.iv.byteLength / 4); 25 | 26 | /* eslint-disable no-new, handle-callback-err */ 27 | new Decrypter(encrypted, 28 | key, 29 | iv, 30 | function(err, bytes) { 31 | window.postMessage(createTransferableMessage({ 32 | source: data.source, 33 | decrypted: bytes 34 | }), [bytes.buffer]); 35 | }); 36 | /* eslint-enable */ 37 | }; 38 | }; 39 | 40 | export default (self) => { 41 | return new DecrypterWorker(self); 42 | }; 43 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/media-segment-request.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import { createTransferableMessage } from './bin-utils'; 3 | 4 | export const REQUEST_ERRORS = { 5 | FAILURE: 2, 6 | TIMEOUT: -101, 7 | ABORTED: -102 8 | }; 9 | 10 | /** 11 | * Turns segment byterange into a string suitable for use in 12 | * HTTP Range requests 13 | * 14 | * @param {Object} byterange - an object with two values defining the start and end 15 | * of a byte-range 16 | */ 17 | const byterangeStr = function(byterange) { 18 | let byterangeStart; 19 | let byterangeEnd; 20 | 21 | // `byterangeEnd` is one less than `offset + length` because the HTTP range 22 | // header uses inclusive ranges 23 | byterangeEnd = byterange.offset + byterange.length - 1; 24 | byterangeStart = byterange.offset; 25 | return 'bytes=' + byterangeStart + '-' + byterangeEnd; 26 | }; 27 | 28 | /** 29 | * Defines headers for use in the xhr request for a particular segment. 30 | * 31 | * @param {Object} segment - a simplified copy of the segmentInfo object 32 | * from SegmentLoader 33 | */ 34 | const segmentXhrHeaders = function(segment) { 35 | let headers = {}; 36 | 37 | if (segment.byterange) { 38 | headers.Range = byterangeStr(segment.byterange); 39 | } 40 | return headers; 41 | }; 42 | 43 | /** 44 | * Abort all requests 45 | * 46 | * @param {Object} activeXhrs - an object that tracks all XHR requests 47 | */ 48 | const abortAll = (activeXhrs) => { 49 | activeXhrs.forEach((xhr) => { 50 | xhr.abort(); 51 | }); 52 | }; 53 | 54 | /** 55 | * Gather important bandwidth stats once a request has completed 56 | * 57 | * @param {Object} request - the XHR request from which to gather stats 58 | */ 59 | const getRequestStats = (request) => { 60 | return { 61 | bandwidth: request.bandwidth, 62 | bytesReceived: request.bytesReceived || 0, 63 | roundTripTime: request.roundTripTime || 0 64 | }; 65 | }; 66 | 67 | /** 68 | * If possible gather bandwidth stats as a request is in 69 | * progress 70 | * 71 | * @param {Event} progressEvent - an event object from an XHR's progress event 72 | */ 73 | const getProgressStats = (progressEvent) => { 74 | const request = progressEvent.target; 75 | const roundTripTime = Date.now() - request.requestTime; 76 | const stats = { 77 | bandwidth: Infinity, 78 | bytesReceived: 0, 79 | roundTripTime: roundTripTime || 0 80 | }; 81 | 82 | stats.bytesReceived = progressEvent.loaded; 83 | // This can result in Infinity if stats.roundTripTime is 0 but that is ok 84 | // because we should only use bandwidth stats on progress to determine when 85 | // abort a request early due to insufficient bandwidth 86 | stats.bandwidth = Math.floor((stats.bytesReceived / stats.roundTripTime) * 8 * 1000); 87 | 88 | return stats; 89 | }; 90 | 91 | /** 92 | * Handle all error conditions in one place and return an object 93 | * with all the information 94 | * 95 | * @param {Error|null} error - if non-null signals an error occured with the XHR 96 | * @param {Object} request - the XHR request that possibly generated the error 97 | */ 98 | const handleErrors = (error, request) => { 99 | if (request.timedout) { 100 | return { 101 | status: request.status, 102 | message: 'HLS request timed-out at URL: ' + request.uri, 103 | code: REQUEST_ERRORS.TIMEOUT, 104 | xhr: request 105 | }; 106 | } 107 | 108 | if (request.aborted) { 109 | return { 110 | status: request.status, 111 | message: 'HLS request aborted at URL: ' + request.uri, 112 | code: REQUEST_ERRORS.ABORTED, 113 | xhr: request 114 | }; 115 | } 116 | 117 | if (error) { 118 | return { 119 | status: request.status, 120 | message: 'HLS request errored at URL: ' + request.uri, 121 | code: REQUEST_ERRORS.FAILURE, 122 | xhr: request 123 | }; 124 | } 125 | 126 | return null; 127 | }; 128 | 129 | /** 130 | * Handle responses for key data and convert the key data to the correct format 131 | * for the decryption step later 132 | * 133 | * @param {Object} segment - a simplified copy of the segmentInfo object 134 | * from SegmentLoader 135 | * @param {Function} finishProcessingFn - a callback to execute to continue processing 136 | * this request 137 | */ 138 | const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => { 139 | const response = request.response; 140 | const errorObj = handleErrors(error, request); 141 | 142 | if (errorObj) { 143 | return finishProcessingFn(errorObj, segment); 144 | } 145 | 146 | if (response.byteLength !== 16) { 147 | return finishProcessingFn({ 148 | status: request.status, 149 | message: 'Invalid HLS key at URL: ' + request.uri, 150 | code: REQUEST_ERRORS.FAILURE, 151 | xhr: request 152 | }, segment); 153 | } 154 | 155 | const view = new DataView(response); 156 | 157 | segment.key.bytes = new Uint32Array([ 158 | view.getUint32(0), 159 | view.getUint32(4), 160 | view.getUint32(8), 161 | view.getUint32(12) 162 | ]); 163 | return finishProcessingFn(null, segment); 164 | }; 165 | 166 | /** 167 | * Handle init-segment responses 168 | * 169 | * @param {Object} segment - a simplified copy of the segmentInfo object 170 | * from SegmentLoader 171 | * @param {Function} finishProcessingFn - a callback to execute to continue processing 172 | * this request 173 | */ 174 | const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, request) => { 175 | const response = request.response; 176 | const errorObj = handleErrors(error, request); 177 | 178 | if (errorObj) { 179 | return finishProcessingFn(errorObj, segment); 180 | } 181 | 182 | // stop processing if received empty content 183 | if (response.byteLength === 0) { 184 | return finishProcessingFn({ 185 | status: request.status, 186 | message: 'Empty HLS segment content at URL: ' + request.uri, 187 | code: REQUEST_ERRORS.FAILURE, 188 | xhr: request 189 | }, segment); 190 | } 191 | 192 | segment.map.bytes = new Uint8Array(request.response); 193 | return finishProcessingFn(null, segment); 194 | }; 195 | 196 | /** 197 | * Response handler for segment-requests being sure to set the correct 198 | * property depending on whether the segment is encryped or not 199 | * Also records and keeps track of stats that are used for ABR purposes 200 | * 201 | * @param {Object} segment - a simplified copy of the segmentInfo object 202 | * from SegmentLoader 203 | * @param {Function} finishProcessingFn - a callback to execute to continue processing 204 | * this request 205 | */ 206 | const handleSegmentResponse = (segment, finishProcessingFn) => (error, request) => { 207 | const response = request.response; 208 | const errorObj = handleErrors(error, request); 209 | 210 | if (errorObj) { 211 | return finishProcessingFn(errorObj, segment); 212 | } 213 | 214 | // stop processing if received empty content 215 | if (response.byteLength === 0) { 216 | return finishProcessingFn({ 217 | status: request.status, 218 | message: 'Empty HLS segment content at URL: ' + request.uri, 219 | code: REQUEST_ERRORS.FAILURE, 220 | xhr: request 221 | }, segment); 222 | } 223 | 224 | segment.stats = getRequestStats(request); 225 | 226 | if (segment.key) { 227 | segment.encryptedBytes = new Uint8Array(request.response); 228 | } else { 229 | segment.bytes = new Uint8Array(request.response); 230 | } 231 | 232 | return finishProcessingFn(null, segment); 233 | }; 234 | 235 | /** 236 | * Decrypt the segment via the decryption web worker 237 | * 238 | * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 239 | * @param {Object} segment - a simplified copy of the segmentInfo object 240 | * from SegmentLoader 241 | * @param {Function} doneFn - a callback that is executed after decryption has completed 242 | */ 243 | const decryptSegment = (decrypter, segment, doneFn) => { 244 | const decryptionHandler = (event) => { 245 | if (event.data.source === segment.requestId) { 246 | decrypter.removeEventListener('message', decryptionHandler); 247 | const decrypted = event.data.decrypted; 248 | 249 | segment.bytes = new Uint8Array(decrypted.bytes, 250 | decrypted.byteOffset, 251 | decrypted.byteLength); 252 | return doneFn(null, segment); 253 | } 254 | }; 255 | 256 | decrypter.addEventListener('message', decryptionHandler); 257 | 258 | // this is an encrypted segment 259 | // incrementally decrypt the segment 260 | decrypter.postMessage(createTransferableMessage({ 261 | source: segment.requestId, 262 | encrypted: segment.encryptedBytes, 263 | key: segment.key.bytes, 264 | iv: segment.key.iv 265 | }), [ 266 | segment.encryptedBytes.buffer, 267 | segment.key.bytes.buffer 268 | ]); 269 | }; 270 | 271 | /** 272 | * The purpose of this function is to get the most pertinent error from the 273 | * array of errors. 274 | * For instance if a timeout and two aborts occur, then the aborts were 275 | * likely triggered by the timeout so return that error object. 276 | */ 277 | const getMostImportantError = (errors) => { 278 | return errors.reduce((prev, err) => { 279 | return err.code > prev.code ? err : prev; 280 | }); 281 | }; 282 | 283 | /** 284 | * This function waits for all XHRs to finish (with either success or failure) 285 | * before continueing processing via it's callback. The function gathers errors 286 | * from each request into a single errors array so that the error status for 287 | * each request can be examined later. 288 | * 289 | * @param {Object} activeXhrs - an object that tracks all XHR requests 290 | * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 291 | * @param {Function} doneFn - a callback that is executed after all resources have been 292 | * downloaded and any decryption completed 293 | */ 294 | const waitForCompletion = (activeXhrs, decrypter, doneFn) => { 295 | let errors = []; 296 | let count = 0; 297 | 298 | return (error, segment) => { 299 | if (error) { 300 | // If there are errors, we have to abort any outstanding requests 301 | abortAll(activeXhrs); 302 | errors.push(error); 303 | } 304 | count += 1; 305 | 306 | if (count === activeXhrs.length) { 307 | // Keep track of when *all* of the requests have completed 308 | segment.endOfAllRequests = Date.now(); 309 | 310 | if (errors.length > 0) { 311 | const worstError = getMostImportantError(errors); 312 | 313 | return doneFn(worstError, segment); 314 | } 315 | if (segment.encryptedBytes) { 316 | return decryptSegment(decrypter, segment, doneFn); 317 | } 318 | // Otherwise, everything is ready just continue 319 | return doneFn(null, segment); 320 | } 321 | }; 322 | }; 323 | 324 | /** 325 | * Simple progress event callback handler that gathers some stats before 326 | * executing a provided callback with the `segment` object 327 | * 328 | * @param {Object} segment - a simplified copy of the segmentInfo object 329 | * from SegmentLoader 330 | * @param {Function} progressFn - a callback that is executed each time a progress event 331 | * is received 332 | * @param {Event} event - the progress event object from XMLHttpRequest 333 | */ 334 | const handleProgress = (segment, progressFn) => (event) => { 335 | segment.stats = videojs.mergeOptions(segment.stats, getProgressStats(event)); 336 | 337 | // record the time that we receive the first byte of data 338 | if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 339 | segment.stats.firstBytesReceivedAt = Date.now(); 340 | } 341 | 342 | return progressFn(event, segment); 343 | }; 344 | 345 | /** 346 | * Load all resources and does any processing necessary for a media-segment 347 | * 348 | * Features: 349 | * decrypts the media-segment if it has a key uri and an iv 350 | * aborts *all* requests if *any* one request fails 351 | * 352 | * The segment object, at minimum, has the following format: 353 | * { 354 | * resolvedUri: String, 355 | * [byterange]: { 356 | * offset: Number, 357 | * length: Number 358 | * }, 359 | * [key]: { 360 | * resolvedUri: String 361 | * [byterange]: { 362 | * offset: Number, 363 | * length: Number 364 | * }, 365 | * iv: { 366 | * bytes: Uint32Array 367 | * } 368 | * }, 369 | * [map]: { 370 | * resolvedUri: String, 371 | * [byterange]: { 372 | * offset: Number, 373 | * length: Number 374 | * }, 375 | * [bytes]: Uint8Array 376 | * } 377 | * } 378 | * ...where [name] denotes optional properties 379 | * 380 | * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 381 | * @param {Object} xhrOptions - the base options to provide to all xhr requests 382 | * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 383 | * decryption routines 384 | * @param {Object} segment - a simplified copy of the segmentInfo object 385 | * from SegmentLoader 386 | * @param {Function} progressFn - a callback that receives progress events from the main 387 | * segment's xhr request 388 | * @param {Function} doneFn - a callback that is executed only once all requests have 389 | * succeeded or failed 390 | * @returns {Function} a function that, when invoked, immediately aborts all 391 | * outstanding requests 392 | */ 393 | export const mediaSegmentRequest = (xhr, 394 | xhrOptions, 395 | decryptionWorker, 396 | segment, 397 | progressFn, 398 | doneFn) => { 399 | const activeXhrs = []; 400 | const finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn); 401 | 402 | // optionally, request the decryption key 403 | if (segment.key) { 404 | const keyRequestOptions = videojs.mergeOptions(xhrOptions, { 405 | uri: segment.key.resolvedUri, 406 | responseType: 'arraybuffer' 407 | }); 408 | const keyRequestCallback = handleKeyResponse(segment, finishProcessingFn); 409 | const keyXhr = xhr(keyRequestOptions, keyRequestCallback); 410 | 411 | activeXhrs.push(keyXhr); 412 | } 413 | 414 | // optionally, request the associated media init segment 415 | if (segment.map && 416 | !segment.map.bytes) { 417 | const initSegmentOptions = videojs.mergeOptions(xhrOptions, { 418 | uri: segment.map.resolvedUri, 419 | responseType: 'arraybuffer', 420 | headers: segmentXhrHeaders(segment.map) 421 | }); 422 | const initSegmentRequestCallback = handleInitSegmentResponse(segment, 423 | finishProcessingFn); 424 | const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 425 | 426 | activeXhrs.push(initSegmentXhr); 427 | } 428 | 429 | const segmentRequestOptions = videojs.mergeOptions(xhrOptions, { 430 | uri: segment.resolvedUri, 431 | responseType: 'arraybuffer', 432 | headers: segmentXhrHeaders(segment) 433 | }); 434 | const segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn); 435 | const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 436 | 437 | segmentXhr.addEventListener('progress', handleProgress(segment, progressFn)); 438 | activeXhrs.push(segmentXhr); 439 | 440 | return () => abortAll(activeXhrs); 441 | }; 442 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/playback-watcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file playback-watcher.js 3 | * 4 | * Playback starts, and now my watch begins. It shall not end until my death. I shall 5 | * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 6 | * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 7 | * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 8 | * my life and honor to the Playback Watch, for this Player and all the Players to come. 9 | */ 10 | 11 | import window from 'global/window'; 12 | import Ranges from './ranges'; 13 | import videojs from 'video.js'; 14 | 15 | // Set of events that reset the playback-watcher time check logic and clear the timeout 16 | const timerCancelEvents = [ 17 | 'seeking', 18 | 'seeked', 19 | 'pause', 20 | 'playing', 21 | 'error' 22 | ]; 23 | 24 | /** 25 | * @class PlaybackWatcher 26 | */ 27 | export default class PlaybackWatcher { 28 | /** 29 | * Represents an PlaybackWatcher object. 30 | * @constructor 31 | * @param {object} options an object that includes the tech and settings 32 | */ 33 | constructor(options) { 34 | this.tech_ = options.tech; 35 | this.seekable = options.seekable; 36 | 37 | this.consecutiveUpdates = 0; 38 | this.lastRecordedTime = null; 39 | this.timer_ = null; 40 | this.checkCurrentTimeTimeout_ = null; 41 | 42 | if (options.debug) { 43 | this.logger_ = videojs.log.bind(videojs, 'playback-watcher ->'); 44 | } 45 | this.logger_('initialize'); 46 | 47 | let canPlayHandler = () => this.monitorCurrentTime_(); 48 | let waitingHandler = () => this.techWaiting_(); 49 | let cancelTimerHandler = () => this.cancelTimer_(); 50 | let fixesBadSeeksHandler = () => this.fixesBadSeeks_(); 51 | 52 | this.tech_.on('seekablechanged', fixesBadSeeksHandler); 53 | this.tech_.on('waiting', waitingHandler); 54 | this.tech_.on(timerCancelEvents, cancelTimerHandler); 55 | this.tech_.on('canplay', canPlayHandler); 56 | 57 | // Define the dispose function to clean up our events 58 | this.dispose = () => { 59 | this.logger_('dispose'); 60 | this.tech_.off('seekablechanged', fixesBadSeeksHandler); 61 | this.tech_.off('waiting', waitingHandler); 62 | this.tech_.off(timerCancelEvents, cancelTimerHandler); 63 | this.tech_.off('canplay', canPlayHandler); 64 | if (this.checkCurrentTimeTimeout_) { 65 | window.clearTimeout(this.checkCurrentTimeTimeout_); 66 | } 67 | this.cancelTimer_(); 68 | }; 69 | } 70 | 71 | /** 72 | * Periodically check current time to see if playback stopped 73 | * 74 | * @private 75 | */ 76 | monitorCurrentTime_() { 77 | this.checkCurrentTime_(); 78 | 79 | if (this.checkCurrentTimeTimeout_) { 80 | window.clearTimeout(this.checkCurrentTimeTimeout_); 81 | } 82 | 83 | // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 84 | this.checkCurrentTimeTimeout_ = 85 | window.setTimeout(this.monitorCurrentTime_.bind(this), 250); 86 | } 87 | 88 | /** 89 | * The purpose of this function is to emulate the "waiting" event on 90 | * browsers that do not emit it when they are waiting for more 91 | * data to continue playback 92 | * 93 | * @private 94 | */ 95 | checkCurrentTime_() { 96 | if (this.tech_.seeking() && this.fixesBadSeeks_()) { 97 | this.consecutiveUpdates = 0; 98 | this.lastRecordedTime = this.tech_.currentTime(); 99 | return; 100 | } 101 | 102 | if (this.tech_.paused() || this.tech_.seeking()) { 103 | return; 104 | } 105 | 106 | let currentTime = this.tech_.currentTime(); 107 | let buffered = this.tech_.buffered(); 108 | 109 | if (this.lastRecordedTime === currentTime && 110 | (!buffered.length || 111 | currentTime + Ranges.SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 112 | // If current time is at the end of the final buffered region, then any playback 113 | // stall is most likely caused by buffering in a low bandwidth environment. The tech 114 | // should fire a `waiting` event in this scenario, but due to browser and tech 115 | // inconsistencies (e.g. The Flash tech does not fire a `waiting` event when the end 116 | // of the buffer is reached and has fallen off the live window). Calling 117 | // `techWaiting_` here allows us to simulate responding to a native `waiting` event 118 | // when the tech fails to emit one. 119 | return this.techWaiting_(); 120 | } 121 | 122 | if (this.consecutiveUpdates >= 5 && 123 | currentTime === this.lastRecordedTime) { 124 | this.consecutiveUpdates++; 125 | this.waiting_(); 126 | } else if (currentTime === this.lastRecordedTime) { 127 | this.consecutiveUpdates++; 128 | } else { 129 | this.consecutiveUpdates = 0; 130 | this.lastRecordedTime = currentTime; 131 | } 132 | } 133 | 134 | /** 135 | * Cancels any pending timers and resets the 'timeupdate' mechanism 136 | * designed to detect that we are stalled 137 | * 138 | * @private 139 | */ 140 | cancelTimer_() { 141 | this.consecutiveUpdates = 0; 142 | 143 | if (this.timer_) { 144 | this.logger_('cancelTimer_'); 145 | clearTimeout(this.timer_); 146 | } 147 | 148 | this.timer_ = null; 149 | } 150 | 151 | /** 152 | * Fixes situations where there's a bad seek 153 | * 154 | * @return {Boolean} whether an action was taken to fix the seek 155 | * @private 156 | */ 157 | fixesBadSeeks_() { 158 | const seeking = this.tech_.seeking(); 159 | const seekable = this.seekable(); 160 | const currentTime = this.tech_.currentTime(); 161 | let seekTo; 162 | 163 | if (seeking && this.afterSeekableWindow_(seekable, currentTime)) { 164 | const seekableEnd = seekable.end(seekable.length - 1); 165 | 166 | // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 167 | seekTo = seekableEnd; 168 | } 169 | 170 | if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) { 171 | const seekableStart = seekable.start(0); 172 | 173 | // sync to the beginning of the live window 174 | // provide a buffer of .1 seconds to handle rounding/imprecise numbers 175 | seekTo = seekableStart + Ranges.SAFE_TIME_DELTA; 176 | } 177 | 178 | if (typeof seekTo !== 'undefined') { 179 | this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + 180 | `seekable range ${Ranges.printableRange(seekable)}. Seeking to ` + 181 | `${seekTo}.`); 182 | 183 | this.tech_.setCurrentTime(seekTo); 184 | return true; 185 | } 186 | 187 | return false; 188 | } 189 | 190 | /** 191 | * Handler for situations when we determine the player is waiting. 192 | * 193 | * @private 194 | */ 195 | waiting_() { 196 | if (this.techWaiting_()) { 197 | return; 198 | } 199 | 200 | // All tech waiting checks failed. Use last resort correction 201 | let currentTime = this.tech_.currentTime(); 202 | let buffered = this.tech_.buffered(); 203 | let currentRange = Ranges.findRange(buffered, currentTime); 204 | 205 | // Sometimes the player can stall for unknown reasons within a contiguous buffered 206 | // region with no indication that anything is amiss (seen in Firefox). Seeking to 207 | // currentTime is usually enough to kickstart the player. This checks that the player 208 | // is currently within a buffered region before attempting a corrective seek. 209 | // Chrome does not appear to continue `timeupdate` events after a `waiting` event 210 | // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 211 | // make sure there is ~3 seconds of forward buffer before taking any corrective action 212 | // to avoid triggering an `unknownwaiting` event when the network is slow. 213 | if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 214 | this.cancelTimer_(); 215 | this.tech_.setCurrentTime(currentTime); 216 | 217 | this.logger_(`Stopped at ${currentTime} while inside a buffered region ` + 218 | `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 219 | `playback by seeking to the current time.`); 220 | 221 | // unknown waiting corrections may be useful for monitoring QoS 222 | this.tech_.trigger({type: 'usage', name: 'hls-unknown-waiting'}); 223 | return; 224 | } 225 | } 226 | 227 | /** 228 | * Handler for situations when the tech fires a `waiting` event 229 | * 230 | * @return {Boolean} 231 | * True if an action (or none) was needed to correct the waiting. False if no 232 | * checks passed 233 | * @private 234 | */ 235 | techWaiting_() { 236 | let seekable = this.seekable(); 237 | let currentTime = this.tech_.currentTime(); 238 | 239 | if (this.tech_.seeking() && this.fixesBadSeeks_()) { 240 | // Tech is seeking or bad seek fixed, no action needed 241 | return true; 242 | } 243 | 244 | if (this.tech_.seeking() || this.timer_ !== null) { 245 | // Tech is seeking or already waiting on another action, no action needed 246 | return true; 247 | } 248 | 249 | if (this.beforeSeekableWindow_(seekable, currentTime)) { 250 | let livePoint = seekable.end(seekable.length - 1); 251 | 252 | this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + 253 | `live point (seekable end) ${livePoint}`); 254 | this.cancelTimer_(); 255 | this.tech_.setCurrentTime(livePoint); 256 | 257 | // live window resyncs may be useful for monitoring QoS 258 | this.tech_.trigger({type: 'usage', name: 'hls-live-resync'}); 259 | return true; 260 | } 261 | 262 | let buffered = this.tech_.buffered(); 263 | let nextRange = Ranges.findNextRange(buffered, currentTime); 264 | 265 | if (this.videoUnderflow_(nextRange, buffered, currentTime)) { 266 | // Even though the video underflowed and was stuck in a gap, the audio overplayed 267 | // the gap, leading currentTime into a buffered range. Seeking to currentTime 268 | // allows the video to catch up to the audio position without losing any audio 269 | // (only suffering ~3 seconds of frozen video and a pause in audio playback). 270 | this.cancelTimer_(); 271 | this.tech_.setCurrentTime(currentTime); 272 | 273 | // video underflow may be useful for monitoring QoS 274 | this.tech_.trigger({type: 'usage', name: 'hls-video-underflow'}); 275 | return true; 276 | } 277 | 278 | // check for gap 279 | if (nextRange.length > 0) { 280 | let difference = nextRange.start(0) - currentTime; 281 | 282 | this.logger_( 283 | `Stopped at ${currentTime}, setting timer for ${difference}, seeking ` + 284 | `to ${nextRange.start(0)}`); 285 | 286 | this.timer_ = setTimeout(this.skipTheGap_.bind(this), 287 | difference * 1000, 288 | currentTime); 289 | return true; 290 | } 291 | 292 | // All checks failed. Returning false to indicate failure to correct waiting 293 | return false; 294 | } 295 | 296 | afterSeekableWindow_(seekable, currentTime) { 297 | if (!seekable.length) { 298 | // we can't make a solid case if there's no seekable, default to false 299 | return false; 300 | } 301 | 302 | if (currentTime > seekable.end(seekable.length - 1) + Ranges.SAFE_TIME_DELTA) { 303 | return true; 304 | } 305 | 306 | return false; 307 | } 308 | 309 | beforeSeekableWindow_(seekable, currentTime) { 310 | if (seekable.length && 311 | // can't fall before 0 and 0 seekable start identifies VOD stream 312 | seekable.start(0) > 0 && 313 | currentTime < seekable.start(0) - Ranges.SAFE_TIME_DELTA) { 314 | return true; 315 | } 316 | 317 | return false; 318 | } 319 | 320 | videoUnderflow_(nextRange, buffered, currentTime) { 321 | if (nextRange.length === 0) { 322 | // Even if there is no available next range, there is still a possibility we are 323 | // stuck in a gap due to video underflow. 324 | let gap = this.gapFromVideoUnderflow_(buffered, currentTime); 325 | 326 | if (gap) { 327 | this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + 328 | `Seeking to current time ${currentTime}`); 329 | 330 | return true; 331 | } 332 | } 333 | 334 | return false; 335 | } 336 | 337 | /** 338 | * Timer callback. If playback still has not proceeded, then we seek 339 | * to the start of the next buffered region. 340 | * 341 | * @private 342 | */ 343 | skipTheGap_(scheduledCurrentTime) { 344 | let buffered = this.tech_.buffered(); 345 | let currentTime = this.tech_.currentTime(); 346 | let nextRange = Ranges.findNextRange(buffered, currentTime); 347 | 348 | this.cancelTimer_(); 349 | 350 | if (nextRange.length === 0 || 351 | currentTime !== scheduledCurrentTime) { 352 | return; 353 | } 354 | 355 | this.logger_('skipTheGap_:', 356 | 'currentTime:', currentTime, 357 | 'scheduled currentTime:', scheduledCurrentTime, 358 | 'nextRange start:', nextRange.start(0)); 359 | 360 | // only seek if we still have not played 361 | this.tech_.setCurrentTime(nextRange.start(0) + Ranges.TIME_FUDGE_FACTOR); 362 | 363 | this.tech_.trigger({type: 'usage', name: 'hls-gap-skip'}); 364 | } 365 | 366 | gapFromVideoUnderflow_(buffered, currentTime) { 367 | // At least in Chrome, if there is a gap in the video buffer, the audio will continue 368 | // playing for ~3 seconds after the video gap starts. This is done to account for 369 | // video buffer underflow/underrun (note that this is not done when there is audio 370 | // buffer underflow/underrun -- in that case the video will stop as soon as it 371 | // encounters the gap, as audio stalls are more noticeable/jarring to a user than 372 | // video stalls). The player's time will reflect the playthrough of audio, so the 373 | // time will appear as if we are in a buffered region, even if we are stuck in a 374 | // "gap." 375 | // 376 | // Example: 377 | // video buffer: 0 => 10.1, 10.2 => 20 378 | // audio buffer: 0 => 20 379 | // overall buffer: 0 => 10.1, 10.2 => 20 380 | // current time: 13 381 | // 382 | // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 383 | // however, the audio continued playing until it reached ~3 seconds past the gap 384 | // (13 seconds), at which point it stops as well. Since current time is past the 385 | // gap, findNextRange will return no ranges. 386 | // 387 | // To check for this issue, we see if there is a gap that starts somewhere within 388 | // a 3 second range (3 seconds +/- 1 second) back from our current time. 389 | let gaps = Ranges.findGaps(buffered); 390 | 391 | for (let i = 0; i < gaps.length; i++) { 392 | let start = gaps.start(i); 393 | let end = gaps.end(i); 394 | 395 | // gap is starts no more than 4 seconds back 396 | if (currentTime - start < 4 && currentTime - start > 2) { 397 | return { 398 | start, 399 | end 400 | }; 401 | } 402 | } 403 | 404 | return null; 405 | } 406 | 407 | /** 408 | * A debugging logger noop that is set to console.log only if debugging 409 | * is enabled globally 410 | * 411 | * @private 412 | */ 413 | logger_() {} 414 | } 415 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/playlist-selectors.js: -------------------------------------------------------------------------------- 1 | import Config from './config'; 2 | import Playlist from './playlist'; 3 | import { parseCodecs } from './util/codecs.js'; 4 | 5 | // Utilities 6 | 7 | /** 8 | * Returns the CSS value for the specified property on an element 9 | * using `getComputedStyle`. Firefox has a long-standing issue where 10 | * getComputedStyle() may return null when running in an iframe with 11 | * `display: none`. 12 | * 13 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 14 | * @param {HTMLElement} el the htmlelement to work on 15 | * @param {string} the proprety to get the style for 16 | */ 17 | const safeGetComputedStyle = function(el, property) { 18 | let result; 19 | 20 | if (!el) { 21 | return ''; 22 | } 23 | 24 | result = window.getComputedStyle(el); 25 | if (!result) { 26 | return ''; 27 | } 28 | 29 | return result[property]; 30 | }; 31 | 32 | /** 33 | * Resuable stable sort function 34 | * 35 | * @param {Playlists} array 36 | * @param {Function} sortFn Different comparators 37 | * @function stableSort 38 | */ 39 | const stableSort = function(array, sortFn) { 40 | let newArray = array.slice(); 41 | 42 | array.sort(function(left, right) { 43 | let cmp = sortFn(left, right); 44 | 45 | if (cmp === 0) { 46 | return newArray.indexOf(left) - newArray.indexOf(right); 47 | } 48 | return cmp; 49 | }); 50 | }; 51 | 52 | /** 53 | * A comparator function to sort two playlist object by bandwidth. 54 | * 55 | * @param {Object} left a media playlist object 56 | * @param {Object} right a media playlist object 57 | * @return {Number} Greater than zero if the bandwidth attribute of 58 | * left is greater than the corresponding attribute of right. Less 59 | * than zero if the bandwidth of right is greater than left and 60 | * exactly zero if the two are equal. 61 | */ 62 | export const comparePlaylistBandwidth = function(left, right) { 63 | let leftBandwidth; 64 | let rightBandwidth; 65 | 66 | if (left.attributes.BANDWIDTH) { 67 | leftBandwidth = left.attributes.BANDWIDTH; 68 | } 69 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; 70 | if (right.attributes.BANDWIDTH) { 71 | rightBandwidth = right.attributes.BANDWIDTH; 72 | } 73 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; 74 | 75 | return leftBandwidth - rightBandwidth; 76 | }; 77 | 78 | /** 79 | * A comparator function to sort two playlist object by resolution (width). 80 | * @param {Object} left a media playlist object 81 | * @param {Object} right a media playlist object 82 | * @return {Number} Greater than zero if the resolution.width attribute of 83 | * left is greater than the corresponding attribute of right. Less 84 | * than zero if the resolution.width of right is greater than left and 85 | * exactly zero if the two are equal. 86 | */ 87 | export const comparePlaylistResolution = function(left, right) { 88 | let leftWidth; 89 | let rightWidth; 90 | 91 | if (left.attributes.RESOLUTION && 92 | left.attributes.RESOLUTION.width) { 93 | leftWidth = left.attributes.RESOLUTION.width; 94 | } 95 | 96 | leftWidth = leftWidth || window.Number.MAX_VALUE; 97 | 98 | if (right.attributes.RESOLUTION && 99 | right.attributes.RESOLUTION.width) { 100 | rightWidth = right.attributes.RESOLUTION.width; 101 | } 102 | 103 | rightWidth = rightWidth || window.Number.MAX_VALUE; 104 | 105 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 106 | // have the same media dimensions/ resolution 107 | if (leftWidth === rightWidth && 108 | left.attributes.BANDWIDTH && 109 | right.attributes.BANDWIDTH) { 110 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 111 | } 112 | return leftWidth - rightWidth; 113 | }; 114 | 115 | /** 116 | * Chooses the appropriate media playlist based on bandwidth and player size 117 | * 118 | * @param {Object} master 119 | * Object representation of the master manifest 120 | * @param {Number} playerBandwidth 121 | * Current calculated bandwidth of the player 122 | * @param {Number} playerWidth 123 | * Current width of the player element 124 | * @param {Number} playerHeight 125 | * Current height of the player element 126 | * @return {Playlist} the highest bitrate playlist less than the 127 | * currently detected bandwidth, accounting for some amount of 128 | * bandwidth variance 129 | */ 130 | export const simpleSelector = function(master, 131 | playerBandwidth, 132 | playerWidth, 133 | playerHeight) { 134 | // convert the playlists to an intermediary representation to make comparisons easier 135 | let sortedPlaylistReps = master.playlists.map((playlist) => { 136 | let width; 137 | let height; 138 | let bandwidth; 139 | 140 | width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 141 | height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 142 | bandwidth = playlist.attributes.BANDWIDTH; 143 | 144 | bandwidth = bandwidth || window.Number.MAX_VALUE; 145 | 146 | return { 147 | bandwidth, 148 | width, 149 | height, 150 | playlist 151 | }; 152 | }); 153 | 154 | stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); 155 | 156 | // filter out any playlists that have been excluded due to 157 | // incompatible configurations 158 | sortedPlaylistReps = sortedPlaylistReps.filter( 159 | (rep) => !Playlist.isIncompatible(rep.playlist) 160 | ); 161 | 162 | // filter out any playlists that have been disabled manually through the representations 163 | // api or blacklisted temporarily due to playback errors. 164 | let enabledPlaylistReps = sortedPlaylistReps.filter( 165 | (rep) => Playlist.isEnabled(rep.playlist) 166 | ); 167 | 168 | if (!enabledPlaylistReps.length) { 169 | // if there are no enabled playlists, then they have all been blacklisted or disabled 170 | // by the user through the representations api. In this case, ignore blacklisting and 171 | // fallback to what the user wants by using playlists the user has not disabled. 172 | enabledPlaylistReps = sortedPlaylistReps.filter( 173 | (rep) => !Playlist.isDisabled(rep.playlist) 174 | ); 175 | } 176 | 177 | // filter out any variant that has greater effective bitrate 178 | // than the current estimated bandwidth 179 | let bandwidthPlaylistReps = enabledPlaylistReps.filter( 180 | (rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth 181 | ); 182 | 183 | let highestRemainingBandwidthRep = 184 | bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; 185 | 186 | // get all of the renditions with the same (highest) bandwidth 187 | // and then taking the very first element 188 | let bandwidthBestRep = bandwidthPlaylistReps.filter( 189 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 190 | )[0]; 191 | 192 | // filter out playlists without resolution information 193 | let haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height); 194 | 195 | // sort variants by resolution 196 | stableSort(haveResolution, (left, right) => left.width - right.width); 197 | 198 | // if we have the exact resolution as the player use it 199 | let resolutionBestRepList = haveResolution.filter( 200 | (rep) => rep.width === playerWidth && rep.height === playerHeight 201 | ); 202 | 203 | highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; 204 | // ensure that we pick the highest bandwidth variant that have exact resolution 205 | let resolutionBestRep = resolutionBestRepList.filter( 206 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 207 | )[0]; 208 | 209 | let resolutionPlusOneList; 210 | let resolutionPlusOneSmallest; 211 | let resolutionPlusOneRep; 212 | 213 | // find the smallest variant that is larger than the player 214 | // if there is no match of exact resolution 215 | if (!resolutionBestRep) { 216 | resolutionPlusOneList = haveResolution.filter( 217 | (rep) => rep.width > playerWidth || rep.height > playerHeight 218 | ); 219 | 220 | // find all the variants have the same smallest resolution 221 | resolutionPlusOneSmallest = resolutionPlusOneList.filter( 222 | (rep) => rep.width === resolutionPlusOneList[0].width && 223 | rep.height === resolutionPlusOneList[0].height 224 | ); 225 | 226 | // ensure that we also pick the highest bandwidth variant that 227 | // is just-larger-than the video player 228 | highestRemainingBandwidthRep = 229 | resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 230 | resolutionPlusOneRep = resolutionPlusOneSmallest.filter( 231 | (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 232 | )[0]; 233 | } 234 | 235 | // fallback chain of variants 236 | let chosenRep = ( 237 | resolutionPlusOneRep || 238 | resolutionBestRep || 239 | bandwidthBestRep || 240 | enabledPlaylistReps[0] || 241 | sortedPlaylistReps[0] 242 | ); 243 | 244 | return chosenRep ? chosenRep.playlist : null; 245 | }; 246 | 247 | // Playlist Selectors 248 | 249 | /** 250 | * Chooses the appropriate media playlist based on the most recent 251 | * bandwidth estimate and the player size. 252 | * 253 | * Expects to be called within the context of an instance of HlsHandler 254 | * 255 | * @return {Playlist} the highest bitrate playlist less than the 256 | * currently detected bandwidth, accounting for some amount of 257 | * bandwidth variance 258 | */ 259 | export const lastBandwidthSelector = function() { 260 | return simpleSelector(this.playlists.master, 261 | this.systemBandwidth, 262 | parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), 263 | parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10)); 264 | }; 265 | 266 | /** 267 | * Chooses the appropriate media playlist based on an 268 | * exponential-weighted moving average of the bandwidth after 269 | * filtering for player size. 270 | * 271 | * Expects to be called within the context of an instance of HlsHandler 272 | * 273 | * @param {Number} decay - a number between 0 and 1. Higher values of 274 | * this parameter will cause previous bandwidth estimates to lose 275 | * significance more quickly. 276 | * @return {Function} a function which can be invoked to create a new 277 | * playlist selector function. 278 | * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 279 | */ 280 | export const movingAverageBandwidthSelector = function(decay) { 281 | let average = -1; 282 | 283 | if (decay < 0 || decay > 1) { 284 | throw new Error('Moving average bandwidth decay must be between 0 and 1.'); 285 | } 286 | 287 | return function() { 288 | if (average < 0) { 289 | average = this.systemBandwidth; 290 | } 291 | 292 | average = decay * this.systemBandwidth + (1 - decay) * average; 293 | return simpleSelector(this.playlists.master, 294 | average, 295 | parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), 296 | parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10)); 297 | }; 298 | }; 299 | 300 | /** 301 | * Chooses the appropriate media playlist based on the potential to rebuffer 302 | * 303 | * @param {Object} settings 304 | * Object of information required to use this selector 305 | * @param {Object} settings.master 306 | * Object representation of the master manifest 307 | * @param {Number} settings.currentTime 308 | * The current time of the player 309 | * @param {Number} settings.bandwidth 310 | * Current measured bandwidth 311 | * @param {Number} settings.duration 312 | * Duration of the media 313 | * @param {Number} settings.segmentDuration 314 | * Segment duration to be used in round trip time calculations 315 | * @param {Number} settings.timeUntilRebuffer 316 | * Time left in seconds until the player has to rebuffer 317 | * @param {Number} settings.currentTimeline 318 | * The current timeline segments are being loaded from 319 | * @param {SyncController} settings.syncController 320 | * SyncController for determining if we have a sync point for a given playlist 321 | * @return {Object|null} 322 | * {Object} return.playlist 323 | * The highest bandwidth playlist with the least amount of rebuffering 324 | * {Number} return.rebufferingImpact 325 | * The amount of time in seconds switching to this playlist will rebuffer. A 326 | * negative value means that switching will cause zero rebuffering. 327 | */ 328 | export const minRebufferMaxBandwidthSelector = function(settings) { 329 | const { 330 | master, 331 | currentTime, 332 | bandwidth, 333 | duration, 334 | segmentDuration, 335 | timeUntilRebuffer, 336 | currentTimeline, 337 | syncController 338 | } = settings; 339 | 340 | // filter out any playlists that have been excluded due to 341 | // incompatible configurations 342 | const compatiblePlaylists = master.playlists.filter( 343 | playlist => !Playlist.isIncompatible(playlist)); 344 | 345 | // filter out any playlists that have been disabled manually through the representations 346 | // api or blacklisted temporarily due to playback errors. 347 | let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 348 | 349 | if (!enabledPlaylists.length) { 350 | // if there are no enabled playlists, then they have all been blacklisted or disabled 351 | // by the user through the representations api. In this case, ignore blacklisting and 352 | // fallback to what the user wants by using playlists the user has not disabled. 353 | enabledPlaylists = compatiblePlaylists.filter( 354 | playlist => !Playlist.isDisabled(playlist)); 355 | } 356 | 357 | const bandwidthPlaylists = 358 | enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 359 | 360 | const rebufferingEstimates = bandwidthPlaylists.map((playlist) => { 361 | const syncPoint = syncController.getSyncPoint(playlist, 362 | duration, 363 | currentTimeline, 364 | currentTime); 365 | // If there is no sync point for this playlist, switching to it will require a 366 | // sync request first. This will double the request time 367 | const numRequests = syncPoint ? 1 : 2; 368 | const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, 369 | bandwidth, 370 | playlist); 371 | const rebufferingImpact = (requestTimeEstimate * numRequests) - timeUntilRebuffer; 372 | 373 | return { 374 | playlist, 375 | rebufferingImpact 376 | }; 377 | }); 378 | 379 | const noRebufferingPlaylists = rebufferingEstimates.filter( 380 | (estimate) => estimate.rebufferingImpact <= 0); 381 | 382 | // Sort by bandwidth DESC 383 | stableSort(noRebufferingPlaylists, 384 | (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)); 385 | 386 | if (noRebufferingPlaylists.length) { 387 | return noRebufferingPlaylists[0]; 388 | } 389 | 390 | stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact); 391 | 392 | return rebufferingEstimates[0] || null; 393 | }; 394 | 395 | /** 396 | * Chooses the appropriate media playlist, which in this case is the lowest bitrate 397 | * one with video. If no renditions with video exist, return the lowest audio rendition. 398 | * 399 | * Expects to be called within the context of an instance of HlsHandler 400 | * 401 | * @return {Object|null} 402 | * {Object} return.playlist 403 | * The lowest bitrate playlist that contains a video codec. If no such rendition 404 | * exists pick the lowest audio rendition. 405 | */ 406 | export const lowestBitrateCompatibleVariantSelector = function() { 407 | // filter out any playlists that have been excluded due to 408 | // incompatible configurations or playback errors 409 | const playlists = this.playlists.master.playlists.filter(Playlist.isEnabled); 410 | 411 | // Sort ascending by bitrate 412 | stableSort(playlists, 413 | (a, b) => comparePlaylistBandwidth(a, b)); 414 | 415 | // Parse and assume that playlists with no video codec have no video 416 | // (this is not necessarily true, although it is generally true). 417 | // 418 | // If an entire manifest has no valid videos everything will get filtered 419 | // out. 420 | const playlistsWithVideo = playlists.filter( 421 | playlist => parseCodecs(playlist.attributes.CODECS).videoCodec 422 | ); 423 | 424 | return playlistsWithVideo[0] || null; 425 | }; 426 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/ranges.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ranges 3 | * 4 | * Utilities for working with TimeRanges. 5 | * 6 | */ 7 | 8 | import videojs from 'video.js'; 9 | 10 | // Fudge factor to account for TimeRanges rounding 11 | const TIME_FUDGE_FACTOR = 1 / 30; 12 | // Comparisons between time values such as current time and the end of the buffered range 13 | // can be misleading because of precision differences or when the current media has poorly 14 | // aligned audio and video, which can cause values to be slightly off from what you would 15 | // expect. This value is what we consider to be safe to use in such comparisons to account 16 | // for these scenarios. 17 | const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 18 | 19 | /** 20 | * Clamps a value to within a range 21 | * @param {Number} num - the value to clamp 22 | * @param {Number} start - the start of the range to clamp within, inclusive 23 | * @param {Number} end - the end of the range to clamp within, inclusive 24 | * @return {Number} 25 | */ 26 | const clamp = function(num, [start, end]) { 27 | return Math.min(Math.max(start, num), end); 28 | }; 29 | const filterRanges = function(timeRanges, predicate) { 30 | let results = []; 31 | let i; 32 | 33 | if (timeRanges && timeRanges.length) { 34 | // Search for ranges that match the predicate 35 | for (i = 0; i < timeRanges.length; i++) { 36 | if (predicate(timeRanges.start(i), timeRanges.end(i))) { 37 | results.push([timeRanges.start(i), timeRanges.end(i)]); 38 | } 39 | } 40 | } 41 | 42 | return videojs.createTimeRanges(results); 43 | }; 44 | 45 | /** 46 | * Attempts to find the buffered TimeRange that contains the specified 47 | * time. 48 | * @param {TimeRanges} buffered - the TimeRanges object to query 49 | * @param {number} time - the time to filter on. 50 | * @returns {TimeRanges} a new TimeRanges object 51 | */ 52 | const findRange = function(buffered, time) { 53 | return filterRanges(buffered, function(start, end) { 54 | return start - TIME_FUDGE_FACTOR <= time && 55 | end + TIME_FUDGE_FACTOR >= time; 56 | }); 57 | }; 58 | 59 | /** 60 | * Returns the TimeRanges that begin later than the specified time. 61 | * @param {TimeRanges} timeRanges - the TimeRanges object to query 62 | * @param {number} time - the time to filter on. 63 | * @returns {TimeRanges} a new TimeRanges object. 64 | */ 65 | const findNextRange = function(timeRanges, time) { 66 | return filterRanges(timeRanges, function(start) { 67 | return start - TIME_FUDGE_FACTOR >= time; 68 | }); 69 | }; 70 | 71 | /** 72 | * Returns gaps within a list of TimeRanges 73 | * @param {TimeRanges} buffered - the TimeRanges object 74 | * @return {TimeRanges} a TimeRanges object of gaps 75 | */ 76 | const findGaps = function(buffered) { 77 | if (buffered.length < 2) { 78 | return videojs.createTimeRanges(); 79 | } 80 | 81 | let ranges = []; 82 | 83 | for (let i = 1; i < buffered.length; i++) { 84 | let start = buffered.end(i - 1); 85 | let end = buffered.start(i); 86 | 87 | ranges.push([start, end]); 88 | } 89 | 90 | return videojs.createTimeRanges(ranges); 91 | }; 92 | 93 | /** 94 | * Search for a likely end time for the segment that was just appened 95 | * based on the state of the `buffered` property before and after the 96 | * append. If we fin only one such uncommon end-point return it. 97 | * @param {TimeRanges} original - the buffered time ranges before the update 98 | * @param {TimeRanges} update - the buffered time ranges after the update 99 | * @returns {Number|null} the end time added between `original` and `update`, 100 | * or null if one cannot be unambiguously determined. 101 | */ 102 | const findSoleUncommonTimeRangesEnd = function(original, update) { 103 | let i; 104 | let start; 105 | let end; 106 | let result = []; 107 | let edges = []; 108 | 109 | // In order to qualify as a possible candidate, the end point must: 110 | // 1) Not have already existed in the `original` ranges 111 | // 2) Not result from the shrinking of a range that already existed 112 | // in the `original` ranges 113 | // 3) Not be contained inside of a range that existed in `original` 114 | const overlapsCurrentEnd = function(span) { 115 | return (span[0] <= end && span[1] >= end); 116 | }; 117 | 118 | if (original) { 119 | // Save all the edges in the `original` TimeRanges object 120 | for (i = 0; i < original.length; i++) { 121 | start = original.start(i); 122 | end = original.end(i); 123 | 124 | edges.push([start, end]); 125 | } 126 | } 127 | 128 | if (update) { 129 | // Save any end-points in `update` that are not in the `original` 130 | // TimeRanges object 131 | for (i = 0; i < update.length; i++) { 132 | start = update.start(i); 133 | end = update.end(i); 134 | 135 | if (edges.some(overlapsCurrentEnd)) { 136 | continue; 137 | } 138 | 139 | // at this point it must be a unique non-shrinking end edge 140 | result.push(end); 141 | } 142 | } 143 | 144 | // we err on the side of caution and return null if didn't find 145 | // exactly *one* differing end edge in the search above 146 | if (result.length !== 1) { 147 | return null; 148 | } 149 | 150 | return result[0]; 151 | }; 152 | 153 | /** 154 | * Calculate the intersection of two TimeRanges 155 | * @param {TimeRanges} bufferA 156 | * @param {TimeRanges} bufferB 157 | * @returns {TimeRanges} The interesection of `bufferA` with `bufferB` 158 | */ 159 | const bufferIntersection = function(bufferA, bufferB) { 160 | let start = null; 161 | let end = null; 162 | let arity = 0; 163 | let extents = []; 164 | let ranges = []; 165 | 166 | if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) { 167 | return videojs.createTimeRange(); 168 | } 169 | 170 | // Handle the case where we have both buffers and create an 171 | // intersection of the two 172 | let count = bufferA.length; 173 | 174 | // A) Gather up all start and end times 175 | while (count--) { 176 | extents.push({time: bufferA.start(count), type: 'start'}); 177 | extents.push({time: bufferA.end(count), type: 'end'}); 178 | } 179 | count = bufferB.length; 180 | while (count--) { 181 | extents.push({time: bufferB.start(count), type: 'start'}); 182 | extents.push({time: bufferB.end(count), type: 'end'}); 183 | } 184 | // B) Sort them by time 185 | extents.sort(function(a, b) { 186 | return a.time - b.time; 187 | }); 188 | 189 | // C) Go along one by one incrementing arity for start and decrementing 190 | // arity for ends 191 | for (count = 0; count < extents.length; count++) { 192 | if (extents[count].type === 'start') { 193 | arity++; 194 | 195 | // D) If arity is ever incremented to 2 we are entering an 196 | // overlapping range 197 | if (arity === 2) { 198 | start = extents[count].time; 199 | } 200 | } else if (extents[count].type === 'end') { 201 | arity--; 202 | 203 | // E) If arity is ever decremented to 1 we leaving an 204 | // overlapping range 205 | if (arity === 1) { 206 | end = extents[count].time; 207 | } 208 | } 209 | 210 | // F) Record overlapping ranges 211 | if (start !== null && end !== null) { 212 | ranges.push([start, end]); 213 | start = null; 214 | end = null; 215 | } 216 | } 217 | 218 | return videojs.createTimeRanges(ranges); 219 | }; 220 | 221 | /** 222 | * Calculates the percentage of `segmentRange` that overlaps the 223 | * `buffered` time ranges. 224 | * @param {TimeRanges} segmentRange - the time range that the segment 225 | * covers adjusted according to currentTime 226 | * @param {TimeRanges} referenceRange - the original time range that the 227 | * segment covers 228 | * @param {Number} currentTime - time in seconds where the current playback 229 | * is at 230 | * @param {TimeRanges} buffered - the currently buffered time ranges 231 | * @returns {Number} percent of the segment currently buffered 232 | */ 233 | const calculateBufferedPercent = function(adjustedRange, 234 | referenceRange, 235 | currentTime, 236 | buffered) { 237 | let referenceDuration = referenceRange.end(0) - referenceRange.start(0); 238 | let adjustedDuration = adjustedRange.end(0) - adjustedRange.start(0); 239 | let bufferMissingFromAdjusted = referenceDuration - adjustedDuration; 240 | let adjustedIntersection = bufferIntersection(adjustedRange, buffered); 241 | let referenceIntersection = bufferIntersection(referenceRange, buffered); 242 | let adjustedOverlap = 0; 243 | let referenceOverlap = 0; 244 | 245 | let count = adjustedIntersection.length; 246 | 247 | while (count--) { 248 | adjustedOverlap += adjustedIntersection.end(count) - 249 | adjustedIntersection.start(count); 250 | 251 | // If the current overlap segment starts at currentTime, then increase the 252 | // overlap duration so that it actually starts at the beginning of referenceRange 253 | // by including the difference between the two Range's durations 254 | // This is a work around for the way Flash has no buffer before currentTime 255 | if (adjustedIntersection.start(count) === currentTime) { 256 | adjustedOverlap += bufferMissingFromAdjusted; 257 | } 258 | } 259 | 260 | count = referenceIntersection.length; 261 | 262 | while (count--) { 263 | referenceOverlap += referenceIntersection.end(count) - 264 | referenceIntersection.start(count); 265 | } 266 | 267 | // Use whichever value is larger for the percentage-buffered since that value 268 | // is likely more accurate because the only way 269 | return Math.max(adjustedOverlap, referenceOverlap) / referenceDuration * 100; 270 | }; 271 | 272 | /** 273 | * Return the amount of a range specified by the startOfSegment and segmentDuration 274 | * overlaps the current buffered content. 275 | * 276 | * @param {Number} startOfSegment - the time where the segment begins 277 | * @param {Number} segmentDuration - the duration of the segment in seconds 278 | * @param {Number} currentTime - time in seconds where the current playback 279 | * is at 280 | * @param {TimeRanges} buffered - the state of the buffer 281 | * @returns {Number} percentage of the segment's time range that is 282 | * already in `buffered` 283 | */ 284 | const getSegmentBufferedPercent = function(startOfSegment, 285 | segmentDuration, 286 | currentTime, 287 | buffered) { 288 | let endOfSegment = startOfSegment + segmentDuration; 289 | 290 | // The entire time range of the segment 291 | let originalSegmentRange = videojs.createTimeRanges([[ 292 | startOfSegment, 293 | endOfSegment 294 | ]]); 295 | 296 | // The adjusted segment time range that is setup such that it starts 297 | // no earlier than currentTime 298 | // Flash has no notion of a back-buffer so adjustedSegmentRange adjusts 299 | // for that and the function will still return 100% if a only half of a 300 | // segment is actually in the buffer as long as the currentTime is also 301 | // half-way through the segment 302 | let adjustedSegmentRange = videojs.createTimeRanges([[ 303 | clamp(startOfSegment, [currentTime, endOfSegment]), 304 | endOfSegment 305 | ]]); 306 | 307 | // This condition happens when the currentTime is beyond the segment's 308 | // end time 309 | if (adjustedSegmentRange.start(0) === adjustedSegmentRange.end(0)) { 310 | return 0; 311 | } 312 | 313 | let percent = calculateBufferedPercent(adjustedSegmentRange, 314 | originalSegmentRange, 315 | currentTime, 316 | buffered); 317 | 318 | // If the segment is reported as having a zero duration, return 0% 319 | // since it is likely that we will need to fetch the segment 320 | if (isNaN(percent) || percent === Infinity || percent === -Infinity) { 321 | return 0; 322 | } 323 | 324 | return percent; 325 | }; 326 | 327 | /** 328 | * Gets a human readable string for a TimeRange 329 | * 330 | * @param {TimeRange} range 331 | * @returns {String} a human readable string 332 | */ 333 | const printableRange = (range) => { 334 | let strArr = []; 335 | 336 | if (!range || !range.length) { 337 | return ''; 338 | } 339 | 340 | for (let i = 0; i < range.length; i++) { 341 | strArr.push(range.start(i) + ' => ' + range.end(i)); 342 | } 343 | 344 | return strArr.join(', '); 345 | }; 346 | 347 | /** 348 | * Calculates the amount of time left in seconds until the player hits the end of the 349 | * buffer and causes a rebuffer 350 | * 351 | * @param {TimeRange} buffered 352 | * The state of the buffer 353 | * @param {Numnber} currentTime 354 | * The current time of the player 355 | * @param {Number} playbackRate 356 | * The current playback rate of the player. Defaults to 1. 357 | * @return {Number} 358 | * Time until the player has to start rebuffering in seconds. 359 | * @function timeUntilRebuffer 360 | */ 361 | const timeUntilRebuffer = function(buffered, currentTime, playbackRate = 1) { 362 | const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 363 | 364 | return (bufferedEnd - currentTime) / playbackRate; 365 | }; 366 | 367 | export default { 368 | findRange, 369 | findNextRange, 370 | findGaps, 371 | findSoleUncommonTimeRangesEnd, 372 | getSegmentBufferedPercent, 373 | TIME_FUDGE_FACTOR, 374 | SAFE_TIME_DELTA, 375 | printableRange, 376 | timeUntilRebuffer 377 | }; 378 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/reload-source-on-error.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | 3 | const defaultOptions = { 4 | errorInterval: 30, 5 | getSource(next) { 6 | let tech = this.tech({ IWillNotUseThisInPlugins: true }); 7 | let sourceObj = tech.currentSource_; 8 | 9 | return next(sourceObj); 10 | } 11 | }; 12 | 13 | /** 14 | * Main entry point for the plugin 15 | * 16 | * @param {Player} player a reference to a videojs Player instance 17 | * @param {Object} [options] an object with plugin options 18 | * @private 19 | */ 20 | const initPlugin = function(player, options) { 21 | let lastCalled = 0; 22 | let seekTo = 0; 23 | let localOptions = videojs.mergeOptions(defaultOptions, options); 24 | 25 | player.ready(() => { 26 | player.trigger({type: 'usage', name: 'hls-error-reload-initialized'}); 27 | }); 28 | 29 | /** 30 | * Player modifications to perform that must wait until `loadedmetadata` 31 | * has been triggered 32 | * 33 | * @private 34 | */ 35 | const loadedMetadataHandler = function() { 36 | if (seekTo) { 37 | player.currentTime(seekTo); 38 | } 39 | }; 40 | 41 | /** 42 | * Set the source on the player element, play, and seek if necessary 43 | * 44 | * @param {Object} sourceObj An object specifying the source url and mime-type to play 45 | * @private 46 | */ 47 | const setSource = function(sourceObj) { 48 | if (sourceObj === null || sourceObj === undefined) { 49 | return; 50 | } 51 | seekTo = (player.duration() !== Infinity && player.currentTime()) || 0; 52 | 53 | player.one('loadedmetadata', loadedMetadataHandler); 54 | 55 | player.src(sourceObj); 56 | player.trigger({type: 'usage', name: 'hls-error-reload'}); 57 | player.play(); 58 | }; 59 | 60 | /** 61 | * Attempt to get a source from either the built-in getSource function 62 | * or a custom function provided via the options 63 | * 64 | * @private 65 | */ 66 | const errorHandler = function() { 67 | // Do not attempt to reload the source if a source-reload occurred before 68 | // 'errorInterval' time has elapsed since the last source-reload 69 | if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 70 | player.trigger({type: 'usage', name: 'hls-error-reload-canceled'}); 71 | return; 72 | } 73 | 74 | if (!localOptions.getSource || 75 | typeof localOptions.getSource !== 'function') { 76 | videojs.log.error( 77 | 'ERROR: reloadSourceOnError - The option getSource must be a function!'); 78 | return; 79 | } 80 | lastCalled = Date.now(); 81 | 82 | return localOptions.getSource.call(player, setSource); 83 | }; 84 | 85 | /** 86 | * Unbind any event handlers that were bound by the plugin 87 | * 88 | * @private 89 | */ 90 | const cleanupEvents = function() { 91 | player.off('loadedmetadata', loadedMetadataHandler); 92 | player.off('error', errorHandler); 93 | player.off('dispose', cleanupEvents); 94 | }; 95 | 96 | /** 97 | * Cleanup before re-initializing the plugin 98 | * 99 | * @param {Object} [newOptions] an object with plugin options 100 | * @private 101 | */ 102 | const reinitPlugin = function(newOptions) { 103 | cleanupEvents(); 104 | initPlugin(player, newOptions); 105 | }; 106 | 107 | player.on('error', errorHandler); 108 | player.on('dispose', cleanupEvents); 109 | 110 | // Overwrite the plugin function so that we can correctly cleanup before 111 | // initializing the plugin 112 | player.reloadSourceOnError = reinitPlugin; 113 | }; 114 | 115 | /** 116 | * Reload the source when an error is detected as long as there 117 | * wasn't an error previously within the last 30 seconds 118 | * 119 | * @param {Object} [options] an object with plugin options 120 | */ 121 | const reloadSourceOnError = function(options) { 122 | initPlugin(this, options); 123 | }; 124 | 125 | export default reloadSourceOnError; 126 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/rendition-mixin.js: -------------------------------------------------------------------------------- 1 | import { isIncompatible, isEnabled } from './playlist.js'; 2 | 3 | /** 4 | * Returns a function that acts as the Enable/disable playlist function. 5 | * 6 | * @param {PlaylistLoader} loader - The master playlist loader 7 | * @param {String} playlistUri - uri of the playlist 8 | * @param {Function} changePlaylistFn - A function to be called after a 9 | * playlist's enabled-state has been changed. Will NOT be called if a 10 | * playlist's enabled-state is unchanged 11 | * @param {Boolean=} enable - Value to set the playlist enabled-state to 12 | * or if undefined returns the current enabled-state for the playlist 13 | * @return {Function} Function for setting/getting enabled 14 | */ 15 | const enableFunction = (loader, playlistUri, changePlaylistFn) => (enable) => { 16 | const playlist = loader.master.playlists[playlistUri]; 17 | const incompatible = isIncompatible(playlist); 18 | const currentlyEnabled = isEnabled(playlist); 19 | 20 | if (typeof enable === 'undefined') { 21 | return currentlyEnabled; 22 | } 23 | 24 | if (enable) { 25 | delete playlist.disabled; 26 | } else { 27 | playlist.disabled = true; 28 | } 29 | 30 | if (enable !== currentlyEnabled && !incompatible) { 31 | // Ensure the outside world knows about our changes 32 | changePlaylistFn(); 33 | if (enable) { 34 | loader.trigger('renditionenabled'); 35 | } else { 36 | loader.trigger('renditiondisabled'); 37 | } 38 | } 39 | return enable; 40 | }; 41 | 42 | /** 43 | * The representation object encapsulates the publicly visible information 44 | * in a media playlist along with a setter/getter-type function (enabled) 45 | * for changing the enabled-state of a particular playlist entry 46 | * 47 | * @class Representation 48 | */ 49 | class Representation { 50 | constructor(hlsHandler, playlist, id) { 51 | // Get a reference to a bound version of fastQualityChange_ 52 | let fastChangeFunction = hlsHandler 53 | .masterPlaylistController_ 54 | .fastQualityChange_ 55 | .bind(hlsHandler.masterPlaylistController_); 56 | 57 | // some playlist attributes are optional 58 | if (playlist.attributes.RESOLUTION) { 59 | const resolution = playlist.attributes.RESOLUTION; 60 | 61 | this.width = resolution.width; 62 | this.height = resolution.height; 63 | } 64 | 65 | this.bandwidth = playlist.attributes.BANDWIDTH; 66 | 67 | // The id is simply the ordinality of the media playlist 68 | // within the master playlist 69 | this.id = id; 70 | 71 | // Partially-apply the enableFunction to create a playlist- 72 | // specific variant 73 | this.enabled = enableFunction(hlsHandler.playlists, 74 | playlist.uri, 75 | fastChangeFunction); 76 | } 77 | } 78 | 79 | /** 80 | * A mixin function that adds the `representations` api to an instance 81 | * of the HlsHandler class 82 | * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the 83 | * representation API into 84 | */ 85 | let renditionSelectionMixin = function(hlsHandler) { 86 | let playlists = hlsHandler.playlists; 87 | 88 | // Add a single API-specific function to the HlsHandler instance 89 | hlsHandler.representations = () => { 90 | return playlists 91 | .master 92 | .playlists 93 | .filter((media) => !isIncompatible(media)) 94 | .map((e, i) => new Representation(hlsHandler, e, e.uri)); 95 | }; 96 | }; 97 | 98 | export default renditionSelectionMixin; 99 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/resolve-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file resolve-url.js 3 | */ 4 | 5 | import URLToolkit from 'url-toolkit'; 6 | import window from 'global/window'; 7 | 8 | const resolveUrl = function(baseURL, relativeURL) { 9 | // return early if we don't need to resolve 10 | if ((/^[a-z]+:/i).test(relativeURL)) { 11 | return relativeURL; 12 | } 13 | 14 | // if the base URL is relative then combine with the current location 15 | if (!(/\/\//i).test(baseURL)) { 16 | baseURL = URLToolkit.buildAbsoluteURL(window.location.href, baseURL); 17 | } 18 | 19 | return URLToolkit.buildAbsoluteURL(baseURL, relativeURL); 20 | }; 21 | 22 | export default resolveUrl; 23 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/source-updater.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file source-updater.js 3 | */ 4 | import videojs from 'video.js'; 5 | 6 | const noop = function() {}; 7 | 8 | /** 9 | * A queue of callbacks to be serialized and applied when a 10 | * MediaSource and its associated SourceBuffers are not in the 11 | * updating state. It is used by the segment loader to update the 12 | * underlying SourceBuffers when new data is loaded, for instance. 13 | * 14 | * @class SourceUpdater 15 | * @param {MediaSource} mediaSource the MediaSource to create the 16 | * SourceBuffer from 17 | * @param {String} mimeType the desired MIME type of the underlying 18 | * SourceBuffer 19 | */ 20 | export default class SourceUpdater { 21 | constructor(mediaSource, mimeType) { 22 | let createSourceBuffer = () => { 23 | this.sourceBuffer_ = mediaSource.addSourceBuffer(mimeType); 24 | 25 | // run completion handlers and process callbacks as updateend 26 | // events fire 27 | this.onUpdateendCallback_ = () => { 28 | let pendingCallback = this.pendingCallback_; 29 | 30 | this.pendingCallback_ = null; 31 | 32 | if (pendingCallback) { 33 | pendingCallback(); 34 | } 35 | 36 | this.runCallback_(); 37 | }; 38 | 39 | this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_); 40 | 41 | this.runCallback_(); 42 | }; 43 | 44 | this.callbacks_ = []; 45 | this.pendingCallback_ = null; 46 | this.timestampOffset_ = 0; 47 | this.mediaSource = mediaSource; 48 | this.processedAppend_ = false; 49 | 50 | if (mediaSource.readyState === 'closed') { 51 | mediaSource.addEventListener('sourceopen', createSourceBuffer); 52 | } else { 53 | createSourceBuffer(); 54 | } 55 | } 56 | 57 | /** 58 | * Aborts the current segment and resets the segment parser. 59 | * 60 | * @param {Function} done function to call when done 61 | * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void 62 | */ 63 | abort(done) { 64 | if (this.processedAppend_) { 65 | this.queueCallback_(() => { 66 | this.sourceBuffer_.abort(); 67 | }, done); 68 | } 69 | } 70 | 71 | /** 72 | * Queue an update to append an ArrayBuffer. 73 | * 74 | * @param {ArrayBuffer} bytes 75 | * @param {Function} done the function to call when done 76 | * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 77 | */ 78 | appendBuffer(bytes, done) { 79 | this.processedAppend_ = true; 80 | this.queueCallback_(() => { 81 | this.sourceBuffer_.appendBuffer(bytes); 82 | }, done); 83 | } 84 | 85 | /** 86 | * Indicates what TimeRanges are buffered in the managed SourceBuffer. 87 | * 88 | * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered 89 | */ 90 | buffered() { 91 | if (!this.sourceBuffer_) { 92 | return videojs.createTimeRanges(); 93 | } 94 | return this.sourceBuffer_.buffered; 95 | } 96 | 97 | /** 98 | * Queue an update to remove a time range from the buffer. 99 | * 100 | * @param {Number} start where to start the removal 101 | * @param {Number} end where to end the removal 102 | * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 103 | */ 104 | remove(start, end) { 105 | if (this.processedAppend_) { 106 | this.queueCallback_(() => { 107 | this.sourceBuffer_.remove(start, end); 108 | }, noop); 109 | } 110 | } 111 | 112 | /** 113 | * Whether the underlying sourceBuffer is updating or not 114 | * 115 | * @return {Boolean} the updating status of the SourceBuffer 116 | */ 117 | updating() { 118 | return !this.sourceBuffer_ || this.sourceBuffer_.updating || this.pendingCallback_; 119 | } 120 | 121 | /** 122 | * Set/get the timestampoffset on the SourceBuffer 123 | * 124 | * @return {Number} the timestamp offset 125 | */ 126 | timestampOffset(offset) { 127 | if (typeof offset !== 'undefined') { 128 | this.queueCallback_(() => { 129 | this.sourceBuffer_.timestampOffset = offset; 130 | }); 131 | this.timestampOffset_ = offset; 132 | } 133 | return this.timestampOffset_; 134 | } 135 | 136 | /** 137 | * Queue a callback to run 138 | */ 139 | queueCallback_(callback, done) { 140 | this.callbacks_.push([callback.bind(this), done]); 141 | this.runCallback_(); 142 | } 143 | 144 | /** 145 | * Run a queued callback 146 | */ 147 | runCallback_() { 148 | let callbacks; 149 | 150 | if (!this.updating() && 151 | this.callbacks_.length) { 152 | callbacks = this.callbacks_.shift(); 153 | this.pendingCallback_ = callbacks[1]; 154 | callbacks[0](); 155 | } 156 | } 157 | 158 | /** 159 | * dispose of the source updater and the underlying sourceBuffer 160 | */ 161 | dispose() { 162 | this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_); 163 | if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') { 164 | this.sourceBuffer_.abort(); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/util/codecs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 4 | * codec strings, or translating codec strings into objects that can be examined. 5 | */ 6 | 7 | /** 8 | * Parses a codec string to retrieve the number of codecs specified, 9 | * the video codec and object type indicator, and the audio profile. 10 | */ 11 | 12 | export const parseCodecs = function(codecs = '') { 13 | let result = { 14 | codecCount: 0 15 | }; 16 | let parsed; 17 | 18 | result.codecCount = codecs.split(',').length; 19 | result.codecCount = result.codecCount || 2; 20 | 21 | // parse the video codec 22 | parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs); 23 | if (parsed) { 24 | result.videoCodec = parsed[2]; 25 | result.videoObjectTypeIndicator = parsed[3]; 26 | } 27 | 28 | // parse the last field of the audio codec 29 | result.audioProfile = 30 | (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs); 31 | result.audioProfile = result.audioProfile && result.audioProfile[2]; 32 | 33 | return result; 34 | }; 35 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/vtt-segment-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vtt-segment-loader.js 3 | */ 4 | import SegmentLoader from './segment-loader'; 5 | import videojs from 'video.js'; 6 | import window from 'global/window'; 7 | import removeCuesFromTrack from 8 | 'videojs-contrib-media-sources/es5/remove-cues-from-track.js'; 9 | import { initSegmentId } from './bin-utils'; 10 | 11 | const VTT_LINE_TERMINATORS = 12 | new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0))); 13 | 14 | const uintToString = function(uintArray) { 15 | return String.fromCharCode.apply(null, uintArray); 16 | }; 17 | 18 | /** 19 | * An object that manages segment loading and appending. 20 | * 21 | * @class VTTSegmentLoader 22 | * @param {Object} options required and optional options 23 | * @extends videojs.EventTarget 24 | */ 25 | export default class VTTSegmentLoader extends SegmentLoader { 26 | constructor(settings, options = {}) { 27 | super(settings, options); 28 | 29 | // SegmentLoader requires a MediaSource be specified or it will throw an error; 30 | // however, VTTSegmentLoader has no need of a media source, so delete the reference 31 | this.mediaSource_ = null; 32 | 33 | this.subtitlesTrack_ = null; 34 | } 35 | 36 | /** 37 | * Indicates which time ranges are buffered 38 | * 39 | * @return {TimeRange} 40 | * TimeRange object representing the current buffered ranges 41 | */ 42 | buffered_() { 43 | if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) { 44 | return videojs.createTimeRanges(); 45 | } 46 | 47 | const cues = this.subtitlesTrack_.cues; 48 | let start = cues[0].startTime; 49 | let end = cues[cues.length - 1].startTime; 50 | 51 | return videojs.createTimeRanges([[start, end]]); 52 | } 53 | 54 | /** 55 | * Gets and sets init segment for the provided map 56 | * 57 | * @param {Object} map 58 | * The map object representing the init segment to get or set 59 | * @param {Boolean=} set 60 | * If true, the init segment for the provided map should be saved 61 | * @return {Object} 62 | * map object for desired init segment 63 | */ 64 | initSegment(map, set = false) { 65 | if (!map) { 66 | return null; 67 | } 68 | 69 | const id = initSegmentId(map); 70 | let storedMap = this.initSegments_[id]; 71 | 72 | if (set && !storedMap && map.bytes) { 73 | // append WebVTT line terminators to the media initialization segment if it exists 74 | // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 75 | // requires two or more WebVTT line terminators between the WebVTT header and the 76 | // rest of the file 77 | const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 78 | const combinedSegment = new Uint8Array(combinedByteLength); 79 | 80 | combinedSegment.set(map.bytes); 81 | combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 82 | 83 | this.initSegments_[id] = storedMap = { 84 | resolvedUri: map.resolvedUri, 85 | byterange: map.byterange, 86 | bytes: combinedSegment 87 | }; 88 | } 89 | 90 | return storedMap || map; 91 | } 92 | 93 | /** 94 | * Returns true if all configuration required for loading is present, otherwise false. 95 | * 96 | * @return {Boolean} True if the all configuration is ready for loading 97 | * @private 98 | */ 99 | couldBeginLoading_() { 100 | return this.playlist_ && 101 | this.subtitlesTrack_ && 102 | !this.paused(); 103 | } 104 | 105 | /** 106 | * Once all the starting parameters have been specified, begin 107 | * operation. This method should only be invoked from the INIT 108 | * state. 109 | * 110 | * @private 111 | */ 112 | init_() { 113 | this.state = 'READY'; 114 | this.resetEverything(); 115 | return this.monitorBuffer_(); 116 | } 117 | 118 | /** 119 | * Set a subtitle track on the segment loader to add subtitles to 120 | * 121 | * @param {TextTrack=} track 122 | * The text track to add loaded subtitles to 123 | * @return {TextTrack} 124 | * Returns the subtitles track 125 | */ 126 | track(track) { 127 | if (typeof track === 'undefined') { 128 | return this.subtitlesTrack_; 129 | } 130 | 131 | this.subtitlesTrack_ = track; 132 | 133 | // if we were unpaused but waiting for a sourceUpdater, start 134 | // buffering now 135 | if (this.state === 'INIT' && this.couldBeginLoading_()) { 136 | this.init_(); 137 | } 138 | 139 | return this.subtitlesTrack_; 140 | } 141 | 142 | /** 143 | * Remove any data in the source buffer between start and end times 144 | * @param {Number} start - the start time of the region to remove from the buffer 145 | * @param {Number} end - the end time of the region to remove from the buffer 146 | */ 147 | remove(start, end) { 148 | removeCuesFromTrack(start, end, this.subtitlesTrack_); 149 | } 150 | 151 | /** 152 | * fill the buffer with segements unless the sourceBuffers are 153 | * currently updating 154 | * 155 | * Note: this function should only ever be called by monitorBuffer_ 156 | * and never directly 157 | * 158 | * @private 159 | */ 160 | fillBuffer_() { 161 | if (!this.syncPoint_) { 162 | this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, 163 | this.duration_(), 164 | this.currentTimeline_, 165 | this.currentTime_()); 166 | } 167 | 168 | // see if we need to begin loading immediately 169 | let segmentInfo = this.checkBuffer_(this.buffered_(), 170 | this.playlist_, 171 | this.mediaIndex, 172 | this.hasPlayed_(), 173 | this.currentTime_(), 174 | this.syncPoint_); 175 | 176 | segmentInfo = this.skipEmptySegments_(segmentInfo); 177 | 178 | if (!segmentInfo) { 179 | return; 180 | } 181 | 182 | if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 183 | // We don't have the timestamp offset that we need to sync subtitles. 184 | // Rerun on a timestamp offset or user interaction. 185 | let checkTimestampOffset = () => { 186 | this.state = 'READY'; 187 | if (!this.paused()) { 188 | // if not paused, queue a buffer check as soon as possible 189 | this.monitorBuffer_(); 190 | } 191 | }; 192 | 193 | this.syncController_.one('timestampoffset', checkTimestampOffset); 194 | this.state = 'WAITING_ON_TIMELINE'; 195 | return; 196 | } 197 | 198 | this.loadSegment_(segmentInfo); 199 | } 200 | 201 | /** 202 | * Prevents the segment loader from requesting segments we know contain no subtitles 203 | * by walking forward until we find the next segment that we don't know whether it is 204 | * empty or not. 205 | * 206 | * @param {Object} segmentInfo 207 | * a segment info object that describes the current segment 208 | * @return {Object} 209 | * a segment info object that describes the current segment 210 | */ 211 | skipEmptySegments_(segmentInfo) { 212 | while (segmentInfo && segmentInfo.segment.empty) { 213 | segmentInfo = this.generateSegmentInfo_( 214 | segmentInfo.playlist, 215 | segmentInfo.mediaIndex + 1, 216 | segmentInfo.startOfSegment + segmentInfo.duration, 217 | segmentInfo.isSyncRequest); 218 | } 219 | return segmentInfo; 220 | } 221 | 222 | /** 223 | * append a decrypted segement to the SourceBuffer through a SourceUpdater 224 | * 225 | * @private 226 | */ 227 | handleSegment_() { 228 | if (!this.pendingSegment_ || !this.subtitlesTrack_) { 229 | this.state = 'READY'; 230 | return; 231 | } 232 | 233 | this.state = 'APPENDING'; 234 | 235 | let segmentInfo = this.pendingSegment_; 236 | let segment = segmentInfo.segment; 237 | 238 | // Make sure that vttjs has loaded, otherwise, wait till it finished loading 239 | if (typeof window.WebVTT !== 'function' && 240 | this.subtitlesTrack_ && 241 | this.subtitlesTrack_.tech_) { 242 | 243 | const loadHandler = () => { 244 | this.handleSegment_(); 245 | }; 246 | 247 | this.state = 'WAITING_ON_VTTJS'; 248 | this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler); 249 | this.subtitlesTrack_.tech_.one('vttjserror', () => { 250 | this.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler); 251 | this.error({ 252 | message: 'Error loading vtt.js' 253 | }); 254 | this.state = 'READY'; 255 | this.pause(); 256 | this.trigger('error'); 257 | }); 258 | 259 | return; 260 | } 261 | 262 | segment.requested = true; 263 | 264 | try { 265 | this.parseVTTCues_(segmentInfo); 266 | } catch (e) { 267 | this.error({ 268 | message: e.message 269 | }); 270 | this.state = 'READY'; 271 | this.pause(); 272 | return this.trigger('error'); 273 | } 274 | 275 | this.updateTimeMapping_(segmentInfo, 276 | this.syncController_.timelines[segmentInfo.timeline], 277 | this.playlist_); 278 | 279 | if (segmentInfo.isSyncRequest) { 280 | this.trigger('syncinfoupdate'); 281 | this.pendingSegment_ = null; 282 | this.state = 'READY'; 283 | return; 284 | } 285 | 286 | segmentInfo.byteLength = segmentInfo.bytes.byteLength; 287 | 288 | this.mediaSecondsLoaded += segment.duration; 289 | 290 | if (segmentInfo.cues.length) { 291 | // remove any overlapping cues to prevent doubling 292 | this.remove(segmentInfo.cues[0].endTime, 293 | segmentInfo.cues[segmentInfo.cues.length - 1].endTime); 294 | } 295 | 296 | segmentInfo.cues.forEach((cue) => { 297 | this.subtitlesTrack_.addCue(cue); 298 | }); 299 | 300 | this.handleUpdateEnd_(); 301 | } 302 | 303 | /** 304 | * Uses the WebVTT parser to parse the segment response 305 | * 306 | * @param {Object} segmentInfo 307 | * a segment info object that describes the current segment 308 | * @private 309 | */ 310 | parseVTTCues_(segmentInfo) { 311 | let decoder; 312 | let decodeBytesToString = false; 313 | 314 | if (typeof window.TextDecoder === 'function') { 315 | decoder = new window.TextDecoder('utf8'); 316 | } else { 317 | decoder = window.WebVTT.StringDecoder(); 318 | decodeBytesToString = true; 319 | } 320 | 321 | const parser = new window.WebVTT.Parser(window, 322 | window.vttjs, 323 | decoder); 324 | 325 | segmentInfo.cues = []; 326 | segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 }; 327 | 328 | parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 329 | parser.ontimestampmap = (map) => segmentInfo.timestampmap = map; 330 | parser.onparsingerror = (error) => { 331 | videojs.log.warn('Error encountered when parsing cues: ' + error.message); 332 | }; 333 | 334 | if (segmentInfo.segment.map) { 335 | let mapData = segmentInfo.segment.map.bytes; 336 | 337 | if (decodeBytesToString) { 338 | mapData = uintToString(mapData); 339 | } 340 | 341 | parser.parse(mapData); 342 | } 343 | 344 | let segmentData = segmentInfo.bytes; 345 | 346 | if (decodeBytesToString) { 347 | segmentData = uintToString(segmentData); 348 | } 349 | 350 | parser.parse(segmentData); 351 | parser.flush(); 352 | } 353 | 354 | /** 355 | * Updates the start and end times of any cues parsed by the WebVTT parser using 356 | * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 357 | * from the SyncController 358 | * 359 | * @param {Object} segmentInfo 360 | * a segment info object that describes the current segment 361 | * @param {Object} mappingObj 362 | * object containing a mapping from TS to media time 363 | * @param {Object} playlist 364 | * the playlist object containing the segment 365 | * @private 366 | */ 367 | updateTimeMapping_(segmentInfo, mappingObj, playlist) { 368 | const segment = segmentInfo.segment; 369 | 370 | if (!mappingObj) { 371 | // If the sync controller does not have a mapping of TS to Media Time for the 372 | // timeline, then we don't have enough information to update the cue 373 | // start/end times 374 | return; 375 | } 376 | 377 | if (!segmentInfo.cues.length) { 378 | // If there are no cues, we also do not have enough information to figure out 379 | // segment timing. Mark that the segment contains no cues so we don't re-request 380 | // an empty segment. 381 | segment.empty = true; 382 | return; 383 | } 384 | 385 | const timestampmap = segmentInfo.timestampmap; 386 | const diff = (timestampmap.MPEGTS / 90000) - timestampmap.LOCAL + mappingObj.mapping; 387 | 388 | segmentInfo.cues.forEach((cue) => { 389 | // First convert cue time to TS time using the timestamp-map provided within the vtt 390 | cue.startTime += diff; 391 | cue.endTime += diff; 392 | }); 393 | 394 | if (!playlist.syncInfo) { 395 | const firstStart = segmentInfo.cues[0].startTime; 396 | const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 397 | 398 | playlist.syncInfo = { 399 | mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 400 | time: Math.min(firstStart, lastStart - segment.duration) 401 | }; 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/sbot/video-hls.js.bak/xhr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file xhr.js 3 | */ 4 | 5 | /** 6 | * A wrapper for videojs.xhr that tracks bandwidth. 7 | * 8 | * @param {Object} options options for the XHR 9 | * @param {Function} callback the callback to call when done 10 | * @return {Request} the xhr request that is going to be made 11 | */ 12 | import { 13 | xhr as videojsXHR, 14 | mergeOptions, 15 | default as videojs 16 | } from 'video.js'; 17 | 18 | const xhrFactory = function() { 19 | const xhr = function XhrFunction(options, callback) { 20 | // Add a default timeout for all hls requests 21 | options = mergeOptions({ 22 | timeout: 45e3 23 | }, options); 24 | 25 | // Allow an optional user-specified function to modify the option 26 | // object before we construct the xhr request 27 | let beforeRequest = XhrFunction.beforeRequest || videojs.Hls.xhr.beforeRequest; 28 | 29 | if (beforeRequest && typeof beforeRequest === 'function') { 30 | let newOptions = beforeRequest(options); 31 | 32 | if (newOptions) { 33 | options = newOptions; 34 | } 35 | } 36 | 37 | let request = videojsXHR(options, function(error, response) { 38 | let reqResponse = request.response; 39 | 40 | if (!error && reqResponse) { 41 | request.responseTime = Date.now(); 42 | request.roundTripTime = request.responseTime - request.requestTime; 43 | request.bytesReceived = reqResponse.byteLength || reqResponse.length; 44 | if (!request.bandwidth) { 45 | request.bandwidth = 46 | Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); 47 | } 48 | } 49 | 50 | // videojs.xhr now uses a specific code on the error 51 | // object to signal that a request has timed out instead 52 | // of setting a boolean on the request object 53 | if (error && error.code === 'ETIMEDOUT') { 54 | request.timedout = true; 55 | } 56 | 57 | // videojs.xhr no longer considers status codes outside of 200 and 0 58 | // (for file uris) to be errors, but the old XHR did, so emulate that 59 | // behavior. Status 206 may be used in response to byterange requests. 60 | if (!error && 61 | !request.aborted && 62 | response.statusCode !== 200 && 63 | response.statusCode !== 206 && 64 | response.statusCode !== 0) { 65 | error = new Error('XHR Failed with a response of: ' + 66 | (request && (reqResponse || request.responseText))); 67 | } 68 | 69 | callback(error, request); 70 | }); 71 | const originalAbort = request.abort; 72 | 73 | request.abort = function() { 74 | request.aborted = true; 75 | return originalAbort.apply(request, arguments); 76 | }; 77 | request.uri = options.uri; 78 | request.requestTime = Date.now(); 79 | return request; 80 | }; 81 | 82 | return xhr; 83 | }; 84 | 85 | export default xhrFactory; 86 | -------------------------------------------------------------------------------- /src/sbot/video.js.bak/font/VideoJS.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomeshnet/meshstream/cf65ad551c6f1dcfda6e7404cc85994f93be499b/src/sbot/video.js.bak/font/VideoJS.ttf -------------------------------------------------------------------------------- /src/sbot/video.js.bak/font/VideoJS.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomeshnet/meshstream/cf65ad551c6f1dcfda6e7404cc85994f93be499b/src/sbot/video.js.bak/font/VideoJS.woff -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ar.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ar",{ 2 | "Play": "تشغيل", 3 | "Pause": "إيقاف", 4 | "Current Time": "الوقت الحالي", 5 | "Duration": "مدة", 6 | "Remaining Time": "الوقت المتبقي", 7 | "Stream Type": "نوع التيار", 8 | "LIVE": "مباشر", 9 | "Loaded": "تم التحميل", 10 | "Progress": "التقدم", 11 | "Fullscreen": "ملء الشاشة", 12 | "Non-Fullscreen": "تعطيل ملء الشاشة", 13 | "Mute": "صامت", 14 | "Unmute": "غير الصامت", 15 | "Playback Rate": "معدل التشغيل", 16 | "Subtitles": "الترجمة", 17 | "subtitles off": "إيقاف الترجمة", 18 | "Captions": "التعليقات", 19 | "captions off": "إيقاف التعليقات", 20 | "Chapters": "فصول", 21 | "You aborted the media playback": "لقد ألغيت تشغيل الفيديو", 22 | "A network error caused the media download to fail part-way.": "تسبب خطأ في الشبكة بفشل تحميل الفيديو بالكامل.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "لا يمكن تحميل الفيديو بسبب فشل في الخادوم أو الشبكة ، أو فشل بسبب عدم إمكانية قراءة تنسيق الفيديو.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "تم إيقاف تشغيل الفيديو بسبب مشكلة فساد أو لأن الفيديو المستخدم يستخدم ميزات غير مدعومة من متصفحك.", 25 | "No compatible source was found for this media.": "فشل العثور على أي مصدر متوافق مع هذا الفيديو.", 26 | "Play Video": "تشغيل الفيديو", 27 | "Close": "أغلق", 28 | "Modal Window": "نافذة مشروطة", 29 | "This is a modal window": "هذه نافذة مشروطة", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "يمكن غلق هذه النافذة المشروطة عن طريق الضغط على زر الخروج أو تفعيل زر الإغلاق", 31 | ", opens captions settings dialog": ", تفتح نافذة خيارات التعليقات", 32 | ", opens subtitles settings dialog": ", تفتح نافذة خيارات الترجمة", 33 | ", selected": ", مختار" 34 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ba.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ba",{ 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration": "Vrijeme trajanja", 6 | "Remaining Time": "Preostalo vrijeme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Puni ekran", 12 | "Non-Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/bg.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("bg",{ 2 | "Play": "Възпроизвеждане", 3 | "Pause": "Пауза", 4 | "Current Time": "Текущо време", 5 | "Duration": "Продължителност", 6 | "Remaining Time": "Оставащо време", 7 | "Stream Type": "Тип на потока", 8 | "LIVE": "НА ЖИВО", 9 | "Loaded": "Заредено", 10 | "Progress": "Прогрес", 11 | "Fullscreen": "Цял екран", 12 | "Non-Fullscreen": "Спиране на цял екран", 13 | "Mute": "Без звук", 14 | "Unmute": "Със звук", 15 | "Playback Rate": "Скорост на възпроизвеждане", 16 | "Subtitles": "Субтитри", 17 | "subtitles off": "Спряни субтитри", 18 | "Captions": "Аудио надписи", 19 | "captions off": "Спряни аудио надписи", 20 | "Chapters": "Глави", 21 | "You aborted the media playback": "Спряхте възпроизвеждането на видеото", 22 | "A network error caused the media download to fail part-way.": "Грешка в мрежата провали изтеглянето на видеото.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Видеото не може да бъде заредено заради проблем със сървъра или мрежата или защото този формат не е поддържан.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Възпроизвеждането на видеото беше прекъснато заради проблем с файла или защото видеото използва опции които браузърът Ви не поддържа.", 25 | "No compatible source was found for this media.": "Не беше намерен съвместим източник за това видео." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ca.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ca",{ 2 | "Play": "Reproducció", 3 | "Pause": "Pausa", 4 | "Current Time": "Temps reproduït", 5 | "Duration": "Durada total", 6 | "Remaining Time": "Temps restant", 7 | "Stream Type": "Tipus de seqüència", 8 | "LIVE": "EN DIRECTE", 9 | "Loaded": "Carregat", 10 | "Progress": "Progrés", 11 | "Fullscreen": "Pantalla completa", 12 | "Non-Fullscreen": "Pantalla no completa", 13 | "Mute": "Silencia", 14 | "Unmute": "Amb so", 15 | "Playback Rate": "Velocitat de reproducció", 16 | "Subtitles": "Subtítols", 17 | "subtitles off": "Subtítols desactivats", 18 | "Captions": "Llegendes", 19 | "captions off": "Llegendes desactivades", 20 | "Chapters": "Capítols", 21 | "You aborted the media playback": "Heu interromput la reproducció del vídeo.", 22 | "A network error caused the media download to fail part-way.": "Un error de la xarxa ha interromput la baixada del vídeo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No s'ha pogut carregar el vídeo perquè el servidor o la xarxa han fallat, o bé perquè el seu format no és compatible.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducció de vídeo s'ha interrumput per un problema de corrupció de dades o bé perquè el vídeo demanava funcions que el vostre navegador no ofereix.", 25 | "No compatible source was found for this media.": "No s'ha trobat cap font compatible amb el vídeo." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/cs.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("cs",{ 2 | "Play": "Přehrát", 3 | "Pause": "Pauza", 4 | "Current Time": "Aktuální čas", 5 | "Duration": "Doba trvání", 6 | "Remaining Time": "Zbývající čas", 7 | "Stream Type": "Stream Type", 8 | "LIVE": "ŽIVĚ", 9 | "Loaded": "Načteno", 10 | "Progress": "Stav", 11 | "Fullscreen": "Celá obrazovka", 12 | "Non-Fullscreen": "Zmenšená obrazovka", 13 | "Mute": "Ztlumit zvuk", 14 | "Unmute": "Přehrát zvuk", 15 | "Playback Rate": "Rychlost přehrávání", 16 | "Subtitles": "Titulky", 17 | "subtitles off": "Titulky vypnuty", 18 | "Captions": "Popisky", 19 | "captions off": "Popisky vypnuty", 20 | "Chapters": "Kapitoly", 21 | "You aborted the media playback": "Přehrávání videa je přerušeno.", 22 | "A network error caused the media download to fail part-way.": "Video nemohlo být načteno, kvůli chybě v síti.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video nemohlo být načteno, buď kvůli chybě serveru nebo sítě nebo proto, že daný formát není podporován.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Váš prohlížeč nepodporuje formát videa.", 25 | "No compatible source was found for this media.": "Špatně zadaný zdroj videa." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/da.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("da",{ 2 | "Play": "Afspil", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuel tid", 5 | "Duration": "Varighed", 6 | "Remaining Time": "Resterende tid", 7 | "Stream Type": "Stream-type", 8 | "LIVE": "LIVE", 9 | "Loaded": "Indlæst", 10 | "Progress": "Status", 11 | "Fullscreen": "Fuldskærm", 12 | "Non-Fullscreen": "Luk fuldskærm", 13 | "Mute": "Uden lyd", 14 | "Unmute": "Med lyd", 15 | "Playback Rate": "Afspilningsrate", 16 | "Subtitles": "Undertekster", 17 | "subtitles off": "Uden undertekster", 18 | "Captions": "Undertekster for hørehæmmede", 19 | "captions off": "Uden undertekster for hørehæmmede", 20 | "Chapters": "Kapitler", 21 | "You aborted the media playback": "Du afbrød videoafspilningen.", 22 | "A network error caused the media download to fail part-way.": "En netværksfejl fik download af videoen til at fejle.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke indlæses, enten fordi serveren eller netværket fejlede, eller fordi formatet ikke er understøttet.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoafspilningen blev afbrudt på grund af ødelagte data eller fordi videoen benyttede faciliteter som din browser ikke understøtter.", 25 | "No compatible source was found for this media.": "Fandt ikke en kompatibel kilde for denne media." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/de.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("de",{ 2 | "Play": "Wiedergabe", 3 | "Pause": "Pause", 4 | "Replay": "Erneut abspielen", 5 | "Current Time": "Aktueller Zeitpunkt", 6 | "Duration": "Dauer", 7 | "Remaining Time": "Verbleibende Zeit", 8 | "Stream Type": "Streamtyp", 9 | "LIVE": "LIVE", 10 | "Loaded": "Geladen", 11 | "Progress": "Status", 12 | "Fullscreen": "Vollbild", 13 | "Non-Fullscreen": "Kein Vollbild", 14 | "Mute": "Ton aus", 15 | "Unmute": "Ton ein", 16 | "Playback Rate": "Wiedergabegeschwindigkeit", 17 | "Subtitles": "Untertitel", 18 | "subtitles off": "Untertitel aus", 19 | "Captions": "Untertitel", 20 | "captions off": "Untertitel aus", 21 | "Chapters": "Kapitel", 22 | "You aborted the media playback": "Sie haben die Videowiedergabe abgebrochen.", 23 | "A network error caused the media download to fail part-way.": "Der Videodownload ist aufgrund eines Netzwerkfehlers fehlgeschlagen.", 24 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Das Video konnte nicht geladen werden, da entweder ein Server- oder Netzwerkfehler auftrat oder das Format nicht unterstützt wird.", 25 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Die Videowiedergabe wurde entweder wegen eines Problems mit einem beschädigten Video oder wegen verwendeten Funktionen, die vom Browser nicht unterstützt werden, abgebrochen.", 26 | "No compatible source was found for this media.": "Für dieses Video wurde keine kompatible Quelle gefunden.", 27 | "Play Video": "Video abspielen", 28 | "Close": "Schließen", 29 | "Modal Window": "Modales Fenster", 30 | "This is a modal window": "Dies ist ein modales Fenster", 31 | "This modal can be closed by pressing the Escape key or activating the close button.": "Durch Drücken der Esc-Taste bzw. Betätigung der Schaltfläche \"Schließen\" wird dieses modale Fenster geschlossen.", 32 | ", opens captions settings dialog": ", öffnet Einstellungen für Untertitel", 33 | ", opens subtitles settings dialog": ", öffnet Einstellungen für Untertitel", 34 | ", selected": ", ausgewählt", 35 | "captions settings": "Untertiteleinstellungen", 36 | "subtitles settings": "Untertiteleinstellungen", 37 | "descriptions settings": "Einstellungen für Beschreibungen", 38 | "Close Modal Dialog": "Modales Fenster schließen", 39 | "Descriptions": "Beschreibungen", 40 | "descriptions off": "Beschreibungen aus", 41 | "The media is encrypted and we do not have the keys to decrypt it.": "Die Entschlüsselungsschlüssel für den verschlüsselten Medieninhalt sind nicht verfügbar.", 42 | ", opens descriptions settings dialog": ", öffnet Einstellungen für Beschreibungen", 43 | "Audio Track": "Tonspur", 44 | "Text": "Schrift", 45 | "White": "Weiß", 46 | "Black": "Schwarz", 47 | "Red": "Rot", 48 | "Green": "Grün", 49 | "Blue": "Blau", 50 | "Yellow": "Gelb", 51 | "Magenta": "Magenta", 52 | "Cyan": "Türkis", 53 | "Background": "Hintergrund", 54 | "Window": "Fenster", 55 | "Transparent": "Durchsichtig", 56 | "Semi-Transparent": "Halbdurchsichtig", 57 | "Opaque": "Undurchsictig", 58 | "Font Size": "Schriftgröße", 59 | "Text Edge Style": "Textkantenstil", 60 | "None": "Kein", 61 | "Raised": "Erhoben", 62 | "Depressed": "Gedrückt", 63 | "Uniform": "Uniform", 64 | "Dropshadow": "Schlagschatten", 65 | "Font Family": "Schristfamilie", 66 | "Proportional Sans-Serif": "Proportionale Sans-Serif", 67 | "Monospace Sans-Serif": "Monospace Sans-Serif", 68 | "Proportional Serif": "Proportionale Serif", 69 | "Monospace Serif": "Monospace Serif", 70 | "Casual": "Zwanglos", 71 | "Script": "Schreibeschrift", 72 | "Small Caps": "Small-Caps", 73 | "Reset": "Zurücksetzen", 74 | "restore all settings to the default values": "Alle Einstellungen auf die Standardwerte zurücksetzen", 75 | "Done": "Fertig", 76 | "Caption Settings Dialog": "Einstellungsdialog für Untertitel", 77 | "Beginning of dialog window. Escape will cancel and close the window.": "Anfang des Dialogfensters. Esc bricht ab und schließt das Fenster.", 78 | "End of dialog window.": "Ende des Dialogfensters.", 79 | "Audio Player": "Audio-Player", 80 | "Video Player": "Video-Player", 81 | "Progress Bar": "Forschrittsbalken", 82 | "progress bar timing: currentTime={1} duration={2}": "{1} von {2}", 83 | "Volume Level": "Lautstärkestufe" 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/el.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("el",{ 2 | "Play": "Aναπαραγωγή", 3 | "Pause": "Παύση", 4 | "Current Time": "Τρέχων χρόνος", 5 | "Duration": "Συνολικός χρόνος", 6 | "Remaining Time": "Υπολοιπόμενος χρόνος", 7 | "Stream Type": "Τύπος ροής", 8 | "LIVE": "ΖΩΝΤΑΝΑ", 9 | "Loaded": "Φόρτωση επιτυχής", 10 | "Progress": "Πρόοδος", 11 | "Fullscreen": "Πλήρης οθόνη", 12 | "Non-Fullscreen": "Έξοδος από πλήρη οθόνη", 13 | "Mute": "Σίγαση", 14 | "Unmute": "Kατάργηση σίγασης", 15 | "Playback Rate": "Ρυθμός αναπαραγωγής", 16 | "Subtitles": "Υπότιτλοι", 17 | "subtitles off": "απόκρυψη υπότιτλων", 18 | "Captions": "Λεζάντες", 19 | "captions off": "απόκρυψη λεζάντων", 20 | "Chapters": "Κεφάλαια", 21 | "Close Modal Dialog": "Κλείσιμο παραθύρου", 22 | "Descriptions": "Περιγραφές", 23 | "descriptions off": "απόκρυψη περιγραφών", 24 | "Audio Track": "Ροή ήχου", 25 | "You aborted the media playback": "Ακυρώσατε την αναπαραγωγή", 26 | "A network error caused the media download to fail part-way.": "Ένα σφάλμα δικτύου προκάλεσε την αποτυχία μεταφόρτωσης του αρχείου προς αναπαραγωγή.", 27 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Το αρχείο προς αναπαραγωγή δεν ήταν δυνατό να φορτωθεί είτε γιατί υπήρξε σφάλμα στον διακομιστή ή το δίκτυο, είτε γιατί ο τύπος του αρχείου δεν υποστηρίζεται.", 28 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Η αναπαραγωγή ακυρώθηκε είτε λόγω κατεστραμμένου αρχείου, είτε γιατί το αρχείο απαιτεί λειτουργίες που δεν υποστηρίζονται από το πρόγραμμα περιήγησης που χρησιμοποιείτε.", 29 | "No compatible source was found for this media.": "Δεν βρέθηκε συμβατή πηγή αναπαραγωγής για το συγκεκριμένο αρχείο.", 30 | "The media is encrypted and we do not have the keys to decrypt it.": "Το αρχείο προς αναπαραγωγή είναι κρυπτογραφημένo και δεν υπάρχουν τα απαραίτητα κλειδιά αποκρυπτογράφησης.", 31 | "Play Video": "Αναπαραγωγή βίντεο", 32 | "Close": "Κλείσιμο", 33 | "Modal Window": "Aναδυόμενο παράθυρο", 34 | "This is a modal window": "Το παρών είναι ένα αναδυόμενο παράθυρο", 35 | "This modal can be closed by pressing the Escape key or activating the close button.": "Αυτό το παράθυρο μπορεί να εξαφανιστεί πατώντας το πλήκτρο Escape ή πατώντας το κουμπί κλεισίματος.", 36 | ", opens captions settings dialog": ", εμφανίζει τις ρυθμίσεις για τις λεζάντες", 37 | ", opens subtitles settings dialog": ", εμφανίζει τις ρυθμίσεις για τους υπότιτλους", 38 | ", opens descriptions settings dialog": ", εμφανίζει τις ρυθμίσεις για τις περιγραφές", 39 | ", selected": ", επιλεγμένο" 40 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/en.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("en",{ 2 | "Audio Player": "Audio Player", 3 | "Video Player": "Video Player", 4 | "Play": "Play", 5 | "Pause": "Pause", 6 | "Replay": "Replay", 7 | "Current Time": "Current Time", 8 | "Duration": "Duration", 9 | "Remaining Time": "Remaining Time", 10 | "Stream Type": "Stream Type", 11 | "LIVE": "LIVE", 12 | "Loaded": "Loaded", 13 | "Progress": "Progress", 14 | "Progress Bar": "Progress Bar", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} of {2}", 16 | "Fullscreen": "Fullscreen", 17 | "Non-Fullscreen": "Non-Fullscreen", 18 | "Mute": "Mute", 19 | "Unmute": "Unmute", 20 | "Playback Rate": "Playback Rate", 21 | "Subtitles": "Subtitles", 22 | "subtitles off": "subtitles off", 23 | "Captions": "Captions", 24 | "captions off": "captions off", 25 | "Chapters": "Chapters", 26 | "Descriptions": "Descriptions", 27 | "descriptions off": "descriptions off", 28 | "Audio Track": "Audio Track", 29 | "Volume Level": "Volume Level", 30 | "You aborted the media playback": "You aborted the media playback", 31 | "A network error caused the media download to fail part-way.": "A network error caused the media download to fail part-way.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "The media could not be loaded, either because the server or network failed or because the format is not supported.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.", 34 | "No compatible source was found for this media.": "No compatible source was found for this media.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "The media is encrypted and we do not have the keys to decrypt it.", 36 | "Play Video": "Play Video", 37 | "Close": "Close", 38 | "Close Modal Dialog": "Close Modal Dialog", 39 | "Modal Window": "Modal Window", 40 | "This is a modal window": "This is a modal window", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "This modal can be closed by pressing the Escape key or activating the close button.", 42 | ", opens captions settings dialog": ", opens captions settings dialog", 43 | ", opens subtitles settings dialog": ", opens subtitles settings dialog", 44 | ", opens descriptions settings dialog": ", opens descriptions settings dialog", 45 | ", selected": ", selected", 46 | "captions settings": "captions settings", 47 | "subtitles settings": "subititles settings", 48 | "descriptions settings": "descriptions settings", 49 | "Text": "Text", 50 | "White": "White", 51 | "Black": "Black", 52 | "Red": "Red", 53 | "Green": "Green", 54 | "Blue": "Blue", 55 | "Yellow": "Yellow", 56 | "Magenta": "Magenta", 57 | "Cyan": "Cyan", 58 | "Background": "Background", 59 | "Window": "Window", 60 | "Transparent": "Transparent", 61 | "Semi-Transparent": "Semi-Transparent", 62 | "Opaque": "Opaque", 63 | "Font Size": "Font Size", 64 | "Text Edge Style": "Text Edge Style", 65 | "None": "None", 66 | "Raised": "Raised", 67 | "Depressed": "Depressed", 68 | "Uniform": "Uniform", 69 | "Dropshadow": "Dropshadow", 70 | "Font Family": "Font Family", 71 | "Proportional Sans-Serif": "Proportional Sans-Serif", 72 | "Monospace Sans-Serif": "Monospace Sans-Serif", 73 | "Proportional Serif": "Proportional Serif", 74 | "Monospace Serif": "Monospace Serif", 75 | "Casual": "Casual", 76 | "Script": "Script", 77 | "Small Caps": "Small Caps", 78 | "Reset": "Reset", 79 | "restore all settings to the default values": "restore all settings to the default values", 80 | "Done": "Done", 81 | "Caption Settings Dialog": "Caption Settings Dialog", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window.", 83 | "End of dialog window.": "End of dialog window.", 84 | "{1} is loading.": "{1} is loading." 85 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/es.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("es",{ 2 | "Play": "Reproducción", 3 | "Play Video": "Reproducción Vídeo", 4 | "Pause": "Pausa", 5 | "Current Time": "Tiempo reproducido", 6 | "Duration": "Duración total", 7 | "Remaining Time": "Tiempo restante", 8 | "Stream Type": "Tipo de secuencia", 9 | "LIVE": "DIRECTO", 10 | "Loaded": "Cargado", 11 | "Progress": "Progreso", 12 | "Fullscreen": "Pantalla completa", 13 | "Non-Fullscreen": "Pantalla no completa", 14 | "Mute": "Silenciar", 15 | "Unmute": "No silenciado", 16 | "Playback Rate": "Velocidad de reproducción", 17 | "Subtitles": "Subtítulos", 18 | "subtitles off": "Subtítulos desactivados", 19 | "Captions": "Subtítulos especiales", 20 | "captions off": "Subtítulos especiales desactivados", 21 | "Chapters": "Capítulos", 22 | "You aborted the media playback": "Ha interrumpido la reproducción del vídeo.", 23 | "A network error caused the media download to fail part-way.": "Un error de red ha interrumpido la descarga del vídeo.", 24 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No se ha podido cargar el vídeo debido a un fallo de red o del servidor o porque el formato es incompatible.", 25 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducción de vídeo se ha interrumpido por un problema de corrupción de datos o porque el vídeo precisa funciones que su navegador no ofrece.", 26 | "No compatible source was found for this media.": "No se ha encontrado ninguna fuente compatible con este vídeo." 27 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/fa.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("fa",{ 2 | "Audio Player": "پخش کننده صوتی", 3 | "Video Player": "پخش کننده ویدیو", 4 | "Play": "پخش", 5 | "Pause": "مکث", 6 | "Replay": "بازپخش", 7 | "Current Time": "زمان کنونی", 8 | "Duration": "مدت زمان", 9 | "Remaining Time": "زمان باقیمانده", 10 | "Stream Type": "نوع استریم", 11 | "LIVE": "زنده", 12 | "Loaded": "بارگیری شده", 13 | "Progress": "پیشرفت", 14 | "Progress Bar": "نوار پیشرفت", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} از {2}", 16 | "Fullscreen": "تمام‌صفحه", 17 | "Non-Fullscreen": "غیر تمام‌صفحه", 18 | "Mute": "بی صدا", 19 | "Unmute": "صدا دار", 20 | "Playback Rate": "سرعت پخش", 21 | "Subtitles": "زیرنویس", 22 | "subtitles off": "بدون زیرنویس", 23 | "Captions": "زیرتوضیح", 24 | "captions off": "بدون زیرتوضیح", 25 | "Chapters": "قسمت‌ها", 26 | "Descriptions": "توصیف", 27 | "descriptions off": "بدون توصیف", 28 | "Audio Track": "صوت", 29 | "Volume Level": "میزان صدا", 30 | "You aborted the media playback": "شما پخش را قطع کردید.", 31 | "A network error caused the media download to fail part-way.": "خطای شبکه باعث عدم بارگیری بخشی از رسانه شد.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": ".رسانه قابل بارگیری نیست. علت آن ممکن است خطا در اتصال یا عدم پشتیبانی از فرمت باشد", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "پخش رسانه به علت اشکال در آن یا عدم پشتیبانی مرورگر شما قطع شد.", 34 | "No compatible source was found for this media.": "هیچ منبع سازگاری، برای این رسانه پیدا نشد.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "این رسانه رمزنگاری شده است و ما کلید رمزگشایی آن را نداریم.", 36 | "Play Video": "پخش ویدیو", 37 | "Close": "بستن", 38 | "Close Modal Dialog": "بستن پنجره مودال", 39 | "Modal Window": "پنجره مودال", 40 | "This is a modal window": "این پنجره مودال", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "این پنجره با دکمه اسکیپ با دکمه بستن قابل بسته شدن میباشد.", 42 | ", opens captions settings dialog": ", تنظیمات زیرتوضیح را باز میکند", 43 | ", opens subtitles settings dialog": ", تنظیمات زیرنویس را باز میکند", 44 | ", opens descriptions settings dialog": ", تنظیمات توصیفات را باز میکند", 45 | ", selected": ", انتخاب شده", 46 | "captions settings": "تنظیمات زیرتوضیح", 47 | "subtitles settings": "تنظیمات زیرنویس", 48 | "descriptions settings": "تنظیمات توصیفات", 49 | "Text": "متن", 50 | "White": "سفید", 51 | "Black": "سیاه", 52 | "Red": "قرمز", 53 | "Green": "سبز", 54 | "Blue": "آبی", 55 | "Yellow": "زرد", 56 | "Magenta": "ارغوانی", 57 | "Cyan": "سبزآبی", 58 | "Background": "زمینه", 59 | "Window": "پنجره", 60 | "Transparent": "شفاف", 61 | "Semi-Transparent": "نیمه شفاف", 62 | "Opaque": "مات", 63 | "Font Size": "اندازه فونت", 64 | "Text Edge Style": "سبک لبه متن", 65 | "None": "هیچ", 66 | "Raised": "برآمده", 67 | "Depressed": "فرورفته", 68 | "Uniform": "یکنواخت", 69 | "Dropshadow": "سایه دار", 70 | "Font Family": "نوع فونت", 71 | "Proportional Sans-Serif": "سنس-سریف متناسب", 72 | "Monospace Sans-Serif": "سنس-سریف هم اندازه", 73 | "Proportional Serif": "سریف متناسب", 74 | "Monospace Serif": "سریف هم اندازه", 75 | "Casual": "فانتزی", 76 | "Script": "دست خط", 77 | "Small Caps": "حروف کوچک به بزرگ", 78 | "Reset": "باز نشاندن", 79 | "restore all settings to the default values": "بازیابی همه تنظیمات به حالت اولیه", 80 | "Done": "تکمیل", 81 | "Caption Settings Dialog": "پنجره تنظیمات عناوین", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "ابتدای پنجره محاوره‌ای. دکمه اسکیپ پنجره را لغو میکند و میبندد.", 83 | "End of dialog window.": "انتهای پنجره محاوره‌ای." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/fi.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("fi",{ 2 | "Play": "Toisto", 3 | "Pause": "Tauko", 4 | "Current Time": "Tämänhetkinen aika", 5 | "Duration": "Kokonaisaika", 6 | "Remaining Time": "Jäljellä oleva aika", 7 | "Stream Type": "Lähetystyyppi", 8 | "LIVE": "LIVE", 9 | "Loaded": "Ladattu", 10 | "Progress": "Edistyminen", 11 | "Fullscreen": "Koko näyttö", 12 | "Non-Fullscreen": "Koko näyttö pois", 13 | "Mute": "Ääni pois", 14 | "Unmute": "Ääni päällä", 15 | "Playback Rate": "Toistonopeus", 16 | "Subtitles": "Tekstitys", 17 | "subtitles off": "Tekstitys pois", 18 | "Captions": "Tekstitys", 19 | "captions off": "Tekstitys pois", 20 | "Chapters": "Kappaleet", 21 | "You aborted the media playback": "Olet keskeyttänyt videotoiston.", 22 | "A network error caused the media download to fail part-way.": "Verkkovirhe keskeytti videon latauksen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videon lataus ei onnistunut joko palvelin- tai verkkovirheestä tai väärästä formaatista johtuen.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videon toisto keskeytyi, koska media on vaurioitunut tai käyttää käyttää toimintoja, joita selaimesi ei tue.", 25 | "No compatible source was found for this media.": "Tälle videolle ei löytynyt yhteensopivaa lähdettä." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/fr.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("fr",{ 2 | "Audio Player": "Lecteur audio", 3 | "Video Player": "Lecteur vidéo", 4 | "Play": "Lecture", 5 | "Pause": "Pause", 6 | "Replay": "Revoir", 7 | "Current Time": "Temps actuel", 8 | "Duration": "Durée", 9 | "Remaining Time": "Temps restant", 10 | "Stream Type": "Type de flux", 11 | "LIVE": "EN DIRECT", 12 | "Loaded": "Chargé", 13 | "Progress": "Progression", 14 | "Progress Bar": "Barre de progression", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} de {2}", 16 | "Fullscreen": "Plein écran", 17 | "Non-Fullscreen": "Fenêtré", 18 | "Mute": "Sourdine", 19 | "Unmute": "Son activé", 20 | "Playback Rate": "Vitesse de lecture", 21 | "Subtitles": "Sous-titres", 22 | "subtitles off": "Sous-titres désactivés", 23 | "Captions": "Sous-titres transcrits", 24 | "captions off": "Sous-titres transcrits désactivés", 25 | "Chapters": "Chapitres", 26 | "Descriptions": "Descriptions", 27 | "descriptions off": "descriptions désactivées", 28 | "Audio Track": "Piste audio", 29 | "Volume Level": "Niveau de volume", 30 | "You aborted the media playback": "Vous avez interrompu la lecture de la vidéo.", 31 | "A network error caused the media download to fail part-way.": "Une erreur de réseau a interrompu le téléchargement de la vidéo.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur.", 34 | "No compatible source was found for this media.": "Aucune source compatible n'a été trouvée pour cette vidéo.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "Le média est chiffré et nous n'avons pas les clés pour le déchiffrer.", 36 | "Play Video": "Lire la vidéo", 37 | "Close": "Fermer", 38 | "Close Modal Dialog": "Fermer la boîte de dialogue modale", 39 | "Modal Window": "Fenêtre modale", 40 | "This is a modal window": "Ceci est une fenêtre modale", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Ce modal peut être fermé en appuyant sur la touche Échap ou activer le bouton de fermeture.", 42 | ", opens captions settings dialog": ", ouvrir les paramètres des sous-titres transcrits", 43 | ", opens subtitles settings dialog": ", ouvrir les paramètres des sous-titres", 44 | ", opens descriptions settings dialog": ", ouvrir les paramètres des descriptions", 45 | ", selected": ", sélectionné", 46 | "captions settings": "Paramètres des sous-titres transcrits", 47 | "subtitles settings": "Paramètres des sous-titres", 48 | "descriptions settings": "Paramètres des descriptions", 49 | "Text": "Texte", 50 | "White": "Blanc", 51 | "Black": "Noir", 52 | "Red": "Rouge", 53 | "Green": "Vert", 54 | "Blue": "Bleu", 55 | "Yellow": "Jaune", 56 | "Magenta": "Magenta", 57 | "Cyan": "Cyan", 58 | "Background": "Arrière-plan", 59 | "Window": "Fenêtre", 60 | "Transparent": "Transparent", 61 | "Semi-Transparent": "Semi-transparent", 62 | "Opaque": "Opaque", 63 | "Font Size": "Taille des caractères", 64 | "Text Edge Style": "Style des contours du texte", 65 | "None": "Aucun", 66 | "Raised": "Élevé", 67 | "Depressed": "Enfoncé", 68 | "Uniform": "Uniforme", 69 | "Dropshadow": "Ombre portée", 70 | "Font Family": "Famille de polices", 71 | "Proportional Sans-Serif": "Polices à chasse variable sans empattement (Proportional Sans-Serif)", 72 | "Monospace Sans-Serif": "Polices à chasse fixe sans empattement (Monospace Sans-Serif)", 73 | "Proportional Serif": "Polices à chasse variable avec empattement (Proportional Serif)", 74 | "Monospace Serif": "Polices à chasse fixe avec empattement (Monospace Serif)", 75 | "Casual": "Manuscrite", 76 | "Script": "Scripte", 77 | "Small Caps": "Petites capitales", 78 | "Reset": "Réinitialiser", 79 | "restore all settings to the default values": "Restaurer tous les paramètres aux valeurs par défaut", 80 | "Done": "Terminé", 81 | "Caption Settings Dialog": "Boîte de dialogue des paramètres des sous-titres transcrits", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre.", 83 | "End of dialog window.": "Fin de la fenêtre de dialogue." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/gl.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("gl",{ 2 | "Play": "Reprodución", 3 | "Play Video": "Reprodución Vídeo", 4 | "Pause": "Pausa", 5 | "Current Time": "Tempo reproducido", 6 | "Duration": "Duración total", 7 | "Remaining Time": "Tempo restante", 8 | "Stream Type": "Tipo de secuencia", 9 | "LIVE": "DIRECTO", 10 | "Loaded": "Cargado", 11 | "Progress": "Progreso", 12 | "Fullscreen": "Pantalla completa", 13 | "Non-Fullscreen": "Pantalla non completa", 14 | "Mute": "Silenciar", 15 | "Unmute": "Non silenciado", 16 | "Playback Rate": "Velocidade de reprodución", 17 | "Subtitles": "Subtítulos", 18 | "subtitles off": "Subtítulos desactivados", 19 | "Captions": "Subtítulos con lenda", 20 | "captions off": "Subtítulos con lenda desactivados", 21 | "Chapters": "Capítulos", 22 | "You aborted the media playback": "Interrompeches a reprodución do vídeo.", 23 | "A network error caused the media download to fail part-way.": "Un erro de rede interrompeu a descarga do vídeo.", 24 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Non se puido cargar o vídeo debido a un fallo de rede ou do servidor ou porque o formato é incompatible.", 25 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reproducción de vídeo interrompeuse por un problema de corrupción de datos ou porque o vídeo precisa funcións que o teu navegador non ofrece.", 26 | "No compatible source was found for this media.": "Non se atopou ningunha fonte compatible con este vídeo." 27 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/he.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("he",{ 2 | "Audio Player": "נַגָּן שמע", 3 | "Video Player": "נַגָּן וידאו", 4 | "Play": "נַגֵּן", 5 | "Pause": "השהה", 6 | "Replay": "נַגֵּן שוב", 7 | "Current Time": "זמן נוכחי", 8 | "Duration": "זמן כולל", 9 | "Remaining Time": "זמן נותר", 10 | "Stream Type": "סוג Stream", 11 | "LIVE": "שידור חי", 12 | "Loaded": "נטען", 13 | "Progress": "התקדמות", 14 | "Progress Bar": "סרגל התקדמות", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} מתוך {2}", 16 | "Fullscreen": "מסך מלא", 17 | "Non-Fullscreen": "מסך לא מלא", 18 | "Mute": "השתק", 19 | "Unmute": "בטל השתקה", 20 | "Playback Rate": "קצב ניגון", 21 | "Subtitles": "כתוביות", 22 | "subtitles off": "כתוביות כבויות", 23 | "Captions": "כיתובים", 24 | "captions off": "כיתובים כבויים", 25 | "Chapters": "פרקים", 26 | "Descriptions": "תיאורים", 27 | "descriptions off": "תיאורים כבויים", 28 | "Audio Track": "רצועת שמע", 29 | "Volume Level": "רמת ווליום", 30 | "You aborted the media playback": "ביטלת את השמעת המדיה", 31 | "A network error caused the media download to fail part-way.": "שגיאת רשת גרמה להורדת המדיה להיכשל באמצע.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "לא ניתן לטעון את המדיה, או מכיוון שהרשת או השרת כשלו או מכיוון שהפורמט אינו נתמך.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "השמעת המדיה בוטלה בשל בעית השחטת מידע או מכיוון שהמדיה עשתה שימוש בתכונות שהדפדפן שלך לא תמך בהן.", 34 | "No compatible source was found for this media.": "לא נמצא מקור תואם עבור מדיה זו.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "המדיה מוצפנת ואין בידינו את המפתח כדי לפענח אותה.", 36 | "Play Video": "נַגֵּן וידאו", 37 | "Close": "סְגוֹר", 38 | "Close Modal Dialog": "סְגוֹר דו-שיח מודאלי", 39 | "Modal Window": "חלון מודאלי", 40 | "This is a modal window": "זהו חלון מודאלי", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "ניתן לסגור חלון מודאלי זה ע\"י לחיצה על כפתור ה-Escape או הפעלת כפתור הסגירה.", 42 | ", opens captions settings dialog": ", פותח חלון הגדרות כיתובים", 43 | ", opens subtitles settings dialog": ", פותח חלון הגדרות כתוביות", 44 | ", opens descriptions settings dialog": ", פותח חלון הגדרות תיאורים", 45 | ", selected": ", נבחר/ו", 46 | "captions settings": "הגדרות כיתובים", 47 | "subtitles settings": "הגדרות כתוביות", 48 | "descriptions settings": "הגדרות תיאורים", 49 | "Text": "טקסט", 50 | "White": "לבן", 51 | "Black": "שחור", 52 | "Red": "אדום", 53 | "Green": "ירוק", 54 | "Blue": "כחול", 55 | "Yellow": "צהוב", 56 | "Magenta": "מַגֶ'נטָה", 57 | "Cyan": "טורקיז", 58 | "Background": "רקע", 59 | "Window": "חלון", 60 | "Transparent": "שקוף", 61 | "Semi-Transparent": "שקוף למחצה", 62 | "Opaque": "אָטוּם", 63 | "Font Size": "גודל גופן", 64 | "Text Edge Style": "סגנון קצוות טקסט", 65 | "None": "ללא", 66 | "Raised": "מורם", 67 | "Depressed": "מורד", 68 | "Uniform": "אחיד", 69 | "Dropshadow": "הטלת צל", 70 | "Font Family": "משפחת גופן", 71 | "Proportional Sans-Serif": "פרופורציוני וללא תגיות (Proportional Sans-Serif)", 72 | "Monospace Sans-Serif": "ברוחב אחיד וללא תגיות (Monospace Sans-Serif)", 73 | "Proportional Serif": "פרופורציוני ועם תגיות (Proportional Serif)", 74 | "Monospace Serif": "ברוחב אחיד ועם תגיות (Monospace Serif)", 75 | "Casual": "אַגָבִי", 76 | "Script": "תסריט", 77 | "Small Caps": "אותיות קטנות", 78 | "Reset": "אִפּוּס", 79 | "restore all settings to the default values": "שחזר את כל ההגדרות לערכי ברירת המחדל", 80 | "Done": "בוצע", 81 | "Caption Settings Dialog": "דו-שיח הגדרות כיתובים", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "תחילת חלון דו-שיח. Escape יבטל ויסגור את החלון", 83 | "End of dialog window.": "סוף חלון דו-שיח." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/hr.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("hr",{ 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration": "Vrijeme trajanja", 6 | "Remaining Time": "Preostalo vrijeme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Puni ekran", 12 | "Non-Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/hu.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("hu",{ 2 | "Play": "Lejátszás", 3 | "Pause": "Szünet", 4 | "Current Time": "Aktuális időpont", 5 | "Duration": "Hossz", 6 | "Remaining Time": "Hátralévő idő", 7 | "Stream Type": "Adatfolyam típusa", 8 | "LIVE": "ÉLŐ", 9 | "Loaded": "Betöltve", 10 | "Progress": "Állapot", 11 | "Fullscreen": "Teljes képernyő", 12 | "Non-Fullscreen": "Normál méret", 13 | "Mute": "Némítás", 14 | "Unmute": "Némítás kikapcsolva", 15 | "Playback Rate": "Lejátszási sebesség", 16 | "Subtitles": "Feliratok", 17 | "subtitles off": "Feliratok kikapcsolva", 18 | "Captions": "Magyarázó szöveg", 19 | "captions off": "Magyarázó szöveg kikapcsolva", 20 | "Chapters": "Fejezetek", 21 | "You aborted the media playback": "Leállította a lejátszást", 22 | "A network error caused the media download to fail part-way.": "Hálózati hiba miatt a videó részlegesen töltődött le.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "A videó nem tölthető be hálózati vagy kiszolgálói hiba miatt, vagy a formátuma nem támogatott.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A lejátszás adatsérülés miatt leállt, vagy a videó egyes tulajdonságait a böngészője nem támogatja.", 25 | "No compatible source was found for this media.": "Nincs kompatibilis forrás ehhez a videóhoz." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/it.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("it",{ 2 | "Play": "Play", 3 | "Pause": "Pausa", 4 | "Current Time": "Orario attuale", 5 | "Duration": "Durata", 6 | "Remaining Time": "Tempo rimanente", 7 | "Stream Type": "Tipo del Streaming", 8 | "LIVE": "LIVE", 9 | "Loaded": "Caricato", 10 | "Progress": "Stato", 11 | "Fullscreen": "Schermo intero", 12 | "Non-Fullscreen": "Chiudi schermo intero", 13 | "Mute": "Muto", 14 | "Unmute": "Audio", 15 | "Playback Rate": "Tasso di riproduzione", 16 | "Subtitles": "Sottotitoli", 17 | "subtitles off": "Senza sottotitoli", 18 | "Captions": "Sottotitoli non udenti", 19 | "captions off": "Senza sottotitoli non udenti", 20 | "Chapters": "Capitolo", 21 | "You aborted the media playback": "La riproduzione del filmato è stata interrotta.", 22 | "A network error caused the media download to fail part-way.": "Il download del filmato è stato interrotto a causa di un problema rete.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Il filmato non può essere caricato a causa di un errore nel server o nella rete o perché il formato non viene supportato.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La riproduzione del filmato è stata interrotta a causa di un file danneggiato o per l’utilizzo di impostazioni non supportate dal browser.", 25 | "No compatible source was found for this media.": "Non ci sono fonti compatibili per questo filmato." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ja.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ja",{ 2 | "Play": "再生", 3 | "Pause": "一時停止", 4 | "Current Time": "現在の時間", 5 | "Duration": "長さ", 6 | "Remaining Time": "残りの時間", 7 | "Stream Type": "ストリームの種類", 8 | "LIVE": "ライブ", 9 | "Loaded": "ロード済み", 10 | "Progress": "進行状況", 11 | "Fullscreen": "フルスクリーン", 12 | "Non-Fullscreen": "フルスクリーン以外", 13 | "Mute": "ミュート", 14 | "Unmute": "ミュート解除", 15 | "Playback Rate": "再生レート", 16 | "Subtitles": "サブタイトル", 17 | "subtitles off": "サブタイトル オフ", 18 | "Captions": "キャプション", 19 | "captions off": "キャプション オフ", 20 | "Chapters": "チャプター", 21 | "You aborted the media playback": "動画再生を中止しました", 22 | "A network error caused the media download to fail part-way.": "ネットワーク エラーにより動画のダウンロードが途中で失敗しました", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "サーバーまたはネットワークのエラー、またはフォーマットがサポートされていないため、動画をロードできませんでした", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "破損の問題、またはお使いのブラウザがサポートしていない機能が動画に使用されていたため、動画の再生が中止されました", 25 | "No compatible source was found for this media.": "この動画に対して互換性のあるソースが見つかりませんでした" 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ko.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ko",{ 2 | "Play": "재생", 3 | "Pause": "일시중지", 4 | "Current Time": "현재 시간", 5 | "Duration": "지정 기간", 6 | "Remaining Time": "남은 시간", 7 | "Stream Type": "스트리밍 유형", 8 | "LIVE": "라이브", 9 | "Loaded": "로드됨", 10 | "Progress": "진행", 11 | "Fullscreen": "전체 화면", 12 | "Non-Fullscreen": "전체 화면 해제", 13 | "Mute": "음소거", 14 | "Unmute": "음소거 해제", 15 | "Playback Rate": "재생 비율", 16 | "Subtitles": "서브타이틀", 17 | "subtitles off": "서브타이틀 끄기", 18 | "Captions": "자막", 19 | "captions off": "자막 끄기", 20 | "Chapters": "챕터", 21 | "You aborted the media playback": "비디오 재생을 취소했습니다.", 22 | "A network error caused the media download to fail part-way.": "네트워크 오류로 인하여 비디오 일부를 다운로드하지 못 했습니다.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "비디오를 로드할 수 없습니다. 서버 혹은 네트워크 오류 때문이거나 지원되지 않는 형식 때문일 수 있습니다.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "비디오 재생이 취소됐습니다. 비디오가 손상되었거나 비디오가 사용하는 기능을 브라우저에서 지원하지 않는 것 같습니다.", 25 | "No compatible source was found for this media.": "비디오에 호환되지 않는 소스가 있습니다." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/nb.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("nb",{ 2 | "Play": "Spill", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuell tid", 5 | "Duration": "Varighet", 6 | "Remaining Time": "Gjenstående tid", 7 | "Stream Type": "Type strøm", 8 | "LIVE": "DIREKTE", 9 | "Loaded": "Lastet inn", 10 | "Progress": "Status", 11 | "Fullscreen": "Fullskjerm", 12 | "Non-Fullscreen": "Lukk fullskjerm", 13 | "Mute": "Lyd av", 14 | "Unmute": "Lyd på", 15 | "Playback Rate": "Avspillingsrate", 16 | "Subtitles": "Undertekst på", 17 | "subtitles off": "Undertekst av", 18 | "Captions": "Undertekst for hørselshemmede på", 19 | "captions off": "Undertekst for hørselshemmede av", 20 | "Chapters": "Kapitler", 21 | "You aborted the media playback": "Du avbrøt avspillingen.", 22 | "A network error caused the media download to fail part-way.": "En nettverksfeil avbrøt nedlasting av videoen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke lastes ned, på grunn av nettverksfeil eller serverfeil, eller fordi formatet ikke er støttet.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspillingen ble avbrudt på grunn av ødelagte data eller fordi videoen ville gjøre noe som nettleseren din ikke har støtte for.", 25 | "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/nl.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("nl",{ 2 | "Audio Player": "Audiospeler", 3 | "Video Player": "Videospeler", 4 | "Play": "Afspelen", 5 | "Pause": "Pauzeren", 6 | "Replay": "Opnieuw afspelen", 7 | "Current Time": "Huidige tijd", 8 | "Duration": "Tijdsduur", 9 | "Remaining Time": "Resterende tijd", 10 | "Stream Type": "Streamtype", 11 | "LIVE": "LIVE", 12 | "Loaded": "Geladen", 13 | "Progress": "Voortgang", 14 | "Progress Bar": "Voortgangsbalk", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} van {2}", 16 | "Fullscreen": "Volledig scherm", 17 | "Non-Fullscreen": "Geen volledig scherm", 18 | "Mute": "Dempen", 19 | "Unmute": "Niet dempen", 20 | "Playback Rate": "Afspeelsnelheid", 21 | "Subtitles": "Ondertiteling", 22 | "subtitles off": "ondertiteling uit", 23 | "Captions": "Bijschriften", 24 | "captions off": "bijschriften uit", 25 | "Chapters": "Hoofdstukken", 26 | "Descriptions": "Beschrijvingen", 27 | "descriptions off": "beschrijvingen uit", 28 | "Audio Track": "Audiospoor", 29 | "Volume Level": "Geluidsniveau", 30 | "You aborted the media playback": "U heeft het afspelen van de media afgebroken", 31 | "A network error caused the media download to fail part-way.": "Een netwerkfout heeft ervoor gezorgd dat het downloaden van de media halverwege is mislukt.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "De media kon niet worden geladen, dit komt doordat of de server of het netwerk mislukt of doordat het formaat niet wordt ondersteund.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Het afspelen van de media is afgebroken door een probleem met beschadeigde gegevens of doordat de media functies gebruikt die uw browser niet ondersteund.", 34 | "No compatible source was found for this media.": "Er is geen geschikte bron voor deze media gevonden.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "De media is versleuteld en we hebben de sleutels niet om deze te ontsleutelen.", 36 | "Play Video": "Video afspelen", 37 | "Close": "Sluiten", 38 | "Close Modal Dialog": "Extra venster sluiten", 39 | "Modal Window": "Extra venster", 40 | "This is a modal window": "Dit is een extra venster", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Dit venster kan worden gesloten door op de Escape-toets te drukken of door de sluiten-knop te activeren.", 42 | ", opens captions settings dialog": ", opent instellingen-venster voor bijschriften", 43 | ", opens subtitles settings dialog": ", opent instellingen-venster voor ondertitelingen", 44 | ", opens descriptions settings dialog": ", opent instellingen-venster voor beschrijvingen", 45 | ", selected": ", geselecteerd", 46 | "captions settings": "bijschriften-instellingen", 47 | "subtitles settings": "ondertiteling-instellingen", 48 | "descriptions settings": "beschrijvingen-instellingen", 49 | "Text": "Tekst", 50 | "White": "Wit", 51 | "Black": "Zwart", 52 | "Red": "Rood", 53 | "Green": "Groen", 54 | "Blue": "Blauw", 55 | "Yellow": "Geel", 56 | "Magenta": "Magenta", 57 | "Cyan": "Cyaan", 58 | "Background": "Achtergrond", 59 | "Window": "Venster", 60 | "Transparent": "Transparant", 61 | "Semi-Transparent": "Semi-transparant", 62 | "Opaque": "Ondoorzichtig", 63 | "Font Size": "Lettergrootte", 64 | "Text Edge Style": "Stijl tekstrand", 65 | "None": "Geen", 66 | "Raised": "Verhoogd", 67 | "Depressed": "Ingedrukt", 68 | "Uniform": "Uniform", 69 | "Dropshadow": "Schaduw", 70 | "Font Family": "Lettertype", 71 | "Proportional Sans-Serif": "Proportioneel sans-serif", 72 | "Monospace Sans-Serif": "Monospace sans-serif", 73 | "Proportional Serif": "Proportioneel serif", 74 | "Monospace Serif": "Monospace serif", 75 | "Casual": "Luchtig", 76 | "Script": "Script", 77 | "Small Caps": "Kleine hoofdletters", 78 | "Reset": "Herstellen", 79 | "restore all settings to the default values": "alle instellingen naar de standaardwaarden herstellen", 80 | "Done": "Klaar", 81 | "Caption Settings Dialog": "Venster voor bijschriften-instellingen", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Begin van dialoogvenster. Escape zal annuleren en het venster sluiten.", 83 | "End of dialog window.": "Einde van dialoogvenster." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/nn.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("nn",{ 2 | "Play": "Spel", 3 | "Pause": "Pause", 4 | "Current Time": "Aktuell tid", 5 | "Duration": "Varigheit", 6 | "Remaining Time": "Tid attende", 7 | "Stream Type": "Type straum", 8 | "LIVE": "DIREKTE", 9 | "Loaded": "Lasta inn", 10 | "Progress": "Status", 11 | "Fullscreen": "Fullskjerm", 12 | "Non-Fullscreen": "Stenga fullskjerm", 13 | "Mute": "Ljod av", 14 | "Unmute": "Ljod på", 15 | "Playback Rate": "Avspelingsrate", 16 | "Subtitles": "Teksting på", 17 | "subtitles off": "Teksting av", 18 | "Captions": "Teksting for høyrselshemma på", 19 | "captions off": "Teksting for høyrselshemma av", 20 | "Chapters": "Kapitel", 21 | "You aborted the media playback": "Du avbraut avspelinga.", 22 | "A network error caused the media download to fail part-way.": "Ein nettverksfeil avbraut nedlasting av videoen.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikkje lastas ned, på grunn av ein nettverksfeil eller serverfeil, eller av di formatet ikkje er stoda.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspelinga blei broten på grunn av øydelagde data eller av di videoen ville gjera noe som nettlesaren din ikkje stodar.", 25 | "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/pl.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("pl",{ 2 | "Play": "Odtwarzaj", 3 | "Pause": "Pauza", 4 | "Current Time": "Aktualny czas", 5 | "Duration": "Czas trwania", 6 | "Remaining Time": "Pozostały czas", 7 | "Stream Type": "Typ strumienia", 8 | "LIVE": "NA ŻYWO", 9 | "Loaded": "Załadowany", 10 | "Progress": "Status", 11 | "Fullscreen": "Pełny ekran", 12 | "Non-Fullscreen": "Pełny ekran niedostępny", 13 | "Mute": "Wyłącz dźwięk", 14 | "Unmute": "Włącz dźwięk", 15 | "Playback Rate": "Szybkość odtwarzania", 16 | "Subtitles": "Napisy", 17 | "subtitles off": "Napisy wyłączone", 18 | "Captions": "Transkrypcja", 19 | "captions off": "Transkrypcja wyłączona", 20 | "Chapters": "Rozdziały", 21 | "You aborted the media playback": "Odtwarzanie zostało przerwane", 22 | "A network error caused the media download to fail part-way.": "Problemy z siecią spowodowały błąd przy pobieraniu materiału wideo.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Materiał wideo nie może być załadowany, ponieważ wystąpił problem z siecią lub format nie jest obsługiwany", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Odtwarzanie materiału wideo zostało przerwane z powodu uszkodzonego pliku wideo lub z powodu błędu funkcji, które nie są wspierane przez przeglądarkę.", 25 | "No compatible source was found for this media.": "Dla tego materiału wideo nie znaleziono kompatybilnego źródła.", 26 | "Play Video": "Odtwarzaj wideo", 27 | "Close": "Zamknij", 28 | "Modal Window": "Okno Modala", 29 | "This is a modal window": "To jest okno modala", 30 | "This modal can be closed by pressing the Escape key or activating the close button.": "Ten modal możesz zamknąć naciskając przycisk Escape albo wybierając przycisk Zamknij.", 31 | ", opens captions settings dialog": ", otwiera okno dialogowe ustawień transkrypcji", 32 | ", opens subtitles settings dialog": ", otwiera okno dialogowe napisów", 33 | ", selected": ", zaznaczone" 34 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/pt-BR.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("pt-BR",{ 2 | "Audio Player": "Reprodutor de áudio", 3 | "Video Player": "Reprodutor de vídeo", 4 | "Play": "Tocar", 5 | "Pause": "Pausar", 6 | "Replay": "Tocar novamente", 7 | "Current Time": "Tempo", 8 | "Duration": "Duração", 9 | "Remaining Time": "Tempo Restante", 10 | "Stream Type": "Tipo de Stream", 11 | "LIVE": "AO VIVO", 12 | "Loaded": "Carregado", 13 | "Progress": "Progresso", 14 | "Progress Bar": "Barra de progresso", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} de {2}", 16 | "Fullscreen": "Tela Cheia", 17 | "Non-Fullscreen": "Tela Normal", 18 | "Mute": "Mudo", 19 | "Unmute": "Habilitar Som", 20 | "Playback Rate": "Velocidade", 21 | "Subtitles": "Legendas", 22 | "subtitles off": "Sem Legendas", 23 | "Captions": "Anotações", 24 | "captions off": "Sem Anotações", 25 | "Chapters": "Capítulos", 26 | "Descriptions": "Descrições", 27 | "descriptions off": "sem descrições", 28 | "Audio Track": "Faixa de áudio", 29 | "Volume Level": "Nível de volume", 30 | "You aborted the media playback": "Você parou a execução do vídeo.", 31 | "A network error caused the media download to fail part-way.": "Um erro na rede causou falha durante o download da mídia.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "A mídia não pode ser carregada, por uma falha de rede ou servidor ou o formato não é suportado.", 33 | "No compatible source was found for this media.": "Nenhuma fonte foi encontrada para esta mídia.", 34 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reprodução foi interrompida devido à um problema de mídia corrompida ou porque a mídia utiliza funções que seu navegador não suporta.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "A mídia está criptografada e não temos as chaves para descriptografar.", 36 | "Play Video": "Tocar Vídeo", 37 | "Close": "Fechar", 38 | "Close Modal Dialog": "Fechar Diálogo Modal", 39 | "Modal Window": "Janela Modal", 40 | "This is a modal window": "Isso é uma janela-modal", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Esta janela pode ser fechada pressionando a tecla de Escape.", 42 | ", opens captions settings dialog": ", abre as configurações de legendas de comentários", 43 | ", opens subtitles settings dialog": ", abre as configurações de legendas", 44 | ", opens descriptions settings dialog": ", abre as configurações", 45 | ", selected": ", selecionada", 46 | "captions settings": "configurações de legendas de comentários", 47 | "subtitles settings": "configurações de legendas", 48 | "descriptions settings": "configurações das descrições", 49 | "Text": "Texto", 50 | "White": "Branco", 51 | "Black": "Preto", 52 | "Red": "Vermelho", 53 | "Green": "Verde", 54 | "Blue": "Azul", 55 | "Yellow": "Amarelo", 56 | "Magenta": "Magenta", 57 | "Cyan": "Ciano", 58 | "Background": "Plano-de-Fundo", 59 | "Window": "Janela", 60 | "Transparent": "Transparente", 61 | "Semi-Transparent": "Semi-Transparente", 62 | "Opaque": "Opaco", 63 | "Font Size": "Tamanho da Fonte", 64 | "Text Edge Style": "Estilo da Borda", 65 | "None": "Nenhum", 66 | "Raised": "Elevado", 67 | "Depressed": "Acachapado", 68 | "Uniform": "Uniforme", 69 | "Dropshadow": "Sombra de projeção", 70 | "Font Family": "Família da Fonte", 71 | "Proportional Sans-Serif": "Sans-Serif(Sem serifa) Proporcional", 72 | "Monospace Sans-Serif": "Sans-Serif(Sem serifa) Monoespaçada", 73 | "Proportional Serif": "Serifa Proporcional", 74 | "Monospace Serif": "Serifa Monoespaçada", 75 | "Casual": "Casual", 76 | "Script": "Script", 77 | "Small Caps": "Maiúsculas Pequenas", 78 | "Reset": "Redefinir", 79 | "restore all settings to the default values": "restaurar todas as configurações aos valores padrão", 80 | "Done": "Salvar", 81 | "Caption Settings Dialog": "Caíxa-de-Diálogo das configurações de Legendas", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Iniciando a Janela-de-Diálogo. Pressionar Escape irá cancelar e fechar a janela.", 83 | "End of dialog window.": "Fim da Janela-de-Diálogo", 84 | "{1} is loading.": "{1} está carregando." 85 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/pt-PT.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("pt-PT",{ 2 | "Play": "Reproduzir", 3 | "Pause": "Parar", 4 | "Replay": "Reiniciar", 5 | "Current Time": "Tempo Atual", 6 | "Duration": "Duração", 7 | "Remaining Time": "Tempo Restante", 8 | "Stream Type": "Tipo de Stream", 9 | "LIVE": "EM DIRETO", 10 | "Loaded": "Carregado", 11 | "Progress": "Progresso", 12 | "Fullscreen": "Ecrã inteiro", 13 | "Non-Fullscreen": "Ecrã normal", 14 | "Mute": "Desativar som", 15 | "Unmute": "Ativar som", 16 | "Playback Rate": "Velocidade de reprodução", 17 | "Subtitles": "Legendas", 18 | "subtitles off": "desativar legendas", 19 | "Captions": "Anotações", 20 | "captions off": "desativar anotações", 21 | "Chapters": "Capítulos", 22 | "Close Modal Dialog": "Fechar Janela Modal", 23 | "Descriptions": "Descrições", 24 | "descriptions off": "desativar descrições", 25 | "Audio Track": "Faixa Áudio", 26 | "You aborted the media playback": "Parou a reprodução do vídeo.", 27 | "A network error caused the media download to fail part-way.": "Um erro na rede fez o vídeo falhar parcialmente.", 28 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "O vídeo não pode ser carregado, ou porque houve um problema na rede ou no servidor, ou porque formato do vídeo não é compatível.", 29 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reprodução foi interrompida por um problema com o vídeo ou porque o formato não é compatível com o seu navegador.", 30 | "No compatible source was found for this media.": "Não foi encontrada uma fonte de vídeo compatível.", 31 | "The media is encrypted and we do not have the keys to decrypt it.": "O vídeo está encriptado e não há uma chave para o desencriptar.", 32 | "Play Video": "Reproduzir Vídeo", 33 | "Close": "Fechar", 34 | "Modal Window": "Janela Modal", 35 | "This is a modal window": "Isto é uma janela modal", 36 | "This modal can be closed by pressing the Escape key or activating the close button.": "Esta modal pode ser fechada pressionando a tecla ESC ou ativando o botão de fechar.", 37 | ", opens captions settings dialog": ", abre janela com definições de legendas", 38 | ", opens subtitles settings dialog": ", abre janela com definições de legendas", 39 | ", opens descriptions settings dialog": ", abre janela com definições de descrições", 40 | ", selected": ", seleccionado" 41 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/ru.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("ru",{ 2 | "Audio Player": "Аудио проигрыватель", 3 | "Video Player": "Видео проигрыватель", 4 | "Play": "Воспроизвести", 5 | "Pause": "Приостановить", 6 | "Replay": "Воспроизвести снова", 7 | "Current Time": "Текущее время", 8 | "Duration": "Продолжительность", 9 | "Remaining Time": "Оставшееся время", 10 | "Stream Type": "Тип потока", 11 | "LIVE": "ОНЛАЙН", 12 | "Loaded": "Загрузка", 13 | "Progress": "Прогресс", 14 | "Progress Bar": "Индикатор загрузки", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} из {2}", 16 | "Fullscreen": "Полноэкранный режим", 17 | "Non-Fullscreen": "Неполноэкранный режим", 18 | "Mute": "Без звука", 19 | "Unmute": "Со звуком", 20 | "Playback Rate": "Скорость воспроизведения", 21 | "Subtitles": "Субтитры", 22 | "subtitles off": "Субтитры выкл.", 23 | "Captions": "Подписи", 24 | "captions off": "Подписи выкл.", 25 | "Chapters": "Главы", 26 | "Descriptions": "Описания", 27 | "descriptions off": "Отключить описания", 28 | "Audio Track": "Звуковая дорожка", 29 | "Volume Level": "Уровень громкости", 30 | "You aborted the media playback": "Вы прервали воспроизведение видео", 31 | "A network error caused the media download to fail part-way.": "Ошибка сети вызвала сбой во время загрузки видео.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Невозможно загрузить видео из-за сетевого или серверного сбоя либо формат не поддерживается.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Воспроизведение видео было приостановлено из-за повреждения либо в связи с тем, что видео использует функции, неподдерживаемые вашим браузером.", 34 | "No compatible source was found for this media.": "Совместимые источники для этого видео отсутствуют.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "Видео в зашифрованном виде, и у нас нет ключей для расшифровки.", 36 | "Play Video": "Воспроизвести видео", 37 | "Close": "Закрыть", 38 | "Close Modal Dialog": "Закрыть модальное окно", 39 | "Modal Window": "Модальное окно", 40 | "This is a modal window": "Это модальное окно", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Модальное окно можно закрыть нажав Esc или кнопку закрытия окна.", 42 | ", opens captions settings dialog": ", откроется диалог настройки подписей", 43 | ", opens subtitles settings dialog": ", откроется диалог настройки субтитров", 44 | ", opens descriptions settings dialog": ", откроется диалог настройки описаний", 45 | ", selected": ", выбрано", 46 | "captions settings": "настройки подписей", 47 | "subtitles settings": "настройки субтитров", 48 | "descriptions settings": "настройки описаний", 49 | "Text": "Текст", 50 | "White": "Белый", 51 | "Black": "Черный", 52 | "Red": "Красный", 53 | "Green": "Зеленый", 54 | "Blue": "Синий", 55 | "Yellow": "Желтый", 56 | "Magenta": "Пурпурный", 57 | "Cyan": "Голубой", 58 | "Background": "Фон", 59 | "Window": "Окно", 60 | "Transparent": "Прозрачный", 61 | "Semi-Transparent": "Полупрозрачный", 62 | "Opaque": "Прозрачность", 63 | "Font Size": "Размер шрифта", 64 | "Text Edge Style": "Стиль края текста", 65 | "None": "Ничего", 66 | "Raised": "Поднятый", 67 | "Depressed": "Пониженный", 68 | "Uniform": "Одинаковый", 69 | "Dropshadow": "Тень", 70 | "Font Family": "Шрифт", 71 | "Proportional Sans-Serif": "Пропорциональный без засечек", 72 | "Monospace Sans-Serif": "Моноширинный без засечек", 73 | "Proportional Serif": "Пропорциональный с засечками", 74 | "Monospace Serif": "Моноширинный с засечками", 75 | "Casual": "Случайный", 76 | "Script": "Письменный", 77 | "Small Caps": "Малые прописные", 78 | "Reset": "Сбросить", 79 | "restore all settings to the default values": "сбросить все найстройки по умолчанию", 80 | "Done": "Готово", 81 | "Caption Settings Dialog": "Диалог настроек подписи", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Начало диалоговго окна. Кнопка Escape закроет или отменит окно", 83 | "End of dialog window.": "Конец диалогового окна." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/sk.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("sk",{ 2 | "Audio Player": "Zvukový prehrávač", 3 | "Video Player": "Video prehrávač", 4 | "Play": "Prehrať", 5 | "Pause": "Pozastaviť", 6 | "Replay": "Prehrať znova", 7 | "Current Time": "Aktuálny čas", 8 | "Duration": "Čas trvania", 9 | "Remaining Time": "Zostávajúci čas", 10 | "Stream Type": "Typ stopy", 11 | "LIVE": "NAŽIVO", 12 | "Loaded": "Načítané", 13 | "Progress": "Priebeh", 14 | "Progress Bar": "Ukazovateľ priebehu", 15 | "progress bar timing: currentTime={1} duration={2}": "časovanie ukazovateľa priebehu: currentTime={1} duration={2}", 16 | "Fullscreen": "Režim celej obrazovky", 17 | "Non-Fullscreen": "Režim normálnej obrazovky", 18 | "Mute": "Stlmiť", 19 | "Unmute": "Zrušiť stlmenie", 20 | "Playback Rate": "Rýchlosť prehrávania", 21 | "Subtitles": "Titulky", 22 | "subtitles off": "titulky vypnuté", 23 | "Captions": "Popisky", 24 | "captions off": "popisky vypnuté", 25 | "Chapters": "Kapitoly", 26 | "Descriptions": "Opisy", 27 | "descriptions off": "opisy vypnuté", 28 | "Audio Track": "Zvuková stopa", 29 | "Volume Level": "Úroveň hlasitosti", 30 | "You aborted the media playback": "Prerušili ste prehrávanie", 31 | "A network error caused the media download to fail part-way.": "Sťahovanie súboru bolo zrušené pre chybu na sieti.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Súbor sa nepodarilo načítať pre chybu servera, sieťového pripojenia, alebo je formát súboru nepodporovaný.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Prehrávanie súboru bolo prerušené pre poškodené dáta, alebo súbor používa vlastnosti, ktoré váš prehliadač nepodporuje.", 34 | "No compatible source was found for this media.": "Nebol nájdený žiaden kompatibilný zdroj pre tento súbor.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "Súbor je zašifrovaný a nie je k dispozícii kľúč na rozšifrovanie.", 36 | "Play Video": "Prehrať video", 37 | "Close": "Zatvoriť", 38 | "Close Modal Dialog": "Zatvoriť modálne okno", 39 | "Modal Window": "Modálne okno", 40 | "This is a modal window": "Toto je modálne okno", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Toto modálne okno je možné zatvoriť stlačením klávesy Escape, alebo aktivovaním tlačidla na zatvorenie.", 42 | ", opens captions settings dialog": ", otvorí okno nastavení popiskov", 43 | ", opens subtitles settings dialog": ", otvorí okno nastavení titulkov", 44 | ", opens descriptions settings dialog": ", otvorí okno nastavení opisov", 45 | ", selected": ", označené", 46 | "captions settings": "nastavenia popiskov", 47 | "subtitles settings": "nastavenia titulkov", 48 | "descriptions settings": "nastavenia opisov", 49 | "Text": "Text", 50 | "White": "Biela", 51 | "Black": "Čierna", 52 | "Red": "Červená", 53 | "Green": "Zelená", 54 | "Blue": "Modrá", 55 | "Yellow": "Žltá", 56 | "Magenta": "Ružová", 57 | "Cyan": "Tyrkysová", 58 | "Background": "Pozadie", 59 | "Window": "Okno", 60 | "Transparent": "Priesvitné", 61 | "Semi-Transparent": "Polopriesvitné", 62 | "Opaque": "Plné", 63 | "Font Size": "Veľkosť písma", 64 | "Text Edge Style": "Typ okrajov písma", 65 | "None": "Žiadne", 66 | "Raised": "Zvýšené", 67 | "Depressed": "Znížené", 68 | "Uniform": "Pravidelné", 69 | "Dropshadow": "S tieňom", 70 | "Font Family": "Typ písma", 71 | "Proportional Sans-Serif": "Proporčné bezpätkové", 72 | "Monospace Sans-Serif": "Pravidelné, bezpätkové", 73 | "Proportional Serif": "Proporčné pätkové", 74 | "Monospace Serif": "Pravidelné pätkové", 75 | "Casual": "Bežné", 76 | "Script": "Písané", 77 | "Small Caps": "Malé kapitálky", 78 | "Reset": "Resetovať", 79 | "restore all settings to the default values": "všetky nastavenia na základné hodnoty", 80 | "Done": "Hotovo", 81 | "Caption Settings Dialog": "Okno nastavení popiskov", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Začiatok okna. Klávesa Escape zruší a zavrie okno.", 83 | "End of dialog window.": "Koniec okna." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/sr.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("sr",{ 2 | "Play": "Pusti", 3 | "Pause": "Pauza", 4 | "Current Time": "Trenutno vrijeme", 5 | "Duration": "Vrijeme trajanja", 6 | "Remaining Time": "Preostalo vrijeme", 7 | "Stream Type": "Način strimovanja", 8 | "LIVE": "UŽIVO", 9 | "Loaded": "Učitan", 10 | "Progress": "Progres", 11 | "Fullscreen": "Puni ekran", 12 | "Non-Fullscreen": "Mali ekran", 13 | "Mute": "Prigušen", 14 | "Unmute": "Ne-prigušen", 15 | "Playback Rate": "Stopa reprodukcije", 16 | "Subtitles": "Podnaslov", 17 | "subtitles off": "Podnaslov deaktiviran", 18 | "Captions": "Titlovi", 19 | "captions off": "Titlovi deaktivirani", 20 | "Chapters": "Poglavlja", 21 | "You aborted the media playback": "Isključili ste reprodukciju videa.", 22 | "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.", 25 | "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/sv.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("sv",{ 2 | "Play": "Spela", 3 | "Pause": "Pausa", 4 | "Current Time": "Aktuell tid", 5 | "Duration": "Total tid", 6 | "Remaining Time": "Återstående tid", 7 | "Stream Type": "Strömningstyp", 8 | "LIVE": "LIVE", 9 | "Loaded": "Laddad", 10 | "Progress": "Förlopp", 11 | "Fullscreen": "Fullskärm", 12 | "Non-Fullscreen": "Ej fullskärm", 13 | "Mute": "Ljud av", 14 | "Unmute": "Ljud på", 15 | "Playback Rate": "Uppspelningshastighet", 16 | "Subtitles": "Text på", 17 | "subtitles off": "Text av", 18 | "Captions": "Text på", 19 | "captions off": "Text av", 20 | "Chapters": "Kapitel", 21 | "You aborted the media playback": "Du har avbrutit videouppspelningen.", 22 | "A network error caused the media download to fail part-way.": "Ett nätverksfel gjorde att nedladdningen av videon avbröts.", 23 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Det gick inte att ladda videon, antingen på grund av ett server- eller nätverksfel, eller för att formatet inte stöds.", 24 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Uppspelningen avbröts på grund av att videon är skadad, eller också för att videon använder funktioner som din webbläsare inte stöder.", 25 | "No compatible source was found for this media.": "Det gick inte att hitta någon kompatibel källa för den här videon." 26 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/tr.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("tr",{ 2 | "Play": "Oynat", 3 | "Pause": "Duraklat", 4 | "Replay": "Yeniden Oynat", 5 | "Current Time": "Süre", 6 | "Duration": "Toplam Süre", 7 | "Remaining Time": "Kalan Süre", 8 | "Stream Type": "Yayın Tipi", 9 | "LIVE": "CANLI", 10 | "Loaded": "Yüklendi", 11 | "Progress": "Yükleniyor", 12 | "Fullscreen": "Tam Ekran", 13 | "Non-Fullscreen": "Küçük Ekran", 14 | "Mute": "Ses Kapa", 15 | "Unmute": "Ses Aç", 16 | "Playback Rate": "Oynatma Hızı", 17 | "Subtitles": "Altyazı", 18 | "subtitles off": "Altyazı Kapalı", 19 | "Captions": "Altyazı", 20 | "captions off": "Altyazı Kapalı", 21 | "Chapters": "Bölümler", 22 | "Close Modal Dialog": "Dialogu Kapat", 23 | "Descriptions": "Açıklamalar", 24 | "descriptions off": "Açıklamalar kapalı", 25 | "Audio Track": "Ses Dosyası", 26 | "You aborted the media playback": "Video oynatmayı iptal ettiniz", 27 | "A network error caused the media download to fail part-way.": "Video indirilirken bağlantı sorunu oluştu.", 28 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video oynatılamadı, ağ ya da sunucu hatası veya belirtilen format desteklenmiyor.", 29 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Tarayıcınız desteklemediği için videoda hata oluştu.", 30 | "No compatible source was found for this media.": "Video için kaynak bulunamadı.", 31 | "The media is encrypted and we do not have the keys to decrypt it.": "Video, şifrelenmiş bir kaynaktan geliyor ve oynatmak için gerekli anahtar bulunamadı.", 32 | "Play Video": "Videoyu Oynat", 33 | "Close": "Kapat", 34 | "Modal Window": "Modal Penceresi", 35 | "This is a modal window": "Bu bir modal penceresidir", 36 | "This modal can be closed by pressing the Escape key or activating the close button.": "Bu modal ESC tuşuna basarak ya da kapata tıklanarak kapatılabilir.", 37 | ", opens captions settings dialog": ", altyazı ayarları menüsünü açar", 38 | ", opens subtitles settings dialog": ", altyazı ayarları menüsünü açar", 39 | ", opens descriptions settings dialog": ", açıklama ayarları menüsünü açar", 40 | ", selected": ", seçildi", 41 | "captions settings": "altyazı ayarları", 42 | "subtitles settings": "altyazı ayarları", 43 | "descriptions settings": "açıklama ayarları", 44 | "Text": "Yazı", 45 | "White": "Beyaz", 46 | "Black": "Siyah", 47 | "Red": "Kırmızı", 48 | "Green": "Yeşil", 49 | "Blue": "Mavi", 50 | "Yellow": "Sarı", 51 | "Magenta": "Macenta", 52 | "Cyan": "Açık Mavi (Camgöbeği)", 53 | "Background": "Arka plan", 54 | "Window": "Pencere", 55 | "Transparent": "Saydam", 56 | "Semi-Transparent": "Yarı-Saydam", 57 | "Opaque": "Mat", 58 | "Font Size": "Yazı Boyutu", 59 | "Text Edge Style": "Yazı Kenarlıkları", 60 | "None": "Hiçbiri", 61 | "Raised": "Kabartılmış", 62 | "Depressed": "Yassı", 63 | "Uniform": "Düz", 64 | "Dropshadow": "Gölgeli", 65 | "Font Family": "Yazı Tipi", 66 | "Proportional Sans-Serif": "Orantılı Sans-Serif", 67 | "Monospace Sans-Serif": "Eşaralıklı Sans-Serif", 68 | "Proportional Serif": "Orantılı Serif", 69 | "Monospace Serif": "Eşaralıklı Serif", 70 | "Casual": "Gündelik", 71 | "Script": "El Yazısı", 72 | "Small Caps": "Küçük Boyutlu Büyük Harfli", 73 | "Done": "Tamam", 74 | "Caption Settings Dialog": "Altyazı Ayarları Menüsü", 75 | "Beginning of dialog window. Escape will cancel and close the window.": "Diyalog penceresinin başlangıcı. ESC tuşu işlemi iptal edip pencereyi kapatacaktır." 76 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/uk.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("uk",{ 2 | "Play": "Відтворити", 3 | "Pause": "Призупинити", 4 | "Current Time": "Поточний час", 5 | "Duration": "Тривалість", 6 | "Remaining Time": "Час, що залишився", 7 | "Stream Type": "Тип потоку", 8 | "LIVE": "НАЖИВО", 9 | "Loaded": "Завантаження", 10 | "Progress": "Прогрес", 11 | "Fullscreen": "Повноекранний режим", 12 | "Non-Fullscreen": "Неповноекранний режим", 13 | "Mute": "Без звуку", 14 | "Unmute": "Зі звуком", 15 | "Playback Rate": "Швидкість відтворення", 16 | "Subtitles": "Субтитри", 17 | "subtitles off": "Без субтитрів", 18 | "Captions": "Підписи", 19 | "captions off": "Без підписів", 20 | "Chapters": "Розділи", 21 | "Close Modal Dialog": "Закрити модальний діалог", 22 | "Descriptions": "Описи", 23 | "descriptions off": "Без описів", 24 | "Audio Track": "Аудіодоріжка", 25 | "You aborted the media playback": "Ви припинили відтворення відео", 26 | "A network error caused the media download to fail part-way.": "Помилка мережі викликала збій під час завантаження відео.", 27 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Неможливо завантажити відео через мережевий чи серверний збій або формат не підтримується.", 28 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Відтворення відео було припинено через пошкодження або у зв'язку з тим, що відео використовує функції, які не підтримуються вашим браузером.", 29 | "No compatible source was found for this media.": "Сумісні джерела для цього відео відсутні.", 30 | "The media is encrypted and we do not have the keys to decrypt it.": "Відео в зашифрованому вигляді, і ми не маємо ключі для розшифровки.", 31 | "Play Video": "Відтворити відео", 32 | "Close": "Закрити", 33 | "Modal Window": "Модальне вікно", 34 | "This is a modal window": "Це модальне вікно.", 35 | "This modal can be closed by pressing the Escape key or activating the close button.": "Модальне вікно можна закрити, натиснувши клавішу Esc або кнопку закриття вікна.", 36 | ", opens captions settings dialog": ", відкриється діалогове вікно налаштування підписів", 37 | ", opens subtitles settings dialog": ", відкриється діалогове вікно налаштування субтитрів", 38 | ", opens descriptions settings dialog": ", відкриється діалогове вікно налаштування описів", 39 | ", selected": ", обраний" 40 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/vi.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("vi",{ 2 | "Audio Player": "Trình phát Audio", 3 | "Video Player": "Trình phát Video", 4 | "Play": "Phát", 5 | "Pause": "Tạm dừng", 6 | "Replay": "Phát lại", 7 | "Current Time": "Thời gian hiện tại", 8 | "Duration": "Độ dài", 9 | "Remaining Time": "Thời gian còn lại", 10 | "Stream Type": "Kiểu Stream", 11 | "LIVE": "TRỰC TIẾP", 12 | "Loaded": "Đã tải", 13 | "Progress": "Tiến trình", 14 | "Progress Bar": "Thanh tiến trình", 15 | "progress bar timing: currentTime={1} duration={2}": "{1} của {2}", 16 | "Fullscreen": "Toàn màn hình", 17 | "Non-Fullscreen": "Thoát toàn màn hình", 18 | "Mute": "Tắt tiếng", 19 | "Unmute": "Bật âm thanh", 20 | "Playback Rate": "Tỉ lệ phát lại", 21 | "Subtitles": "Phụ đề", 22 | "subtitles off": "tắt phụ đề", 23 | "Captions": "Chú thích", 24 | "captions off": "tắt chú thích", 25 | "Chapters": "Chương", 26 | "Descriptions": "Mô tả", 27 | "descriptions off": "tắt mô tả", 28 | "Audio Track": "Track âm thanh", 29 | "Volume Level": "Mức âm lượng", 30 | "You aborted the media playback": "Bạn đã hủy việc phát lại media.", 31 | "A network error caused the media download to fail part-way.": "Một lỗi mạng dẫn đến việc tải media bị lỗi.", 32 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video không tải được, mạng hay server có lỗi hoặc định dạng không được hỗ trợ.", 33 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Phát media đã bị hủy do một sai lỗi hoặc media sử dụng những tính năng trình duyệt không hỗ trợ.", 34 | "No compatible source was found for this media.": "Không có nguồn tương thích cho media này.", 35 | "The media is encrypted and we do not have the keys to decrypt it.": "Media đã được mã hóa và chúng tôi không có để giải mã nó.", 36 | "Play Video": "Phát Video", 37 | "Close": "Đóng", 38 | "Close Modal Dialog": "Đóng cửa sổ", 39 | "Modal Window": "Cửa sổ", 40 | "This is a modal window": "Đây là một cửa sổ", 41 | "This modal can be closed by pressing the Escape key or activating the close button.": "Cửa sổ này có thể thoát bằng việc nhấn phím Esc hoặc kích hoạt nút đóng.", 42 | ", opens captions settings dialog": ", mở hộp thoại cài đặt chú thích", 43 | ", opens subtitles settings dialog": ", mở hộp thoại cài đặt phụ đề", 44 | ", opens descriptions settings dialog": ", mở hộp thoại cài đặt mô tả", 45 | ", selected": ", đã chọn", 46 | "captions settings": "cài đặt chú thích", 47 | "subtitles settings": "cài đặt phụ đề", 48 | "descriptions settings": "cài đặt mô tả", 49 | "Text": "Văn bản", 50 | "White": "Trắng", 51 | "Black": "Đen", 52 | "Red": "Đỏ", 53 | "Green": "Xanh lá cây", 54 | "Blue": "Xanh da trời", 55 | "Yellow": "Vàng", 56 | "Magenta": "Đỏ tươi", 57 | "Cyan": "Lam", 58 | "Background": "Nền", 59 | "Window": "Cửa sổ", 60 | "Transparent": "Trong suốt", 61 | "Semi-Transparent": "Bán trong suốt", 62 | "Opaque": "Mờ", 63 | "Font Size": "Kích cỡ phông chữ", 64 | "Text Edge Style": "Dạng viền văn bản", 65 | "None": "None", 66 | "Raised": "Raised", 67 | "Depressed": "Depressed", 68 | "Uniform": "Uniform", 69 | "Dropshadow": "Dropshadow", 70 | "Font Family": "Phông chữ", 71 | "Proportional Sans-Serif": "Proportional Sans-Serif", 72 | "Monospace Sans-Serif": "Monospace Sans-Serif", 73 | "Proportional Serif": "Proportional Serif", 74 | "Monospace Serif": "Monospace Serif", 75 | "Casual": "Casual", 76 | "Script": "Script", 77 | "Small Caps": "Small Caps", 78 | "Reset": "Đặt lại", 79 | "restore all settings to the default values": "khôi phục lại tất cả các cài đặt về giá trị mặc định", 80 | "Done": "Xong", 81 | "Caption Settings Dialog": "Hộp thoại cài đặt chú thích", 82 | "Beginning of dialog window. Escape will cancel and close the window.": "Bắt đầu cửa sổ hộp thoại. Esc sẽ thoát và đóng cửa sổ.", 83 | "End of dialog window.": "Kết thúc cửa sổ hộp thoại." 84 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("zh-CN",{ 2 | "Play": "播放", 3 | "Pause": "暂停", 4 | "Current Time": "当前时间", 5 | "Duration": "时长", 6 | "Remaining Time": "剩余时间", 7 | "Stream Type": "媒体流类型", 8 | "LIVE": "直播", 9 | "Loaded": "加载完毕", 10 | "Progress": "进度", 11 | "Fullscreen": "全屏", 12 | "Non-Fullscreen": "退出全屏", 13 | "Mute": "静音", 14 | "Unmute": "取消静音", 15 | "Playback Rate": "播放速度", 16 | "Subtitles": "字幕", 17 | "subtitles off": "关闭字幕", 18 | "Captions": "内嵌字幕", 19 | "captions off": "关闭内嵌字幕", 20 | "Chapters": "节目段落", 21 | "Close Modal Dialog": "关闭弹窗", 22 | "Descriptions": "描述", 23 | "descriptions off": "关闭描述", 24 | "Audio Track": "音轨", 25 | "You aborted the media playback": "视频播放被终止", 26 | "A network error caused the media download to fail part-way.": "网络错误导致视频下载中途失败。", 27 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "视频因格式不支持或者服务器或网络的问题无法加载。", 28 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "由于视频文件损坏或是该视频使用了你的浏览器不支持的功能,播放终止。", 29 | "No compatible source was found for this media.": "无法找到此视频兼容的源。", 30 | "The media is encrypted and we do not have the keys to decrypt it.": "视频已加密,无法解密。", 31 | "Play Video": "播放视频", 32 | "Close": "关闭", 33 | "Modal Window": "弹窗", 34 | "This is a modal window": "这是一个弹窗", 35 | "This modal can be closed by pressing the Escape key or activating the close button.": "可以按ESC按键或启用关闭按钮来关闭此弹窗。", 36 | ", opens captions settings dialog": ", 开启标题设置弹窗", 37 | ", opens subtitles settings dialog": ", 开启字幕设置弹窗", 38 | ", opens descriptions settings dialog": ", 开启描述设置弹窗", 39 | ", selected": ", 选择", 40 | "captions settings": "字幕设定", 41 | "Audio Player": "音频播放器", 42 | "Video Player": "视频播放器", 43 | "Replay": "重播", 44 | "Progress Bar": "进度小节", 45 | "Volume Level": "音量", 46 | "subtitles settings": "字幕设定", 47 | "descriptions settings": "描述设定", 48 | "Text": "文字", 49 | "White": "白", 50 | "Black": "黑", 51 | "Red": "红", 52 | "Green": "绿", 53 | "Blue": "蓝", 54 | "Yellow": "黄", 55 | "Magenta": "紫红", 56 | "Cyan": "青", 57 | "Background": "背景", 58 | "Window": "视窗", 59 | "Transparent": "透明", 60 | "Semi-Transparent": "半透明", 61 | "Opaque": "不透明", 62 | "Font Size": "字体尺寸", 63 | "Text Edge Style": "字体边缘样式", 64 | "None": "无", 65 | "Raised": "浮雕", 66 | "Depressed": "压低", 67 | "Uniform": "均匀", 68 | "Dropshadow": "下阴影", 69 | "Font Family": "字体库", 70 | "Proportional Sans-Serif": "比例无细体", 71 | "Monospace Sans-Serif": "单间隔无细体", 72 | "Proportional Serif": "比例细体", 73 | "Monospace Serif": "单间隔细体", 74 | "Casual": "舒适", 75 | "Script": "手写体", 76 | "Small Caps": "小型大写字体", 77 | "Reset": "重启", 78 | "restore all settings to the default values": "恢复全部设定至预设值", 79 | "Done": "完成", 80 | "Caption Settings Dialog": "字幕设定视窗", 81 | "Beginning of dialog window. Escape will cancel and close the window.": "开始对话视窗。离开会取消及关闭视窗", 82 | "End of dialog window.": "结束对话视窗" 83 | }); -------------------------------------------------------------------------------- /src/sbot/video.js.bak/lang/zh-TW.js: -------------------------------------------------------------------------------- 1 | videojs.addLanguage("zh-TW",{ 2 | "Play": "播放", 3 | "Pause": "暫停", 4 | "Current Time": "目前時間", 5 | "Duration": "總共時間", 6 | "Remaining Time": "剩餘時間", 7 | "Stream Type": "串流類型", 8 | "LIVE": "直播", 9 | "Loaded": "載入完畢", 10 | "Progress": "進度", 11 | "Fullscreen": "全螢幕", 12 | "Non-Fullscreen": "退出全螢幕", 13 | "Mute": "靜音", 14 | "Unmute": "取消靜音", 15 | "Playback Rate": " 播放速率", 16 | "Subtitles": "字幕", 17 | "subtitles off": "關閉字幕", 18 | "Captions": "內嵌字幕", 19 | "captions off": "關閉內嵌字幕", 20 | "Chapters": "章節", 21 | "Close Modal Dialog": "關閉彈窗", 22 | "Descriptions": "描述", 23 | "descriptions off": "關閉描述", 24 | "Audio Track": "音軌", 25 | "You aborted the media playback": "影片播放已終止", 26 | "A network error caused the media download to fail part-way.": "網路錯誤導致影片下載失敗。", 27 | "The media could not be loaded, either because the server or network failed or because the format is not supported.": "影片因格式不支援或者伺服器或網路的問題無法載入。", 28 | "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "由於影片檔案損毀或是該影片使用了您的瀏覽器不支援的功能,播放終止。", 29 | "No compatible source was found for this media.": "無法找到相容此影片的來源。", 30 | "The media is encrypted and we do not have the keys to decrypt it.": "影片已加密,無法解密。", 31 | "Play Video": "播放影片", 32 | "Close": "關閉", 33 | "Modal Window": "對話框", 34 | "This is a modal window": "這是一個對話框", 35 | "This modal can be closed by pressing the Escape key or activating the close button.": "可以按ESC按鍵或啟用關閉按鈕來關閉此對話框。", 36 | ", opens captions settings dialog": ", 開啟標題設定對話框", 37 | ", opens subtitles settings dialog": ", 開啟字幕設定對話框", 38 | ", opens descriptions settings dialog": ", 開啟描述設定對話框", 39 | ", selected": ", 選擇", 40 | "captions settings": "字幕設定", 41 | "Audio Player": "音頻播放器", 42 | "Video Player": "視頻播放器", 43 | "Replay": "重播", 44 | "Progress Bar": "進度小節", 45 | "Volume Level": "音量", 46 | "subtitles settings": "字幕設定", 47 | "descriptions settings": "描述設定", 48 | "Text": "文字", 49 | "White": "白", 50 | "Black": "黑", 51 | "Red": "紅", 52 | "Green": "綠", 53 | "Blue": "藍", 54 | "Yellow": "黃", 55 | "Magenta": "紫紅", 56 | "Cyan": "青", 57 | "Background": "背景", 58 | "Window": "視窗", 59 | "Transparent": "透明", 60 | "Semi-Transparent": "半透明", 61 | "Opaque": "不透明", 62 | "Font Size": "字型尺寸", 63 | "Text Edge Style": "字型邊緣樣式", 64 | "None": "無", 65 | "Raised": "浮雕", 66 | "Depressed": "壓低", 67 | "Uniform": "均勻", 68 | "Dropshadow": "下陰影", 69 | "Font Family": "字型庫", 70 | "Proportional Sans-Serif": "比例無細體", 71 | "Monospace Sans-Serif": "單間隔無細體", 72 | "Proportional Serif": "比例細體", 73 | "Monospace Serif": "單間隔細體", 74 | "Casual": "輕便的", 75 | "Script": "手寫體", 76 | "Small Caps": "小型大寫字體", 77 | "Reset": "重置", 78 | "restore all settings to the default values": "恢復全部設定至預設值", 79 | "Done": "完成", 80 | "Caption Settings Dialog": "字幕設定視窗", 81 | "Beginning of dialog window. Escape will cancel and close the window.": "開始對話視窗。離開會取消及關閉視窗", 82 | "End of dialog window.": "結束對話視窗" 83 | }); -------------------------------------------------------------------------------- /src/sbot/view.php: -------------------------------------------------------------------------------- 1 | Post($p); 6 | header("location: view.php"); 7 | } 8 | ?> 9 | 10 | 11 | 12 | 13 | 14 | 15 | Untitled Document 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 36 |
37 | 38 | 71 | 82 |
83 |
84 | 85 | 86 | 87 |
88 | 89 |
90 |
91 |
92 |

Peers


93 | getPeers()['local']; 95 | foreach ($p as $peer) 96 | echo substr($peer['name'],0,20) . "
"; 97 | 98 | ?> 99 |
100 | id; 105 | $v= shell_exec("/usr/local/bin/sbot feed --reverse 2>&1"); 106 | $v="[" . str_replace("}\n\n{","}\n,\n{",$v) . "]"; 107 | $v=json_decode($v,true); 108 | 109 | foreach ($v as $post) { 110 | echo ""; 113 | ?> 114 |
115 |
Author: 116 | getName($post['value']['author'])?> 117 |
118 | 119 | toDate($post['value']['timestamp'])?> 120 |
121 |
122 |
123 | render($post['value']['content']['text']); 133 | break; 134 | default: 135 | print_r($post); 136 | } 137 | ?> 138 |
139 |
140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/sbot/view2.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | Untitled Document 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 34 |
35 | 36 | 46 |
Post
47 | &1"); 49 | $v="[" . $v . " {}]"; 50 | $v=json_decode($v,true); 51 | array_pop($v); 52 | foreach ($v as $post) { 53 | echo ""; 56 | ?> 57 |
58 | Author:
59 |
60 |
61 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | --------------------------------------------------------------------------------