├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── README_warp.md ├── cert ├── .gitignore ├── fingerprint └── generate ├── media ├── .gitignore └── generate ├── player ├── .env.production ├── .gitignore ├── package-lock.json ├── package.json ├── src │ ├── appsettings.js │ ├── assets │ │ └── github-mark-white.svg │ ├── db.ts │ ├── estimator.ts │ ├── index.html │ ├── index.ts │ ├── init.ts │ ├── message.ts │ ├── mp4.ts │ ├── mp4box.all.js │ ├── player.css │ ├── player.ts │ ├── segment.ts │ ├── source.ts │ ├── stream.ts │ ├── track.ts │ ├── types │ │ ├── global.d.ts │ │ └── webtransport.d.ts │ └── util.ts ├── tsconfig.json └── yarn.lock ├── server ├── .gitignore ├── bin │ └── speedtest.sh ├── go.mod ├── go.sum ├── internal │ └── warp │ │ ├── media.go │ │ ├── message.go │ │ ├── server.go │ │ ├── session.go │ │ └── stream.go ├── main.go ├── start.sh └── tc_scripts │ ├── profile_cascade │ ├── profile_fast_jitters │ ├── profile_intra_cascade │ ├── profile_lte │ ├── profile_slow_jitters │ ├── profile_spike │ ├── profile_twitch │ ├── tc_reset.sh │ ├── test_profile │ ├── test_profile copy │ └── throttle.sh ├── side-load └── 1MB-chunk.m4s └── tc_profiles ├── FCCamazone ├── FCCamazone_x0.25 ├── NYUbus ├── NYUbus_x0.25 ├── Synthtic ├── Synthtic_x0.25 ├── bandwidth_scale.ods ├── cascade_profile ├── cascade_profile_x0.05 ├── cascade_profile_x0.25 ├── cascade_profile_x1.25 ├── cascade_profile_x3 ├── cascade_profile_x4 ├── cascade_profile_x4.5 ├── lte_profile ├── lte_profile_x0.25 ├── lte_profile_x3 ├── lte_profile_x4 ├── tc_clear.sh ├── tc_limit.sh ├── tc_netem.sh ├── tc_policy.sh ├── tc_start.sh ├── twitch_profile ├── twitch_profile_x0.25 ├── twitch_profile_x3 └── twitch_profile_x4 /.gitignore: -------------------------------------------------------------------------------- 1 | *.mp4 2 | logs/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com Inc. or its affiliates. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARP as a MOQ Protocol 2 | 3 | Warp is a live media transport protocol over QUIC. Media is split into objects based on the underlying media encoding and transmitted independently over QUIC streams. QUIC streams are prioritized based on the delivery order, allowing less important objects to be starved or dropped during congestion. 4 | 5 | See the [Warp draft](https://datatracker.ietf.org/doc/draft-lcurley-warp/). 6 | 7 | # MOQ Testbed 8 | 9 | This demo has been forked off the original [WARP demo application code](https://github.com/kixelated/warp). 10 | 11 | You can find a working demo [here](https://moq.streaming.university). 12 | 13 | There are numerous additions as follows: 14 | 15 | - Server-to-client informational messages: Added Common Media Server Data (CMSD, CTA-5006) 16 | keys: Availability Time (at) and Estimated Throughput (ETP). 17 | - Client-to-server control messages 18 | - Passive bandwidth measurements: Sliding Window Moving Average with Threshold (SWMAth) and I-Frame Average (IFA). 19 | - Active bandwidth measurements 20 | - Enhanced user interface 21 | 22 | More information and test results have been published in the following paper: 23 | 24 | Z. Gurel, T. E. Civelek, A. Bodur, S. Bilgin, D. Yeniceri, and A. C. Begen. Media 25 | over QUIC: Initial testing, findings and results. In ACM MMSys, 2023. -------------------------------------------------------------------------------- /README_warp.md: -------------------------------------------------------------------------------- 1 | # Warp 2 | Segmented live media delivery protocol utilizing QUIC streams. See the [Warp draft](https://datatracker.ietf.org/doc/draft-lcurley-warp/). 3 | 4 | Warp works by delivering each audio and video segment as a separate QUIC stream. These streams are assigned a priority such that old video will arrive last and can be dropped. This avoids buffering in many cases, offering the viewer a potentially better experience. 5 | 6 | # Limitations 7 | ## Browser Support 8 | This demo currently only works on Chrome for two reasons: 9 | 10 | 1. WebTransport support. 11 | 2. [Media underflow behavior](https://github.com/whatwg/html/issues/6359). 12 | 13 | The ability to skip video abuses the fact that Chrome can play audio without video for up to 3 seconds (hardcoded!) when using MSE. It is possible to use something like WebCodecs instead... but that's still Chrome only at the moment. 14 | 15 | ## Streaming 16 | This demo works by reading pre-encoded media and sleeping based on media timestamps. Obviously this is not a live stream; you should plug in your own encoder or source. 17 | 18 | The media is encoded on disk as a LL-DASH playlist. There's a crude parser and I haven't used DASH before so don't expect it to work with arbitrary inputs. 19 | 20 | ## QUIC Implementation 21 | This demo uses a fork of [quic-go](https://github.com/lucas-clemente/quic-go). There are two critical features missing upstream: 22 | 23 | 1. ~~[WebTransport](https://github.com/lucas-clemente/quic-go/issues/3191)~~ 24 | 2. [Prioritization](https://github.com/lucas-clemente/quic-go/pull/3442) 25 | 26 | ## Congestion Control 27 | This demo uses a single rendition. A production implementation will want to: 28 | 29 | 1. Change the rendition bitrate to match the estimated bitrate. 30 | 2. Switch renditions at segment boundaries based on the estimated bitrate. 31 | 3. or both! 32 | 33 | Also, quic-go ships with the default New Reno congestion control. Something like [BBRv2](https://github.com/lucas-clemente/quic-go/issues/341) will work much better for live video as it limits RTT growth. 34 | 35 | 36 | # Setup 37 | ## Requirements 38 | * Go 39 | * ffmpeg 40 | * openssl 41 | * Chrome Canary 42 | 43 | ## Media 44 | This demo simulates a live stream by reading a file from disk and sleeping based on media timestamps. Obviously you should hook this up to a real live stream to do anything useful. 45 | 46 | Download your favorite media file: 47 | ``` 48 | wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O media/source.mp4 49 | ``` 50 | 51 | Use ffmpeg to create a LL-DASH playlist. This creates a segment every 2s and MP4 fragment every 10ms. 52 | ``` 53 | ffmpeg -i media/source.mp4 -f dash -use_timeline 0 -r:v 24 -g:v 48 -keyint_min:v 48 -sc_threshold:v 0 -tune zerolatency -streaming 1 -ldash 1 -seg_duration 2 -frag_duration 0.01 -frag_type duration media/playlist.mpd 54 | ``` 55 | 56 | You can increase the `frag_duration` (microseconds) to slightly reduce the file size in exchange for higher latency. 57 | 58 | ## TLS 59 | Unfortunately, QUIC mandates TLS and makes local development difficult. 60 | 61 | If you have a valid certificate you can use it instead of self-signing. The go binaries take a `-tls-cert` and `-tls-key` argument. Skip the remaining steps in this section and use your hostname instead. 62 | 63 | Otherwise, use [mkcert](https://github.com/FiloSottile/mkcert) to install a self-signed CA: 64 | ``` 65 | mkcert -install 66 | ``` 67 | 68 | With no arguments, the server will generate self-signed cert using this root CA. 69 | 70 | ## Server 71 | The Warp server supports WebTransport, pushing media over streams once a connection has been established. A more refined implementation would load content based on the WebTransport URL or some other messaging scheme. 72 | 73 | ``` 74 | cd server 75 | go run main.go 76 | ``` 77 | 78 | This can be accessed via WebTransport on `https://localhost:4443` by default. 79 | 80 | ## Web Player 81 | The web assets need to be hosted with a HTTPS server. If you're using a self-signed certificate, you may need to ignore the security warning in Chrome (Advanced -> proceed to localhost). 82 | 83 | ``` 84 | cd player 85 | yarn install 86 | yarn serve 87 | ``` 88 | 89 | These can be accessed on `https://localhost:4444` by default. 90 | 91 | If you use a custom domain for the Warp server, make sure to override the server URL with the `url` query string parameter, e.g. `https://localhost:4444/?url=https://warp.demo`. 92 | 93 | ## Chrome 94 | Now we need to make Chrome accept these certificates, which normally would involve trusting a root CA but this was not working with WebTransport when I last tried. 95 | 96 | Instead, we need to run a *fresh instance* of Chrome, instructing it to allow our self-signed certificate. This command will not work if Chrome is already running, so it's easier to use Chrome Canary instead. 97 | 98 | Launch a new instance of Chrome Canary: 99 | ``` 100 | /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --allow-insecure-localhost --origin-to-force-quic-on=localhost:4443 https://localhost:4444 101 | ``` 102 | -------------------------------------------------------------------------------- /cert/.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | -------------------------------------------------------------------------------- /cert/fingerprint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | HOST="localhost" 5 | 6 | cd "$(dirname "${BASH_SOURCE[0]}")" 7 | 8 | # Outputs the certificate fingerprint in the format Chrome expects 9 | openssl x509 -pubkey -noout -in "${HOST}.crt" | 10 | openssl rsa -pubin -outform der 2>/dev/null | 11 | openssl dgst -sha256 -binary | 12 | base64 13 | -------------------------------------------------------------------------------- /cert/generate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | cd "$(dirname "${BASH_SOURCE[0]}")" 5 | 6 | mkcert -cert-file localhost.crt -key-file localhost.key localhost 127.0.0.1 ::1 7 | -------------------------------------------------------------------------------- /media/.gitignore: -------------------------------------------------------------------------------- 1 | *.mp4 2 | *.mpd 3 | *.m4s 4 | -------------------------------------------------------------------------------- /media/generate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | input_file="source.mp4" 3 | segment_duration=2 4 | chunk_duration=0.04 5 | fps=25 6 | 7 | ffmpeg -i $input_file \ 8 | -f dash -ldash 1 \ 9 | -c:v libx264 \ 10 | -filter:v fps=$fps \ 11 | -preset veryfast -tune zerolatency \ 12 | -c:a aac \ 13 | -b:a 128k -ac 2 -ar 44100 \ 14 | -map v:0 -s:v:0 1920x1080 -b:v:0 4M \ 15 | -map v:0 -s:v:1 1080x720 -b:v:1 2.6M \ 16 | -map v:0 -s:v:2 960x540 -b:v:2 1.3M \ 17 | -map v:0 -s:v:3 640x360 -b:v:3 365k \ 18 | -map 0:a \ 19 | -force_key_frames "expr:gte(t,n_forced*2)" \ 20 | -sc_threshold 0 \ 21 | -streaming 1 \ 22 | -use_timeline 0 \ 23 | -seg_duration $segment_duration -frag_duration $chunk_duration \ 24 | -frag_type duration \ 25 | playlist.mpd 26 | -------------------------------------------------------------------------------- /player/.env.production: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=https://moq.streaming.university 2 | SERVER_URL=https://moq.streaming.university:8443 3 | 4 | HOST=moq.streaming.university 5 | PORT=443 -------------------------------------------------------------------------------- /player/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache 3 | dist 4 | .env.development -------------------------------------------------------------------------------- /player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "src/index.html", 3 | "scripts": { 4 | "serve": "parcel serve --https --cert ../cert/cert.pem --key ../cert/privkey.pem", 5 | "build": "parcel build src/index.html --no-optimize src/appsettings.js", 6 | "check": "tsc --noEmit" 7 | }, 8 | "devDependencies": { 9 | "@parcel/validator-typescript": "^2.6.0", 10 | "@types/node": "^18.11.18", 11 | "@types/plotly.js-dist": "npm:@types/plotly.js", 12 | "buffer": "^5.7.1", 13 | "parcel": "^2.8.2", 14 | "typescript": ">=3.0.0" 15 | }, 16 | "dependencies": { 17 | "plotly.js-dist": "^2.17.0" 18 | } 19 | } -------------------------------------------------------------------------------- /player/src/appsettings.js: -------------------------------------------------------------------------------- 1 | window.config = { 2 | serverURL: ":4443", 3 | resolutions: { 3: "360p", 2: "540p", 1: "720p", 0: "1080p" }, 4 | throttleData: { 5 | 209715200: "200Mb/s", 6 | 67108864: "64Mb/s", 7 | 16777216: "16Mb/s", 8 | 4194304: "4Mb/s", 9 | 2097152: "2Mb/s", 10 | 1048576: "1Mb/s", 11 | 524288: "512Kb/s", 12 | 262144: "256Kb/s", 13 | 131072: "128Kb/s", 14 | }, 15 | activeBWAsset: { 16 | url: "https://moq.streaming.university/side-load/1MB-chunk.m4s" 17 | }, 18 | activeBWTestInterval: 1000000, 19 | autoStart: false, 20 | testDuration: 100, 21 | swma_calculation_type: 'segment', 22 | swma_threshold: 5, 23 | swma_threshold_type: 'percentage', 24 | swma_window_size: 25, 25 | swma_calculation_interval: 5 26 | }; 27 | -------------------------------------------------------------------------------- /player/src/assets/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /player/src/db.ts: -------------------------------------------------------------------------------- 1 | const DB_VERSION = 8; 2 | let db: IDBDatabase; 3 | let status: 'none' | 'pending' | 'inited' = 'none'; 4 | 5 | const getStatus = () => { 6 | return status; 7 | }; 8 | 9 | const init = (): Promise => { 10 | return new Promise((resolve, reject) => { 11 | try { 12 | if (status !== 'none') { 13 | resolve(false); 14 | } 15 | status = 'pending'; 16 | 17 | console.log('opening db'); 18 | const DBOpenRequest = window.indexedDB.open('logs', DB_VERSION); 19 | 20 | console.log('opened db'); 21 | 22 | // Register two event handlers to act on the database being opened successfully, or not 23 | DBOpenRequest.onerror = (event) => { 24 | console.error('error opening database', event); 25 | reject('error opening database'); 26 | }; 27 | 28 | DBOpenRequest.onsuccess = (event) => { 29 | console.log('Database initialised.'); 30 | db = DBOpenRequest.result; 31 | status = 'inited'; 32 | resolve(true); 33 | }; 34 | 35 | DBOpenRequest.onupgradeneeded = (event: any) => { 36 | console.log('onupgradeneeded', event) 37 | 38 | db = event.target.result; 39 | 40 | db.onerror = (event) => { 41 | console.error('error loading database', event); 42 | reject('error loading database'); 43 | }; 44 | 45 | // Create an objectStore for this database 46 | try { 47 | let testStore, logStore, resultStore; 48 | if (db.objectStoreNames.contains('tests')) { 49 | db.deleteObjectStore('tests') 50 | } 51 | 52 | if (db.objectStoreNames.contains('test_logs')) { 53 | db.deleteObjectStore('test_logs') 54 | } 55 | 56 | if (db.objectStoreNames.contains('test_results')) { 57 | db.deleteObjectStore('test_results') 58 | } 59 | 60 | 61 | logStore = db.createObjectStore('test_logs', { keyPath: 'key', autoIncrement: true }); 62 | testStore = db.createObjectStore('tests', { keyPath: 'testId' }); 63 | resultStore = db.createObjectStore('test_results', { keyPath: 'key', autoIncrement: true }); 64 | 65 | testStore.createIndex('ix_testId', 'testId', { unique: false }); 66 | logStore.createIndex('ix_testId', 'testId', { unique: false }); 67 | logStore.createIndex('ix_chunk_no', 'no', { unique: false }); 68 | resultStore.createIndex('ix_testId', 'testId', { unique: false }); 69 | 70 | console.log('Object stores created.'); 71 | status = 'inited'; 72 | } catch (ex) { 73 | console.error('exception in onupgradeneeded', ex); 74 | reject(ex); 75 | } 76 | // a 1 sec delay to avoid the error "A version change transaction is running" 77 | setTimeout(() => resolve(true), 1000); 78 | }; 79 | } catch (ex) { 80 | console.error('Error in opening db', ex); 81 | reject(ex); 82 | } 83 | }); 84 | }; 85 | 86 | const addTestEntry = (test: any) => { 87 | addEntry('tests', test); 88 | }; 89 | 90 | const addLogEntry = (log: any) => { 91 | addEntry('test_logs', log); 92 | }; 93 | 94 | const addResultEntry = (result: any) => { 95 | addEntry('test_results', result); 96 | }; 97 | 98 | const addEntry = (storeName: string, entry: any) => { 99 | // console.log('addEntry to %s', storeName, entry); 100 | 101 | const transaction = db.transaction([storeName], 'readwrite'); 102 | transaction.oncomplete = () => { 103 | // console.log('added to %s', storeName); 104 | }; 105 | transaction.onerror = () => { 106 | console.error('add to %s | error: %s', storeName, transaction.error); 107 | }; 108 | const objectStore = transaction.objectStore(storeName); 109 | const objectStoreRequest = objectStore.add(entry); 110 | objectStoreRequest.onsuccess = (event) => { 111 | // console.log('request successful - %s', storeName) 112 | }; 113 | }; 114 | 115 | const getLogs = async (testId?: string): Promise => { 116 | if (testId) { 117 | return getEntriesByTestId('test_logs', testId); 118 | } else { 119 | return getEntries('test_logs'); 120 | } 121 | 122 | }; 123 | 124 | const getResults = async (testId: string): Promise => { 125 | return getEntriesByTestId('test_results', testId); 126 | }; 127 | 128 | const getTests = async (): Promise => { 129 | return getEntries('tests'); 130 | }; 131 | 132 | const getEntries = async (storeName: string): Promise => { 133 | console.log('in getEntries | %s', storeName); 134 | return new Promise((resolve, reject) => { 135 | const transaction = db.transaction([storeName], 'readonly'); 136 | transaction.oncomplete = () => { 137 | console.log('getEntries | transaction complete: %s', storeName); 138 | resolve(objectStoreRequest.result); 139 | }; 140 | transaction.onerror = () => { 141 | console.error('add to %s | error: %s', storeName, transaction.error); 142 | reject('transaction error ' + transaction.error); 143 | }; 144 | const objectStore = transaction.objectStore(storeName); 145 | const objectStoreRequest = objectStore.getAll(); 146 | objectStoreRequest.onsuccess = (event) => { 147 | console.log('request successful', event); 148 | }; 149 | }); 150 | }; 151 | 152 | const getEntriesByTestId = async (storeName: string, testId: string): Promise => { 153 | console.log('in getEntries | store: %s testId: %s', storeName, testId); 154 | return new Promise((resolve, reject) => { 155 | const keyRangeValue = IDBKeyRange.only(testId); 156 | const transaction = db.transaction([storeName], 'readonly'); 157 | transaction.oncomplete = () => { 158 | console.log('transaction complete', objectStoreRequest.result); 159 | resolve(objectStoreRequest.result); 160 | }; 161 | transaction.onerror = () => { 162 | console.error('add to %s | error: %s', storeName, transaction.error); 163 | reject('transaction error ' + transaction.error); 164 | }; 165 | const objectStore = transaction.objectStore(storeName); 166 | const storeIndex = objectStore.index("ix_testId"); 167 | const objectStoreRequest = storeIndex.getAll(keyRangeValue); 168 | objectStoreRequest.onsuccess = (event) => { 169 | console.log('request successful', event); 170 | }; 171 | }); 172 | }; 173 | 174 | const getDb = () => { 175 | return db; 176 | }; 177 | 178 | 179 | export const dbStore = { 180 | getStatus, 181 | addTestEntry, 182 | addLogEntry, 183 | addResultEntry, 184 | init, 185 | getLogs, 186 | getResults, 187 | getDb, 188 | getTests 189 | }; 190 | -------------------------------------------------------------------------------- /player/src/estimator.ts: -------------------------------------------------------------------------------- 1 | import { dbStore } from './db'; 2 | 3 | const chunkStats: any[] = []; 4 | let totalChunkCount = 0; 5 | let tputs: any[] = []; 6 | 7 | const getSWMAThreshold = () => { 8 | return window.config.swma_threshold || 5; 9 | } 10 | 11 | const getSWMACalculationType = () => { 12 | return window.config.swma_calculation_type; 13 | } 14 | 15 | const getSWMAThresholdType = () => { 16 | return window.config.swma_threshold_type || 'percentage' 17 | }; 18 | 19 | const getSWMACalculationInterval = () => { 20 | return window.config.swma_calculation_interval || 10; 21 | } 22 | 23 | const getSWMAWindowSize = () => { 24 | return window.config.swma_window_size || 50; 25 | } 26 | 27 | const initDb = async () => { 28 | try { 29 | console.log('initing db'); 30 | if (!await dbStore.init()) { 31 | console.log('db already inited'); 32 | } else { 33 | console.log('db inited'); 34 | } 35 | } catch (ex) { 36 | alert('db store could not be created'); 37 | console.error(ex); 38 | return; 39 | } 40 | } 41 | 42 | const getTests = async () => { 43 | await initDb(); 44 | const tests = await dbStore.getTests(); 45 | return tests; 46 | } 47 | const computeByTestId = async (testId: string) => { 48 | await initDb(); 49 | const logs = await dbStore.getLogs(testId); 50 | if (logs?.length > 0) { 51 | tputs.splice(0); 52 | let lastTPut = 0; 53 | logs.forEach((item, i) => { 54 | totalChunkCount++; 55 | chunkStats.push([item.chunkSize, item.chunkDownloadDuration]); 56 | if (totalChunkCount >= getSWMAWindowSize() && totalChunkCount % getSWMACalculationInterval() === 0) { 57 | let stats = chunkStats.slice(-getSWMAWindowSize()); 58 | let filteredStats = filterStats(stats, getSWMAThreshold(), getSWMAThresholdType(), lastTPut); 59 | // this.throughputs.set('swma', computeTPut(filteredStats)); 60 | const tput = computeTPut(filteredStats); 61 | 62 | filteredStats = filterStats(stats, getSWMAThreshold(), getSWMAThresholdType()); 63 | const tput2 = computeTPut(filteredStats); 64 | 65 | tputs.push([ 66 | tput, 67 | tput2, 68 | item.msg_tc_rate * 1000, 69 | !item.msg_tc_rate ? 0 : Math.round(Math.abs(tput - item.msg_tc_rate * 1000)), // abs diff 70 | !item.msg_tc_rate ? 0 : Math.round(Math.abs(tput - item.msg_tc_rate * 1000) / (item.msg_tc_rate * 1000) * 100), // abs diff percent 71 | !item.msg_tc_rate ? 0 : Math.round(Math.abs(tput2 - item.msg_tc_rate * 1000)), // abs diff 72 | !item.msg_tc_rate ? 0 : Math.round(Math.abs(tput2 - item.msg_tc_rate * 1000) / (item.msg_tc_rate * 1000) * 100) // abs diff percent 73 | ]); 74 | lastTPut = tput; 75 | console.log('%d %f %f', (i + 1), tput, tput2); 76 | } 77 | }); 78 | 79 | const totalAbsDiff = tputs.reduce((prev: number, current: any) => { 80 | return (prev || 0) + current[3]; 81 | }, 0); 82 | 83 | const totalAbsDiffPercentage = tputs.reduce((prev: number, current: any) => { 84 | return (prev || 0) + current[4]; // percentage diff 85 | }, 0); 86 | 87 | const totalAbsDiff_2 = tputs.reduce((prev: number, current: any) => { 88 | return (prev || 0) + current[5]; 89 | }, 0); 90 | 91 | const totalAbsDiffPercentage_2 = tputs.reduce((prev: number, current: any) => { 92 | return (prev || 0) + current[6]; // percentage diff 93 | }, 0); 94 | 95 | const meanAbsDiff = Math.round(totalAbsDiff / tputs.length); 96 | const meanAbsDiffPercentage = Math.round(totalAbsDiffPercentage / tputs.length); 97 | const meanAbsDiff_2 = Math.round(totalAbsDiff_2 / tputs.length); 98 | const meanAbsDiffPercentage_2 = Math.round(totalAbsDiffPercentage_2 / tputs.length); 99 | 100 | // console.log(tputs.map(x => x.join(',')).join('\n')); 101 | // console.log('totalAbsDiff: %f totalAbsDiffPercentage: %f meanAbsDiff: %f meanAbsDiffPercentage: %f total: %d', totalAbsDiff, totalAbsDiffPercentage, meanAbsDiff, meanAbsDiffPercentage, tputs.length); 102 | console.log('meanAbsDiff: %f meanAbsDiffPercentage: %f meanAbsDiff_2: %f meanAbsDiffPercentage_2: %f total: %d', meanAbsDiff, meanAbsDiffPercentage, meanAbsDiff_2, meanAbsDiffPercentage_2, tputs.length); 103 | } 104 | } 105 | 106 | const filterStats = (chunkStats: any[], threshold: number, thresholdType: string, lastTPut?: number) => { 107 | // filter out the ones with zero download duration 108 | let filteredStats = chunkStats.slice().filter(a => a[1] > 0); 109 | console.log('computeTPut | chunk count: %d thresholdType: %s threshold: %d', filteredStats.length, thresholdType, threshold); 110 | 111 | if (threshold > 0 && threshold < 100) { 112 | // sort chunk by download rate, in descending order 113 | filteredStats.sort((a, b) => { 114 | return (a[1] ? a[0] / a[1] : 0) > (b[1] ? b[0] / b[1] : 0) ? -1 : 1; 115 | }); 116 | 117 | const topCut = Math.ceil(threshold / 100 * filteredStats.length); 118 | const bottomCut = Math.floor(threshold / 100 * filteredStats.length); 119 | 120 | filteredStats.splice(0, topCut); 121 | filteredStats.splice(filteredStats.length - bottomCut, bottomCut); 122 | } 123 | 124 | if (lastTPut) { 125 | const magicalNumber = 5; 126 | filteredStats = filteredStats.filter(m => { 127 | const chunkTPut = m[0] * 8 * 1000 / m[1]; 128 | // if lastTPut is less than n time chunk tput or n times lastTPut is greater than chunk tput 129 | if (lastTPut < magicalNumber * chunkTPut && lastTPut * magicalNumber > chunkTPut) { 130 | return true; 131 | } else { 132 | console.log('filter %f %f', chunkTPut, lastTPut); 133 | return false; 134 | }; 135 | }); 136 | } 137 | 138 | console.log('computeTPut | after filtering: chunk count: %d', filteredStats.length); 139 | return filteredStats; 140 | } 141 | 142 | const computeTPut = (stats: any[]) => { 143 | let totalSize = 0; 144 | let totalDuration = 0; 145 | stats.forEach((arr, i) => { 146 | const size = arr[0]; 147 | const downloadDurationOfChunk = arr[1]; 148 | if (size > 0 && downloadDurationOfChunk > 0) { 149 | totalSize += size; 150 | totalDuration += downloadDurationOfChunk; 151 | } 152 | }); 153 | return totalSize * 8 * 1000 / totalDuration; 154 | }; 155 | 156 | export const estimator = { 157 | computeByTestId, 158 | getTests 159 | }; 160 | 161 | 162 | -------------------------------------------------------------------------------- /player/src/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | WARP 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
click to play
19 | 20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 31 | 32 | 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 | Show Logs 72 |
73 |

Log

74 |
75 |
76 |
77 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /player/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Player } from './player'; 2 | import * as Plotly from 'plotly.js-dist'; 3 | import { estimator } from './estimator'; 4 | 5 | // This is so ghetto but I'm too lazy to improve it right now 6 | const vidRef = document.getElementById("vid") as HTMLVideoElement; 7 | const startRef = document.getElementById("start") as HTMLButtonElement; 8 | const liveRef = document.getElementById("live") as HTMLButtonElement; 9 | const throttleRef = document.getElementById("throttle") as HTMLButtonElement; 10 | const throttleDDL = document.getElementById("throttles") as HTMLSelectElement; 11 | const statsRef = document.getElementById("stats") as HTMLDivElement; 12 | const playRef = document.getElementById("play") as HTMLDivElement; 13 | const resolutionsRef = document.getElementById("resolutions") as HTMLSelectElement; 14 | const activeBWTestRef = document.getElementById("active_bw_test") 15 | const continueStreamingRef = document.getElementById("continue_streaming") 16 | const logContentRef = document.querySelector("#log_content") as HTMLTextAreaElement; 17 | const toggleLogRef = document.querySelector("#toggle_log") as HTMLAnchorElement; 18 | 19 | const params = new URLSearchParams(window.location.search) 20 | window.estimator = estimator; 21 | 22 | if (process.env.SERVER_URL) { 23 | console.log('Setting server url to %s', process.env.SERVER_URL) 24 | window.config.serverURL = process.env.SERVER_URL 25 | } 26 | 27 | // get default values from the querystring if there's any 28 | if (params.get('swma_calculation_type')) { 29 | window.config.swma_calculation_type = params.get('swma_calculation_type') as SWMACalculationType; 30 | } 31 | 32 | if (params.get('swma_threshold_type')) { 33 | window.config.swma_threshold_type = params.get('swma_threshold_type') as SWMAThresholdType; 34 | } 35 | 36 | if (params.get('swma_threshold')) { 37 | window.config.swma_threshold = parseInt(params.get('swma_threshold') || '5', 10); 38 | } 39 | 40 | if (params.get('swma_window_size')) { 41 | window.config.swma_window_size = parseInt(params.get('swma_window_size') || '50', 10); 42 | } 43 | 44 | if (params.get('swma_calculation_interval')) { 45 | window.config.swma_calculation_interval = parseInt(params.get('swma_calculation_interval') || '10', 10); 46 | } 47 | 48 | const logHandler = (txt: string) => { 49 | const div = document.createElement('div'); 50 | const pre = document.createElement('pre'); 51 | pre.innerText = txt; 52 | div.appendChild(pre); 53 | logContentRef.appendChild(div); 54 | }; 55 | 56 | // fill resolutions combobox 57 | Object.keys(window.config.resolutions).forEach(key => { 58 | resolutionsRef.options[resolutionsRef.options.length] = new Option(window.config.resolutions[key], key); 59 | }); 60 | 61 | Object.keys(window.config.throttleData).forEach(key => { 62 | throttleDDL.options[throttleDDL.options.length] = new Option(window.config.throttleData[parseInt(key)], key); 63 | }); 64 | 65 | const plotConfig = { 66 | toImageButtonOptions: { 67 | format: 'svg', // one of png, svg, jpeg, webp 68 | filename: 'custom_image', 69 | width: 700, 70 | scale: 1 // Multiply title/legend/axis/canvas sizes by this factor 71 | }, 72 | displayModeBar: true, 73 | scrollZoom: true, 74 | displaylogo: false, 75 | responsive: true 76 | } as Plotly.Config; 77 | 78 | const plotLayout = { 79 | hovermode: 'closest', 80 | margin: { 81 | r: 10, 82 | t: 40, 83 | b: 40, 84 | l: 50 85 | }, 86 | height: 400, 87 | title: '', 88 | showlegend: true, 89 | legend: { 90 | x: 0, 91 | y: -0.3, 92 | orientation: 'h', 93 | }, 94 | grid: { 95 | rows: 1, 96 | columns: 1, 97 | pattern: 'independent' 98 | }, 99 | xaxis: { 100 | anchor: 'y', 101 | type: 'linear', 102 | showgrid: true, 103 | showticklabels: true, 104 | title: 'Time (s)', 105 | rangemode: 'tozero' 106 | }, 107 | yaxis: { 108 | anchor: 'x', 109 | showgrid: true, 110 | title: 'Mbps', 111 | rangemode: 'tozero' 112 | }, 113 | font: { 114 | family: 'sans-serif', 115 | size: 18, 116 | color: '#000' 117 | }, 118 | 119 | } as Plotly.Layout; 120 | 121 | const plotData = [{ 122 | x: [] as number[], 123 | y: [] as number[], 124 | name: 'Server ETP', 125 | mode: 'markers', 126 | xaxis: 'x', 127 | yaxis: 'y', 128 | marker: { 129 | color: 'black', 130 | size: 11, 131 | symbol: 'cross-thin', 132 | line: { 133 | width: 3, 134 | } 135 | } 136 | }, { 137 | x: [] as number[], 138 | y: [] as number[], 139 | name: 'tc Rate', 140 | mode: 'line', 141 | xaxis: 'x', 142 | yaxis: 'y', 143 | line: { 144 | color: '#0905ed', 145 | width: 3 146 | } 147 | }, { 148 | x: [] as number[], 149 | y: [] as number[], 150 | name: 'SWMA', 151 | mode: 'markers', 152 | xaxis: 'x', 153 | yaxis: 'y', 154 | marker: { 155 | color: '#b33dc6', 156 | size: 11, 157 | symbol: 'x-thin', 158 | line: { 159 | width: 3, 160 | color: 'red' 161 | } 162 | } 163 | }, 164 | { 165 | x: [] as number[], 166 | y: [] as number[], 167 | name: 'IFA', 168 | mode: 'markers', 169 | xaxis: 'x', 170 | yaxis: 'y', 171 | marker: { 172 | color: '#037325', 173 | size: 11, 174 | symbol: 'star-triangle-down' 175 | } 176 | }, 177 | { 178 | x: [], 179 | y: [], 180 | name: 'Active Bandwidth Test', 181 | mode: 'markers', 182 | xaxis: 'x', 183 | yaxis: 'y', 184 | marker: { 185 | size: 7, 186 | color: '#27aeef' 187 | }, 188 | } 189 | ] as any[]; 190 | 191 | const plot = Plotly.newPlot(document.getElementById('plot') as HTMLDivElement, plotData, plotLayout, plotConfig); 192 | 193 | const player = new Player({ 194 | url: params.get("url") || window.config.serverURL, 195 | vid: vidRef, 196 | stats: statsRef, 197 | throttle: throttleRef, 198 | throttleDDLRef: throttleDDL, 199 | resolutions: resolutionsRef, 200 | activeBWTestRef: activeBWTestRef, 201 | continueStreamingRef: continueStreamingRef, 202 | activeBWAsset: window.config.activeBWAsset, 203 | activeBWTestInterval: window.config.activeBWTestInterval, 204 | autioStart: window.config.autoStart || true, 205 | logger: logHandler 206 | }) 207 | 208 | // expose player 209 | window.player = player; 210 | 211 | 212 | let timePassed = 0; 213 | let playerRefreshInterval = 1000; // 1 second 214 | const displayedHistory = 240; // 4 minutes 215 | const plotStartDelay = 4000; // 4 seconds 216 | const testDuration = window.config.testDuration || 0; 217 | 218 | let plotTimer: NodeJS.Timer; 219 | 220 | const startPlotting = () => { 221 | console.log('in startPlotting'); 222 | plotTimer = setInterval(() => { 223 | if (!player.started || player.paused) { 224 | return; 225 | } 226 | timePassed += playerRefreshInterval; 227 | 228 | const currentSec = Math.round(timePassed / 1000); 229 | 230 | if (testDuration > 0 && currentSec === testDuration) { 231 | player.pauseOrResume(true); 232 | player.downloadStats().then(results => { 233 | console.log('results', results); 234 | }); 235 | return; 236 | } 237 | 238 | // save results by time 239 | // these will be downloaded after the test 240 | player.saveResultBySecond('swma', player.throughputs.get('swma') || 0, currentSec); 241 | player.saveResultBySecond('ifa', player.throughputs.get('ifa') || 0, currentSec); 242 | player.saveResultBySecond('etp', player.serverBandwidth || 0, currentSec); 243 | player.saveResultBySecond('tcRate', player.tcRate || 0, currentSec); 244 | player.saveResultBySecond('last-active-bw', player.lastActiveBWTestResult || 0, currentSec); 245 | 246 | plotData.forEach(p => (p.x as Plotly.Datum[]).push(currentSec)); 247 | (plotData[0].y as Plotly.Datum[]).push(player.serverBandwidth / 1000000); 248 | (plotData[1].y as Plotly.Datum[]).push(player.tcRate / 1000000); 249 | (plotData[2].y as Plotly.Datum[]).push(player.supress_throughput_value ? null : (player.throughputs.get('swma') || 0) / 1000000); 250 | (plotData[3].y as Plotly.Datum[]).push(player.supress_throughput_value ? null : (player.throughputs.get('ifa') || 0) / 1000000); 251 | (plotData[4].y as Plotly.Datum[]).push(player.activeBWTestResult === 0 ? null : player.activeBWTestResult / 1000000); 252 | 253 | // show max 60 seconds 254 | if (plotData[0].x.length > displayedHistory) { 255 | plotData.forEach(item => { 256 | (item.x as Plotly.Datum[]).splice(0, 1); 257 | (item.y as Plotly.Datum[]).splice(0, 1); 258 | }) 259 | } 260 | 261 | const data_update = { 262 | x: Object.values(plotData).map(item => item.x), 263 | y: Object.values(plotData).map(item => item.y), 264 | } as Plotly.Data; 265 | 266 | Plotly.update(document.getElementById('plot') as Plotly.Root, data_update, plotLayout) 267 | }, playerRefreshInterval); 268 | }; 269 | 270 | startRef.addEventListener("click", async (e) => { 271 | e.preventDefault(); 272 | if (!player.started) { 273 | await player.start(); 274 | if (player.started) { 275 | startRef.innerText = 'Stop'; 276 | setTimeout(() => startPlotting(), plotStartDelay); 277 | } else { 278 | alert('Error occurred in starting!'); 279 | } 280 | } else { 281 | player.stop(); 282 | } 283 | }) 284 | 285 | liveRef.addEventListener("click", (e) => { 286 | e.preventDefault() 287 | player.goLive() 288 | }) 289 | 290 | throttleRef.addEventListener("click", (e) => { 291 | e.preventDefault() 292 | player.throttle() 293 | }) 294 | 295 | playRef.addEventListener('click', (e) => { 296 | vidRef.muted = true; 297 | vidRef.play() 298 | e.preventDefault() 299 | }) 300 | 301 | toggleLogRef.addEventListener('click', (e) => { 302 | const logEl = document.getElementById('log'); 303 | if (!logEl) { 304 | return; 305 | }; 306 | 307 | if (toggleLogRef.innerText === 'Show Logs') { 308 | toggleLogRef.innerText = 'Hide Logs'; 309 | logEl.style.display = 'block'; 310 | } else { 311 | toggleLogRef.innerText = 'Show Logs'; 312 | logEl.style.display = 'none'; 313 | } 314 | }); 315 | 316 | function playFunc(e: Event) { 317 | playRef.style.display = "none" 318 | //player.goLive() 319 | 320 | // Only fire once to restore pause/play functionality 321 | vidRef.removeEventListener('play', playFunc) 322 | } 323 | 324 | vidRef.addEventListener('play', playFunc) 325 | vidRef.volume = 0.5 326 | 327 | // Try to autoplay but ignore errors on mobile; they need to click 328 | // vidRef.play().catch((e) => console.warn(e)) -------------------------------------------------------------------------------- /player/src/init.ts: -------------------------------------------------------------------------------- 1 | import { MP4New, MP4File, MP4ArrayBuffer, MP4Info } from "./mp4" 2 | 3 | export class InitParser { 4 | mp4box: MP4File; 5 | offset: number; 6 | 7 | raw: MP4ArrayBuffer[]; 8 | ready: Promise; 9 | 10 | constructor() { 11 | this.mp4box = MP4New() 12 | 13 | this.raw = [] 14 | this.offset = 0 15 | 16 | // Create a promise that gets resolved once the init segment has been parsed. 17 | this.ready = new Promise((resolve, reject) => { 18 | this.mp4box.onError = reject 19 | 20 | // https://github.com/gpac/mp4box.js#onreadyinfo 21 | this.mp4box.onReady = (info: MP4Info) => { 22 | if (!info.isFragmented) { 23 | reject("expected a fragmented mp4") 24 | } 25 | 26 | if (info.tracks.length != 1) { 27 | reject("expected a single track") 28 | } 29 | 30 | resolve({ 31 | info: info, 32 | raw: this.raw, 33 | }) 34 | } 35 | }) 36 | } 37 | 38 | push(data: Uint8Array) { 39 | // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately 40 | let box = new Uint8Array(data.byteLength); 41 | box.set(data) 42 | 43 | // and for some reason we need to modify the underlying ArrayBuffer with fileStart 44 | let buffer = box.buffer as MP4ArrayBuffer 45 | buffer.fileStart = this.offset 46 | 47 | // Parse the data 48 | this.offset = this.mp4box.appendBuffer(buffer) 49 | this.mp4box.flush() 50 | 51 | // Add the box to our queue of chunks 52 | this.raw.push(buffer) 53 | } 54 | } 55 | 56 | export interface Init { 57 | raw: MP4ArrayBuffer[]; 58 | info: MP4Info; 59 | } 60 | -------------------------------------------------------------------------------- /player/src/message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | init?: MessageInit 3 | segment?: MessageSegment 4 | ping?: MessagePing 5 | pong?: MessagePong 6 | } 7 | 8 | export interface MessageInit { 9 | id: string 10 | } 11 | 12 | export interface MessageSegment { 13 | init: string // id of the init segment 14 | timestamp: number // presentation timestamp in milliseconds of the first sample 15 | etp: number // estimated throughput in Kbps / CTA 5006 16 | tc_rate: number // applied tc netem rate in Mbps 17 | at: number // availability time / CTA 5006 18 | // TODO track would be nice 19 | } 20 | 21 | export interface MessagePing { 22 | 23 | } 24 | 25 | 26 | export interface MessagePong { 27 | 28 | } 29 | 30 | // user preference 31 | export interface MessagePref { 32 | name: string; 33 | value: string; 34 | } 35 | 36 | export interface Debug { 37 | max_bitrate: number 38 | } 39 | -------------------------------------------------------------------------------- /player/src/mp4.ts: -------------------------------------------------------------------------------- 1 | // Wrapper around MP4Box to play nicely with MP4Box. 2 | // I tried getting a mp4box.all.d.ts file to work but just couldn't figure it out 3 | import { createFile, ISOFile, DataStream, BoxParser } from "./mp4box.all" 4 | 5 | // Rename some stuff so it's on brand. 6 | export { createFile as MP4New, ISOFile as MP4File, DataStream as MP4Stream, BoxParser as MP4Parser } 7 | 8 | export type MP4ArrayBuffer = ArrayBuffer & {fileStart: number}; 9 | 10 | export interface MP4MediaTrack { 11 | id: number; 12 | created: Date; 13 | modified: Date; 14 | movie_duration: number; 15 | layer: number; 16 | alternate_group: number; 17 | volume: number; 18 | track_width: number; 19 | track_height: number; 20 | timescale: number; 21 | duration: number; 22 | bitrate: number; 23 | codec: string; 24 | language: string; 25 | nb_samples: number; 26 | } 27 | 28 | export interface MP4VideoData { 29 | width: number; 30 | height: number; 31 | } 32 | 33 | export interface MP4VideoTrack extends MP4MediaTrack { 34 | video: MP4VideoData; 35 | } 36 | 37 | export interface MP4AudioData { 38 | sample_rate: number; 39 | channel_count: number; 40 | sample_size: number; 41 | } 42 | 43 | export interface MP4AudioTrack extends MP4MediaTrack { 44 | audio: MP4AudioData; 45 | } 46 | 47 | export type MP4Track = MP4VideoTrack | MP4AudioTrack; 48 | 49 | export interface MP4Info { 50 | duration: number; 51 | timescale: number; 52 | fragment_duration: number; 53 | isFragmented: boolean; 54 | isProgressive: boolean; 55 | hasIOD: boolean; 56 | brands: string[]; 57 | created: Date; 58 | modified: Date; 59 | tracks: MP4Track[]; 60 | mime: string; 61 | videoTracks: MP4Track[]; 62 | audioTracks: MP4Track[]; 63 | } 64 | 65 | export interface MP4Sample { 66 | number: number; 67 | track_id: number; 68 | timescale: number; 69 | description_index: number; 70 | description: any; 71 | data: ArrayBuffer; 72 | size: number; 73 | alreadyRead: number; 74 | duration: number; 75 | cts: number; 76 | dts: number; 77 | is_sync: boolean; 78 | is_leading: number; 79 | depends_on: number; 80 | is_depended_on: number; 81 | has_redundancy: number; 82 | degration_priority: number; 83 | offset: number; 84 | subsamples: any; 85 | } 86 | -------------------------------------------------------------------------------- /player/src/player.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | } 4 | 5 | 6 | body { 7 | background: #000000; 8 | color: #ffffff; 9 | padding: 0; 10 | margin: 0; 11 | display: flex; 12 | justify-content: center; 13 | font-family: sans-serif; 14 | } 15 | 16 | #main { 17 | padding: 20px; 18 | width: 100%; 19 | } 20 | 21 | #main > div:nth-child(1) { 22 | display: grid; 23 | grid-template-columns: 0.6fr 0.4fr; 24 | min-height: 50%; 25 | align-items: center; 26 | margin-bottom: 40px; 27 | } 28 | 29 | #screen { 30 | position: relative; 31 | } 32 | 33 | #play { 34 | position: absolute; 35 | width: 100%; 36 | height: 100%; 37 | background: rgba(0, 0, 0, 0.5); 38 | 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | 43 | z-index: 1; 44 | } 45 | 46 | #vid { 47 | width: 100%; 48 | border-radius: 20px; 49 | margin-bottom: 20px; 50 | } 51 | 52 | #screen_bottom { 53 | display: flex; 54 | justify-content: space-between; 55 | } 56 | 57 | #controls { 58 | display: flex; 59 | align-items: center; 60 | } 61 | 62 | #controls > * { 63 | margin-right: 8px; 64 | } 65 | 66 | #controls label { 67 | margin-right: 8px; 68 | } 69 | 70 | #stats { 71 | display: grid; 72 | grid-template-columns: auto 1fr; 73 | gap: 5px; 74 | height: fit-content; 75 | } 76 | 77 | #stats label { 78 | padding: 0 1rem; 79 | } 80 | 81 | .buffer { 82 | position: relative; 83 | width: 100%; 84 | } 85 | 86 | .buffer .fill { 87 | position: absolute; 88 | transition-duration: 0.1s; 89 | transition-property: left, right, background-color; 90 | background-color: turquoise; 91 | height: 100%; 92 | text-align: right; 93 | padding-right: 0.5rem; 94 | overflow: hidden; 95 | } 96 | 97 | .label>.seconds { 98 | font-size: 0.8em; 99 | } 100 | 101 | .buffer .fill.net { 102 | background-color: turquoise; 103 | } 104 | 105 | #plot { 106 | margin-top: 20px; 107 | margin-bottom: 20px; 108 | } 109 | 110 | 111 | 112 | #repo_info { 113 | margin-left: 17px; 114 | border-radius: 10px; 115 | width: 100%; 116 | text-align: right; 117 | } 118 | 119 | #right_col { 120 | display: grid; 121 | grid-template-rows: 1fr auto; 122 | height: 80%; 123 | width: 100%; 124 | } 125 | 126 | .github_logo { 127 | width: 48px; 128 | } 129 | 130 | #toggle_log { 131 | color: white; 132 | font-size: 0.8rem; 133 | } 134 | 135 | #log { 136 | display: none; 137 | height: 500px; 138 | background-color: white; 139 | overflow: auto; 140 | color: black; 141 | } -------------------------------------------------------------------------------- /player/src/segment.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "./source" 2 | import { Init } from "./init" 3 | import { MP4New, MP4File, MP4Sample, MP4Stream, MP4Parser, MP4ArrayBuffer } from "./mp4" 4 | 5 | // Manage a segment download, keeping a buffer of a single sample to potentially rewrite the duration. 6 | export class Segment { 7 | source: Source; // The SourceBuffer used to decode media. 8 | offset: number; // The byte offset in the received file so far 9 | samples: MP4Sample[]; // The samples ready to be flushed to the source. 10 | timestamp: number; // The expected timestamp of the first sample in milliseconds 11 | init: Init; 12 | 13 | dts?: number; // The parsed DTS of the first sample 14 | timescale?: number; // The parsed timescale of the segment 15 | 16 | input: MP4File; // MP4Box file used to parse the incoming atoms. 17 | output: MP4File; // MP4Box file used to write the outgoing atoms after modification. 18 | 19 | done: boolean; // The segment has been completed 20 | 21 | constructor(source: Source, init: Init, timestamp: number) { 22 | this.source = source 23 | this.offset = 0 24 | this.done = false 25 | this.timestamp = timestamp 26 | this.init = init 27 | 28 | this.input = MP4New(); 29 | this.output = MP4New(); 30 | this.samples = []; 31 | 32 | this.input.onReady = (info: any) => { 33 | this.input.setExtractionOptions(info.tracks[0].id, {}, { nbSamples: 1 }); 34 | 35 | this.input.onSamples = this.onSamples.bind(this) 36 | this.input.start(); 37 | } 38 | 39 | // We have to reparse the init segment to work with mp4box 40 | for (let i = 0; i < init.raw.length; i += 1) { 41 | this.offset = this.input.appendBuffer(init.raw[i]) 42 | 43 | // Also populate the output with our init segment so it knows about tracks 44 | this.output.appendBuffer(init.raw[i]) 45 | } 46 | 47 | this.input.flush() 48 | this.output.flush() 49 | } 50 | 51 | push(data: Uint8Array) { 52 | if (this.done) return; // ignore new data after marked done 53 | 54 | // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately 55 | let box = new Uint8Array(data.byteLength); 56 | box.set(data); 57 | 58 | // and for some reason we need to modify the underlying ArrayBuffer with offset 59 | let buffer = box.buffer as MP4ArrayBuffer 60 | buffer.fileStart = this.offset 61 | 62 | // Parse the data 63 | this.offset = this.input.appendBuffer(buffer) 64 | this.input.flush() 65 | } 66 | 67 | onSamples(id: number, user: any, samples: MP4Sample[]) { 68 | if (!samples.length) return; 69 | 70 | if (this.dts === undefined) { 71 | this.dts = samples[0].dts; 72 | this.timescale = samples[0].timescale; 73 | } 74 | 75 | // Add the samples to a queue 76 | this.samples.push(...samples) 77 | } 78 | 79 | // Flushes any pending samples, returning true if the stream has finished. 80 | flush(): boolean { 81 | let stream = new MP4Stream(new ArrayBuffer(0), 0, false); // big-endian 82 | 83 | while (this.samples.length) { 84 | // Keep a single sample if we're not done yet 85 | if (!this.done && this.samples.length < 2) break; 86 | 87 | const sample = this.samples.shift() 88 | if (!sample) break; 89 | 90 | let moof = this.output.createSingleSampleMoof(sample); 91 | moof.write(stream); 92 | 93 | // adjusting the data_offset now that the moof size is known 94 | moof.trafs[0].truns[0].data_offset = moof.size+8; //8 is mdat header 95 | stream.adjustUint32(moof.trafs[0].truns[0].data_offset_position, moof.trafs[0].truns[0].data_offset); 96 | 97 | // @ts-ignore 98 | var mdat = new MP4Parser.mdatBox(); 99 | mdat.data = sample.data; 100 | mdat.write(stream); 101 | } 102 | 103 | this.source.initialize(this.init) 104 | this.source.append(stream.buffer as ArrayBuffer) 105 | 106 | return this.done 107 | } 108 | 109 | // The segment has completed 110 | finish() { 111 | console.log(this.timestamp + " done") 112 | this.done = true 113 | this.flush() 114 | 115 | // Trim the buffer to 30s long after each segment. 116 | this.source.trim(30) 117 | } 118 | 119 | // Extend the last sample so it reaches the provided timestamp 120 | skipTo(pts: number) { 121 | if (this.samples.length == 0) return 122 | let last = this.samples[this.samples.length-1] 123 | 124 | const skip = pts - (last.dts + last.duration); 125 | 126 | if (skip == 0) return; 127 | if (skip < 0) throw "can't skip backwards" 128 | 129 | last.duration += skip 130 | 131 | if (this.timescale) { 132 | console.warn("skipping video", skip / this.timescale) 133 | } 134 | } 135 | 136 | buffered() { 137 | // Ignore if we have a single sample 138 | if (this.samples.length <= 1) return undefined; 139 | if (!this.timescale) return undefined; 140 | 141 | const first = this.samples[0]; 142 | const last = this.samples[this.samples.length-1] 143 | 144 | 145 | return { 146 | length: 1, 147 | start: first.dts / this.timescale, 148 | end: (last.dts + last.duration) / this.timescale, 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /player/src/source.ts: -------------------------------------------------------------------------------- 1 | import { Init } from "./init" 2 | 3 | // Create a SourceBuffer with convenience methods 4 | export class Source { 5 | sourceBuffer?: SourceBuffer; 6 | mediaSource: MediaSource; 7 | queue: Array; 8 | init?: Init; 9 | 10 | constructor(mediaSource: MediaSource) { 11 | this.mediaSource = mediaSource; 12 | this.queue = []; 13 | } 14 | 15 | // (re)initialize the source using the provided init segment. 16 | initialize(init: Init) { 17 | // Check if the init segment is already in the queue. 18 | for (let i = this.queue.length - 1; i >= 0; i--) { 19 | if ((this.queue[i] as SourceInit).init == init) { 20 | // Already queued up. 21 | return 22 | } 23 | } 24 | 25 | // Check if the init segment has already been applied. 26 | if (this.init == init) { 27 | return 28 | } 29 | 30 | // Add the init segment to the queue so we call addSourceBuffer or changeType 31 | this.queue.push({ 32 | kind: "init", 33 | init: init, 34 | }) 35 | 36 | for (let i = 0; i < init.raw.length; i += 1) { 37 | this.queue.push({ 38 | kind: "data", 39 | data: init.raw[i], 40 | }) 41 | } 42 | 43 | this.flush() 44 | } 45 | 46 | // Append the segment data to the buffer. 47 | append(data: Uint8Array | ArrayBuffer) { 48 | this.queue.push({ 49 | kind: "data", 50 | data: data, 51 | }) 52 | 53 | this.flush() 54 | } 55 | 56 | // Return the buffered range. 57 | buffered() { 58 | if (!this.sourceBuffer) { 59 | return { length: 0 } 60 | } 61 | 62 | return this.sourceBuffer.buffered 63 | } 64 | 65 | // Delete any media older than x seconds from the buffer. 66 | trim(duration: number) { 67 | this.queue.push({ 68 | kind: "trim", 69 | trim: duration, 70 | }) 71 | 72 | this.flush() 73 | } 74 | 75 | // Flush any queued instructions 76 | flush() { 77 | while (1) { 78 | // Check if the buffer is currently busy. 79 | if (this.sourceBuffer && this.sourceBuffer.updating) { 80 | break; 81 | } 82 | 83 | // Process the next item in the queue. 84 | const next = this.queue.shift() 85 | if (!next) { 86 | break; 87 | } 88 | 89 | switch (next.kind) { 90 | case "init": 91 | this.init = next.init; 92 | 93 | if (!this.sourceBuffer) { 94 | // Create a new source buffer. 95 | try { 96 | // 97 | // Sometimes DomException occurs. 98 | // DOMException: Failed to execute 'addSourceBuffer' on 'MediaSource': 99 | // This MediaSource has reached the limit of SourceBuffer objects it can handle. 100 | // No additional SourceBuffer objects may be added. 101 | // 102 | this.sourceBuffer = this.mediaSource.addSourceBuffer(this.init.info.mime) 103 | } catch (err) { 104 | // TODO: handle this in a better way 105 | console.error(err); 106 | location.reload(); 107 | return; 108 | } 109 | 110 | // Call flush automatically after each update finishes. 111 | this.sourceBuffer.addEventListener('updateend', this.flush.bind(this)) 112 | } else { 113 | this.sourceBuffer.changeType(next.init.info.mime) 114 | } 115 | 116 | break; 117 | case "data": 118 | if (!this.sourceBuffer) { 119 | throw "failed to call initialize before append" 120 | } 121 | this.sourceBuffer.appendBuffer(next.data) 122 | break; 123 | case "trim": 124 | if (!this.sourceBuffer) { 125 | throw "failed to call initialize before trim" 126 | } 127 | 128 | try { 129 | const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1) - next.trim; 130 | const start = this.sourceBuffer.buffered.start(0) 131 | 132 | if (end > start) { 133 | this.sourceBuffer.remove(start, end) 134 | } 135 | } catch (e) { 136 | console.warn('error in trimming sourceBuffer') 137 | } 138 | break; 139 | default: 140 | throw "impossible; unknown SourceItem" 141 | } 142 | } 143 | } 144 | } 145 | 146 | interface SourceItem { } 147 | 148 | class SourceInit implements SourceItem { 149 | kind!: "init"; 150 | init!: Init; 151 | } 152 | 153 | class SourceData implements SourceItem { 154 | kind!: "data"; 155 | data!: Uint8Array | ArrayBuffer; 156 | } 157 | 158 | class SourceTrim implements SourceItem { 159 | kind!: "trim"; 160 | trim!: number; 161 | } -------------------------------------------------------------------------------- /player/src/stream.ts: -------------------------------------------------------------------------------- 1 | // Reader wraps a stream and provides convience methods for reading pieces from a stream 2 | export class StreamReader { 3 | reader: ReadableStreamDefaultReader; // TODO make a separate class without promises when null 4 | buffer: Uint8Array; 5 | 6 | constructor(reader: ReadableStreamDefaultReader, buffer: Uint8Array = new Uint8Array(0)) { 7 | this.reader = reader 8 | this.buffer = buffer 9 | } 10 | 11 | // TODO implementing pipeTo seems more reasonable than releasing the lock 12 | release() { 13 | this.reader.releaseLock() 14 | } 15 | 16 | // Returns any number of bytes 17 | async read(): Promise { 18 | if (this.buffer.byteLength) { 19 | const buffer = this.buffer; 20 | this.buffer = new Uint8Array() 21 | return buffer 22 | } 23 | 24 | const result = await this.reader.read() 25 | return result.value 26 | } 27 | 28 | async bytes(size: number): Promise { 29 | while (this.buffer.byteLength < size) { 30 | const result = await this.reader.read() 31 | if (result.done) { 32 | throw "short buffer" 33 | } 34 | 35 | const buffer = new Uint8Array(result.value) 36 | 37 | if (this.buffer.byteLength == 0) { 38 | this.buffer = buffer 39 | } else { 40 | const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) 41 | temp.set(this.buffer) 42 | temp.set(buffer, this.buffer.byteLength) 43 | this.buffer = temp 44 | } 45 | } 46 | 47 | const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) 48 | this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) 49 | 50 | return result 51 | } 52 | 53 | async peek(size: number): Promise { 54 | while (this.buffer.byteLength < size) { 55 | const result = await this.reader.read() 56 | if (result.done) { 57 | throw "short buffer" 58 | } 59 | 60 | const buffer = new Uint8Array(result.value) 61 | 62 | if (this.buffer.byteLength == 0) { 63 | this.buffer = buffer 64 | } else { 65 | const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) 66 | temp.set(this.buffer) 67 | temp.set(buffer, this.buffer.byteLength) 68 | this.buffer = temp 69 | } 70 | } 71 | 72 | return new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) 73 | } 74 | 75 | async view(size: number): Promise { 76 | const buf = await this.bytes(size) 77 | return new DataView(buf.buffer, buf.byteOffset, buf.byteLength) 78 | } 79 | 80 | async uint8(): Promise { 81 | const view = await this.view(1) 82 | return view.getUint8(0) 83 | } 84 | 85 | async uint16(): Promise { 86 | const view = await this.view(2) 87 | return view.getUint16(0) 88 | } 89 | 90 | async uint32(): Promise { 91 | const view = await this.view(4) 92 | return view.getUint32(0) 93 | } 94 | 95 | // Returns a Number using 52-bits, the max Javascript can use for integer math 96 | async uint52(): Promise { 97 | const v = await this.uint64() 98 | if (v > Number.MAX_SAFE_INTEGER) { 99 | throw "overflow" 100 | } 101 | 102 | return Number(v) 103 | } 104 | 105 | // Returns a Number using 52-bits, the max Javascript can use for integer math 106 | async vint52(): Promise { 107 | const v = await this.vint64() 108 | if (v > Number.MAX_SAFE_INTEGER) { 109 | throw "overflow" 110 | } 111 | 112 | return Number(v) 113 | } 114 | 115 | // NOTE: Returns a BigInt instead of a Number 116 | async uint64(): Promise { 117 | const view = await this.view(8) 118 | return view.getBigUint64(0) 119 | } 120 | 121 | // NOTE: Returns a BigInt instead of a Number 122 | async vint64(): Promise { 123 | const peek = await this.peek(1) 124 | const first = new DataView(peek.buffer, peek.byteOffset, peek.byteLength).getUint8(0) 125 | const size = (first & 0xc0) >> 6 126 | 127 | switch (size) { 128 | case 0: 129 | const v0 = await this.uint8() 130 | return BigInt(v0) & 0x3fn 131 | case 1: 132 | const v1 = await this.uint16() 133 | return BigInt(v1) & 0x3fffn 134 | case 2: 135 | const v2 = await this.uint32() 136 | return BigInt(v2) & 0x3fffffffn 137 | case 3: 138 | const v3 = await this.uint64() 139 | return v3 & 0x3fffffffffffffffn 140 | default: 141 | throw "impossible" 142 | } 143 | } 144 | 145 | async done(): Promise { 146 | try { 147 | const peek = await this.peek(1) 148 | return false 149 | } catch (err) { 150 | return true // Assume EOF 151 | } 152 | } 153 | } 154 | 155 | // StreamWriter wraps a stream and writes chunks of data 156 | export class StreamWriter { 157 | buffer: ArrayBuffer; 158 | writer: WritableStreamDefaultWriter; 159 | 160 | constructor(stream: WritableStream) { 161 | this.buffer = new ArrayBuffer(8) 162 | this.writer = stream.getWriter() 163 | } 164 | 165 | release() { 166 | this.writer.releaseLock() 167 | } 168 | 169 | async close() { 170 | return this.writer.close() 171 | } 172 | 173 | async uint8(v: number) { 174 | const view = new DataView(this.buffer, 0, 1) 175 | view.setUint8(0, v) 176 | return this.writer.write(view) 177 | } 178 | 179 | async uint16(v: number) { 180 | const view = new DataView(this.buffer, 0, 2) 181 | view.setUint16(0, v) 182 | return this.writer.write(view) 183 | } 184 | 185 | async uint24(v: number) { 186 | const v1 = (v >> 16) & 0xff 187 | const v2 = (v >> 8) & 0xff 188 | const v3 = (v) & 0xff 189 | 190 | const view = new DataView(this.buffer, 0, 3) 191 | view.setUint8(0, v1) 192 | view.setUint8(1, v2) 193 | view.setUint8(2, v3) 194 | 195 | return this.writer.write(view) 196 | } 197 | 198 | async uint32(v: number) { 199 | const view = new DataView(this.buffer, 0, 4) 200 | view.setUint32(0, v) 201 | return this.writer.write(view) 202 | } 203 | 204 | async uint52(v: number) { 205 | if (v > Number.MAX_SAFE_INTEGER) { 206 | throw "value too large" 207 | } 208 | 209 | this.uint64(BigInt(v)) 210 | } 211 | 212 | async vint52(v: number) { 213 | if (v > Number.MAX_SAFE_INTEGER) { 214 | throw "value too large" 215 | } 216 | 217 | if (v < (1 << 6)) { 218 | return this.uint8(v) 219 | } else if (v < (1 << 14)) { 220 | return this.uint16(v|0x4000) 221 | } else if (v < (1 << 30)) { 222 | return this.uint32(v|0x80000000) 223 | } else { 224 | return this.uint64(BigInt(v) | 0xc000000000000000n) 225 | } 226 | } 227 | 228 | async uint64(v: bigint) { 229 | const view = new DataView(this.buffer, 0, 8) 230 | view.setBigUint64(0, v) 231 | return this.writer.write(view) 232 | } 233 | 234 | async vint64(v: bigint) { 235 | if (v < (1 << 6)) { 236 | return this.uint8(Number(v)) 237 | } else if (v < (1 << 14)) { 238 | return this.uint16(Number(v)|0x4000) 239 | } else if (v < (1 << 30)) { 240 | return this.uint32(Number(v)|0x80000000) 241 | } else { 242 | return this.uint64(v | 0xc000000000000000n) 243 | } 244 | } 245 | 246 | async bytes(buffer: ArrayBuffer) { 247 | return this.writer.write(buffer) 248 | } 249 | 250 | async string(str: string) { 251 | const data = new TextEncoder().encode(str) 252 | return this.writer.write(data) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /player/src/track.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "./source" 2 | import { Segment } from "./segment" 3 | import { TimeRange } from "./util" 4 | 5 | // An audio or video track that consists of multiple sequential segments. 6 | // 7 | // Instead of buffering, we want to drop video while audio plays uninterupted. 8 | // Chrome actually plays up to 3s of audio without video before buffering when in low latency mode. 9 | // Unforuntately, this does not recover correctly when there are gaps (pls fix). 10 | // Our solution is to flush segments in decode order, buffering a single additional frame. 11 | // We extend the duration of the buffered frame and flush it to cover any gaps. 12 | export class Track { 13 | source: Source; 14 | segments: Segment[]; 15 | 16 | constructor(source: Source) { 17 | this.source = source; 18 | this.segments = []; 19 | } 20 | 21 | add(segment: Segment) { 22 | // TODO don't add if the segment is out of date already 23 | this.segments.push(segment) 24 | 25 | // Sort by timestamp ascending 26 | // NOTE: The timestamp is in milliseconds, and we need to parse the media to get the accurate PTS/DTS. 27 | this.segments.sort((a: Segment, b: Segment): number => { 28 | return a.timestamp - b.timestamp 29 | }) 30 | } 31 | 32 | buffered(): TimeRanges { 33 | let ranges: TimeRange[] = [] 34 | 35 | const buffered = this.source.buffered() as TimeRanges 36 | for (let i = 0; i < buffered.length; i += 1) { 37 | // Convert the TimeRanges into an oject we can modify 38 | ranges.push({ 39 | start: buffered.start(i), 40 | end: buffered.end(i) 41 | }) 42 | } 43 | 44 | // Loop over segments and add in their ranges, merging if possible. 45 | for (let segment of this.segments) { 46 | const buffered = segment.buffered() 47 | if (!buffered) continue; 48 | 49 | if (ranges.length) { 50 | // Try to merge with an existing range 51 | const last = ranges[ranges.length-1]; 52 | if (buffered.start < last.start) { 53 | // Network buffer is old; ignore it 54 | continue 55 | } 56 | 57 | // Extend the end of the last range instead of pushing 58 | if (buffered.start <= last.end && buffered.end > last.end) { 59 | last.end = buffered.end 60 | continue 61 | } 62 | } 63 | 64 | ranges.push(buffered) 65 | } 66 | 67 | // TODO typescript 68 | return { 69 | length: ranges.length, 70 | start: (x) => { return ranges[x].start }, 71 | end: (x) => { return ranges[x].end }, 72 | } 73 | } 74 | 75 | flush() { 76 | while (1) { 77 | if (!this.segments.length) break 78 | 79 | const first = this.segments[0] 80 | const done = first.flush() 81 | if (!done) break 82 | 83 | this.segments.shift() 84 | } 85 | } 86 | 87 | // Given the current playhead, determine if we should drop any segments 88 | // If playhead is undefined, it means we're buffering so skip to anything now. 89 | advance(playhead: number | undefined) { 90 | if (this.segments.length < 2) return 91 | 92 | while (this.segments.length > 1) { 93 | const current = this.segments[0]; 94 | const next = this.segments[1]; 95 | 96 | if (next.dts === undefined || next.timescale == undefined) { 97 | // No samples have been parsed for the next segment yet. 98 | break 99 | } 100 | 101 | if (current.dts === undefined) { 102 | // No samples have been parsed for the current segment yet. 103 | // We can't cover the gap by extending the sample so we have to seek. 104 | // TODO I don't think this can happen, but I guess we have to seek past the gap. 105 | break 106 | } 107 | 108 | if (playhead !== undefined) { 109 | // Check if the next segment has playable media now. 110 | // Otherwise give the current segment more time to catch up. 111 | if ((next.dts / next.timescale) > playhead) { 112 | return 113 | } 114 | } 115 | 116 | current.skipTo(next.dts || 0) // tell typescript that it's not undefined; we already checked 117 | current.finish() 118 | 119 | // TODO cancel the QUIC stream to save bandwidth 120 | 121 | this.segments.shift() 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /player/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | 3 | declare global { 4 | interface Window { 5 | config: AppSettings; 6 | player: any; 7 | estimator: any; 8 | } 9 | type SWMAThresholdType = 'minimum_duration' | 'percentage'; 10 | type SWMACalculationType = 'segment' | 'window' | 'first_chunk'; 11 | 12 | type StreamingStatus = 'paused' | 'streaming'; 13 | } 14 | 15 | 16 | interface AppSettings { 17 | defaultPlayerURL: string; 18 | resolutions: { [id: string]: string }; 19 | throttleData: { [id: number]: string }; 20 | serverURL: string; 21 | activeBWAsset: { url: string; size: number }; 22 | activeBWTestInterval?: number, 23 | autoStart: boolean; 24 | testDuration?: number; 25 | swma_threshold: number; 26 | swma_calculation_type: SWMACalculationType; 27 | swma_threshold_type: SWMAThresholdType; 28 | swma_window_size: number; // sliding window size in terms of chunk count 29 | swma_calculation_interval: number; // tput is computed each N chunk. 30 | } -------------------------------------------------------------------------------- /player/src/types/webtransport.d.ts: -------------------------------------------------------------------------------- 1 | declare module "webtransport" 2 | 3 | /* 4 | There's no WebTransport support in TypeScript yet. Use this script to update definitions: 5 | 6 | npx webidl2ts -i https://www.w3.org/TR/webtransport/ -o webtransport.d.ts 7 | You'll have to fix the constructors by hand. 8 | */ 9 | 10 | interface WebTransportDatagramDuplexStream { 11 | readonly readable: ReadableStream; 12 | readonly writable: WritableStream; 13 | readonly maxDatagramSize: number; 14 | incomingMaxAge: number; 15 | outgoingMaxAge: number; 16 | incomingHighWaterMark: number; 17 | outgoingHighWaterMark: number; 18 | } 19 | 20 | interface WebTransport { 21 | getStats(): Promise; 22 | readonly ready: Promise; 23 | readonly closed: Promise; 24 | close(closeInfo?: WebTransportCloseInfo): undefined; 25 | readonly datagrams: WebTransportDatagramDuplexStream; 26 | createBidirectionalStream(): Promise; 27 | readonly incomingBidirectionalStreams: ReadableStream; 28 | createUnidirectionalStream(): Promise; 29 | readonly incomingUnidirectionalStreams: ReadableStream; 30 | } 31 | 32 | declare var WebTransport: { 33 | prototype: WebTransport; 34 | new(url: string, options?: WebTransportOptions): WebTransport; 35 | }; 36 | 37 | interface WebTransportHash { 38 | algorithm?: string; 39 | value?: BufferSource; 40 | } 41 | 42 | interface WebTransportOptions { 43 | allowPooling?: boolean; 44 | serverCertificateHashes?: Array; 45 | } 46 | 47 | interface WebTransportCloseInfo { 48 | closeCode?: number; 49 | reason?: string; 50 | } 51 | 52 | interface WebTransportStats { 53 | timestamp?: DOMHighResTimeStamp; 54 | bytesSent?: number; 55 | packetsSent?: number; 56 | numOutgoingStreamsCreated?: number; 57 | numIncomingStreamsCreated?: number; 58 | bytesReceived?: number; 59 | packetsReceived?: number; 60 | minRtt?: DOMHighResTimeStamp; 61 | numReceivedDatagramsDropped?: number; 62 | } 63 | 64 | interface WebTransportBidirectionalStream { 65 | readonly readable: ReadableStream; 66 | readonly writable: WritableStream; 67 | } 68 | 69 | interface WebTransportError extends DOMException { 70 | readonly source: WebTransportErrorSource; 71 | readonly streamErrorCode: number; 72 | } 73 | 74 | declare var WebTransportError: { 75 | prototype: WebTransportError; 76 | new(init?: WebTransportErrorInit): WebTransportError; 77 | }; 78 | 79 | interface WebTransportErrorInit { 80 | streamErrorCode?: number; 81 | message?: string; 82 | } 83 | 84 | type WebTransportErrorSource = "stream" | "session"; 85 | -------------------------------------------------------------------------------- /player/src/util.ts: -------------------------------------------------------------------------------- 1 | export interface TimeRange { 2 | start: number; 3 | end: number; 4 | } 5 | -------------------------------------------------------------------------------- /player/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "exclude": [ 6 | "node_modules", 7 | ], 8 | "compilerOptions": { 9 | "target": "es2021", 10 | "strict": true, 11 | "typeRoots": [ 12 | "src/types", 13 | "./node_modules/@types" 14 | ], 15 | "allowJs": true, 16 | "moduleResolution": "node" 17 | } 18 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | .env -------------------------------------------------------------------------------- /server/bin/speedtest.sh: -------------------------------------------------------------------------------- 1 | url=$1 2 | 3 | if avg_speed=$(curl -qkfsS -w '%{speed_download}' -o /dev/null --url "$url") 4 | then 5 | echo "$((avg_speed*8)) bits/sec" 6 | fi 7 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kixelated/warp-demo/server 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/abema/go-mp4 v0.7.2 7 | github.com/kixelated/invoker v1.0.0 8 | github.com/kixelated/quic-go v1.31.0 9 | github.com/kixelated/webtransport-go v1.4.1 10 | github.com/zencoder/go-dash/v3 v3.0.2 11 | ) 12 | 13 | require ( 14 | github.com/francoispqt/gojay v1.2.13 // indirect 15 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 16 | github.com/golang/mock v1.6.0 // indirect 17 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 18 | github.com/google/uuid v1.1.2 // indirect 19 | github.com/marten-seemann/qpack v0.3.0 // indirect 20 | github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect 21 | github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect 22 | github.com/onsi/ginkgo/v2 v2.2.0 // indirect 23 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect 24 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect 25 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect 26 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect 27 | golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06 // indirect 28 | golang.org/x/text v0.3.7 // indirect 29 | golang.org/x/tools v0.1.12 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /server/internal/warp/media.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | 16 | "github.com/abema/go-mp4" 17 | "github.com/kixelated/invoker" 18 | "github.com/zencoder/go-dash/v3/mpd" 19 | ) 20 | 21 | // This is a demo; you should actually fetch media from a live backend. 22 | // It's just much easier to read from disk and "fake" being live. 23 | type Media struct { 24 | base fs.FS 25 | inits map[string]*MediaInit 26 | video []*mpd.Representation 27 | audio []*mpd.Representation 28 | } 29 | 30 | func NewMedia(playlistPath string) (m *Media, err error) { 31 | m = new(Media) 32 | 33 | // Create a fs.FS out of the folder holding the playlist 34 | m.base = os.DirFS(filepath.Dir(playlistPath)) 35 | 36 | // Read the playlist file 37 | playlist, err := mpd.ReadFromFile(playlistPath) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to open playlist: %w", err) 40 | } 41 | 42 | if len(playlist.Periods) > 1 { 43 | return nil, fmt.Errorf("multiple periods not supported") 44 | } 45 | 46 | period := playlist.Periods[0] 47 | 48 | for _, adaption := range period.AdaptationSets { 49 | representation := adaption.Representations[0] 50 | 51 | if representation.MimeType == nil { 52 | return nil, fmt.Errorf("missing representation mime type") 53 | } 54 | 55 | if representation.Bandwidth == nil { 56 | return nil, fmt.Errorf("missing representation bandwidth") 57 | } 58 | 59 | switch *representation.MimeType { 60 | case "video/mp4": 61 | m.video = append(m.video, representation) 62 | case "audio/mp4": 63 | m.audio = append(m.audio, representation) 64 | } 65 | } 66 | 67 | if len(m.video) == 0 { 68 | return nil, fmt.Errorf("no video representation found") 69 | } 70 | 71 | if len(m.audio) == 0 { 72 | return nil, fmt.Errorf("no audio representation found") 73 | } 74 | 75 | m.inits = make(map[string]*MediaInit) 76 | 77 | var reps []*mpd.Representation 78 | reps = append(reps, m.audio...) 79 | reps = append(reps, m.video...) 80 | 81 | for _, rep := range reps { 82 | path := *rep.SegmentTemplate.Initialization 83 | 84 | // TODO Support the full template engine 85 | path = strings.ReplaceAll(path, "$RepresentationID$", *rep.ID) 86 | 87 | f, err := fs.ReadFile(m.base, path) 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to read init file: %w", err) 90 | } 91 | 92 | init, err := newMediaInit(*rep.ID, f) 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to create init segment: %w", err) 95 | } 96 | 97 | m.inits[*rep.ID] = init 98 | } 99 | 100 | return m, nil 101 | } 102 | 103 | func (m *Media) Start(bitrate func() uint64) (inits map[string]*MediaInit, audio *MediaStream, video *MediaStream, err error) { 104 | start := time.Now() 105 | 106 | audio, err = newMediaStream(m, m.audio, start, bitrate) 107 | if err != nil { 108 | return nil, nil, nil, err 109 | } 110 | 111 | video, err = newMediaStream(m, m.video, start, bitrate) 112 | if err != nil { 113 | return nil, nil, nil, err 114 | } 115 | 116 | return m.inits, audio, video, nil 117 | } 118 | 119 | type MediaStream struct { 120 | Media *Media 121 | 122 | start time.Time 123 | reps []*mpd.Representation 124 | sequence int 125 | bitrate func() uint64 // returns the current estimated bitrate 126 | } 127 | 128 | func newMediaStream(m *Media, reps []*mpd.Representation, start time.Time, bitrate func() uint64) (ms *MediaStream, err error) { 129 | ms = new(MediaStream) 130 | ms.Media = m 131 | ms.reps = reps 132 | ms.start = start 133 | ms.bitrate = bitrate 134 | return ms, nil 135 | } 136 | 137 | func (ms *MediaStream) chooseRepresentation(preferredId string) (choice *mpd.Representation) { 138 | bitrate := ms.bitrate() 139 | 140 | // Loop over the renditions and pick the highest bitrate we can support 141 | for _, r := range ms.reps { 142 | if *r.ID == preferredId { 143 | choice = r 144 | } else if uint64(*r.Bandwidth) <= bitrate && (choice == nil || *r.Bandwidth > *choice.Bandwidth) { 145 | choice = r 146 | } 147 | } 148 | 149 | if choice != nil { 150 | return choice 151 | } 152 | 153 | // We can't support any of the bitrates, so find the lowest one. 154 | for _, r := range ms.reps { 155 | if choice == nil || *r.Bandwidth < *choice.Bandwidth { 156 | choice = r 157 | } 158 | } 159 | 160 | return choice 161 | } 162 | 163 | // Returns the next segment in the stream 164 | func (ms *MediaStream) Next(ctx context.Context, session *Session, timeOffset time.Duration) (segment *MediaSegment, err error) { 165 | rep := ms.chooseRepresentation(session.prefs["resolution"]) 166 | 167 | if rep.SegmentTemplate == nil { 168 | return nil, fmt.Errorf("missing segment template") 169 | } 170 | 171 | if rep.SegmentTemplate.Media == nil { 172 | return nil, fmt.Errorf("no media template") 173 | } 174 | 175 | if rep.SegmentTemplate.StartNumber == nil { 176 | return nil, fmt.Errorf("missing start number") 177 | } 178 | 179 | path := *rep.SegmentTemplate.Media 180 | sequence := ms.sequence + int(*rep.SegmentTemplate.StartNumber) 181 | 182 | // TODO Support the full template engine 183 | path = strings.ReplaceAll(path, "$RepresentationID$", *rep.ID) 184 | path = strings.ReplaceAll(path, "$Number%05d$", fmt.Sprintf("%05d", sequence)) // TODO TODO 185 | 186 | // Try openning the file 187 | f, err := ms.Media.base.Open(path) 188 | if errors.Is(err, os.ErrNotExist) && ms.sequence != 0 { 189 | // Return EOF if the next file is missing 190 | return nil, nil 191 | } else if err != nil { 192 | return nil, fmt.Errorf("failed to open segment file: %w", err) 193 | } 194 | 195 | duration := time.Duration(*rep.SegmentTemplate.Duration) / time.Nanosecond 196 | timestamp := time.Duration(ms.sequence)*duration + timeOffset 197 | 198 | init := ms.Media.inits[*rep.ID] 199 | 200 | segment, err = newMediaSegment(ms, init, f, timestamp) 201 | if err != nil { 202 | return nil, fmt.Errorf("failed to create segment: %w", err) 203 | } 204 | 205 | ms.sequence += 1 206 | 207 | return segment, nil 208 | } 209 | 210 | type MediaInit struct { 211 | ID string 212 | Raw []byte 213 | Timescale int 214 | } 215 | 216 | func newMediaInit(id string, raw []byte) (mi *MediaInit, err error) { 217 | mi = new(MediaInit) 218 | mi.ID = id 219 | mi.Raw = raw 220 | 221 | err = mi.parse() 222 | if err != nil { 223 | return nil, fmt.Errorf("failed to parse init segment: %w", err) 224 | } 225 | 226 | return mi, nil 227 | } 228 | 229 | // Parse through the init segment, literally just to populate the timescale 230 | func (mi *MediaInit) parse() (err error) { 231 | r := bytes.NewReader(mi.Raw) 232 | 233 | _, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 234 | if !h.BoxInfo.IsSupportedType() { 235 | return nil, nil 236 | } 237 | 238 | payload, _, err := h.ReadPayload() 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | switch box := payload.(type) { 244 | case *mp4.Mdhd: // Media Header; moov -> trak -> mdia > mdhd 245 | if mi.Timescale != 0 { 246 | // verify only one track 247 | return nil, fmt.Errorf("multiple mdhd atoms") 248 | } 249 | 250 | mi.Timescale = int(box.Timescale) 251 | } 252 | 253 | // Expands children 254 | return h.Expand() 255 | }) 256 | 257 | if err != nil { 258 | return fmt.Errorf("failed to parse MP4 file: %w", err) 259 | } 260 | 261 | return nil 262 | } 263 | 264 | type MediaSegment struct { 265 | Stream *MediaStream 266 | Init *MediaInit 267 | 268 | file fs.File 269 | timestamp time.Duration 270 | } 271 | 272 | func newMediaSegment(s *MediaStream, init *MediaInit, file fs.File, timestamp time.Duration) (ms *MediaSegment, err error) { 273 | ms = new(MediaSegment) 274 | ms.Stream = s 275 | ms.Init = init 276 | 277 | ms.file = file 278 | ms.timestamp = timestamp 279 | 280 | return ms, nil 281 | } 282 | 283 | // Return the next atom, sleeping based on the PTS to simulate a live stream 284 | func (ms *MediaSegment) Read(ctx context.Context) (chunk []byte, err error) { 285 | // Read the next top-level box 286 | var header [8]byte 287 | 288 | _, err = io.ReadFull(ms.file, header[:]) 289 | if err != nil { 290 | return nil, fmt.Errorf("failed to read header: %w", err) 291 | } 292 | 293 | size := int(binary.BigEndian.Uint32(header[0:4])) 294 | if size < 8 { 295 | return nil, fmt.Errorf("box is too small") 296 | } 297 | 298 | buf := make([]byte, size) 299 | n := copy(buf, header[:]) 300 | 301 | _, err = io.ReadFull(ms.file, buf[n:]) 302 | if err != nil { 303 | return nil, fmt.Errorf("failed to read atom: %w", err) 304 | } 305 | 306 | sample, err := ms.parseAtom(ctx, buf) 307 | if err != nil { 308 | return nil, fmt.Errorf("failed to parse atom: %w", err) 309 | } 310 | 311 | if sample != nil { 312 | // Simulate a live stream by sleeping before we write this sample. 313 | // Figure out how much time has elapsed since the start 314 | elapsed := time.Since(ms.Stream.start) 315 | delay := (sample.Timestamp - elapsed) 316 | 317 | if delay > 0 { 318 | // Sleep until we're supposed to see these samples 319 | err = invoker.Sleep(delay)(ctx) 320 | if err != nil { 321 | return nil, err 322 | } 323 | } 324 | } 325 | 326 | return buf, nil 327 | } 328 | 329 | // Parse through the MP4 atom, returning infomation about the next fragmented sample 330 | func (ms *MediaSegment) parseAtom(ctx context.Context, buf []byte) (sample *mediaSample, err error) { 331 | r := bytes.NewReader(buf) 332 | 333 | _, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 334 | if !h.BoxInfo.IsSupportedType() { 335 | return nil, nil 336 | } 337 | 338 | payload, _, err := h.ReadPayload() 339 | if err != nil { 340 | return nil, err 341 | } 342 | 343 | switch box := payload.(type) { 344 | case *mp4.Moof: 345 | sample = new(mediaSample) 346 | case *mp4.Tfdt: // Track Fragment Decode Timestamp; moof -> traf -> tfdt 347 | // TODO This box isn't required 348 | // TODO we want the last PTS if there are multiple samples 349 | var dts time.Duration 350 | if box.FullBox.Version == 0 { 351 | dts = time.Duration(box.BaseMediaDecodeTimeV0) 352 | } else { 353 | dts = time.Duration(box.BaseMediaDecodeTimeV1) 354 | } 355 | 356 | if ms.Init.Timescale == 0 { 357 | return nil, fmt.Errorf("missing timescale") 358 | } 359 | 360 | // Convert to seconds 361 | // TODO What about PTS? 362 | sample.Timestamp = dts * time.Second / time.Duration(ms.Init.Timescale) 363 | } 364 | 365 | // Expands children 366 | return h.Expand() 367 | }) 368 | 369 | if err != nil { 370 | return nil, fmt.Errorf("failed to parse MP4 file: %w", err) 371 | } 372 | 373 | return sample, nil 374 | } 375 | 376 | func (ms *MediaSegment) Close() (err error) { 377 | return ms.file.Close() 378 | } 379 | 380 | type mediaSample struct { 381 | Timestamp time.Duration // The timestamp of the first sample 382 | } 383 | -------------------------------------------------------------------------------- /server/internal/warp/message.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | type Message struct { 4 | Init *MessageInit `json:"init,omitempty"` 5 | Segment *MessageSegment `json:"segment,omitempty"` 6 | Ping *MessagePing `json:"x-ping,omitempty"` 7 | Pong *MessagePong `json:"pong,omitempty"` 8 | Debug *MessageDebug `json:"debug,omitempty"` 9 | Pref *MessagePref `json:"x-pref,omitempty"` 10 | } 11 | 12 | type MessageInit struct { 13 | Id string `json:"id"` // ID of the init segment 14 | } 15 | 16 | type MessageSegment struct { 17 | Init string `json:"init"` // ID of the init segment to use for this segment 18 | Timestamp int `json:"timestamp"` // PTS of the first frame in milliseconds 19 | ETP int `json:"etp"` // Estimated throughput in bytes - CTA 5006 20 | TcRate float64 `json:"tc_rate"` // Applied tc rate 21 | AvailabilityTime int `json:"at"` // The wallclock time at which the first byte of this object became available at the origin for successful request. - CTA 5006 22 | } 23 | 24 | type MessageDebug struct { 25 | MaxBitrate *int `json:"max_bitrate,omitempty"` // Artificially limit the QUIC max bitrate 26 | ContinueStreaming *bool `json:"continue_streaming,omitempty"` // Resume or pause streaming 27 | TcReset *bool `json:"tc_reset,omitempty"` // Set tc profile 28 | } 29 | 30 | type MessagePing struct { 31 | } 32 | 33 | type MessagePong struct { 34 | } 35 | 36 | type MessagePref struct { 37 | Name string `json:"name"` 38 | Value string `json:"value"` 39 | } 40 | -------------------------------------------------------------------------------- /server/internal/warp/server.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/kixelated/invoker" 20 | "github.com/kixelated/quic-go" 21 | "github.com/kixelated/quic-go/http3" 22 | "github.com/kixelated/quic-go/logging" 23 | "github.com/kixelated/quic-go/qlog" 24 | "github.com/kixelated/webtransport-go" 25 | ) 26 | 27 | type Server struct { 28 | inner *webtransport.Server 29 | media *Media 30 | 31 | // The following properties were added to implement tc rate limiting 32 | // tcRate is the Mbps value which is read from a file. 33 | // continueStreaming is a boolean which is set when a user pauses or plays the video 34 | // these two variables are server-scoped meaning they affect other sessions as well. 35 | // Hence, the tests should be conducted by one user. 36 | tcRate float64 37 | isTcActive bool 38 | continueStreaming bool 39 | 40 | sessions invoker.Tasks 41 | } 42 | 43 | type ServerConfig struct { 44 | Addr string 45 | Cert *tls.Certificate 46 | LogDir string 47 | } 48 | 49 | func NewServer(config ServerConfig, media *Media) (s *Server, err error) { 50 | s = new(Server) 51 | 52 | s.continueStreaming = true 53 | s.tcRate = -1 54 | 55 | quicConfig := &quic.Config{} 56 | 57 | if config.LogDir != "" { 58 | quicConfig.Tracer = qlog.NewTracer(func(p logging.Perspective, connectionID []byte) io.WriteCloser { 59 | path := fmt.Sprintf("%s-%s.qlog", p, hex.EncodeToString(connectionID)) 60 | 61 | f, err := os.Create(filepath.Join(config.LogDir, path)) 62 | if err != nil { 63 | // lame 64 | panic(err) 65 | } 66 | 67 | return f 68 | }) 69 | } 70 | 71 | tlsConfig := &tls.Config{ 72 | Certificates: []tls.Certificate{*config.Cert}, 73 | } 74 | 75 | mux := http.NewServeMux() 76 | 77 | s.inner = &webtransport.Server{ 78 | H3: http3.Server{ 79 | TLSConfig: tlsConfig, 80 | QuicConfig: quicConfig, 81 | Addr: config.Addr, 82 | Handler: mux, 83 | }, 84 | CheckOrigin: func(r *http.Request) bool { return true }, 85 | } 86 | 87 | s.media = media 88 | 89 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 90 | hijacker, ok := w.(http3.Hijacker) 91 | if !ok { 92 | panic("unable to hijack connection: must use kixelated/quic-go") 93 | } 94 | 95 | conn := hijacker.Connection() 96 | 97 | s.isTcActive = true 98 | s.tcRate = -1 // reset tc 99 | 100 | // wait for 1 sec for the tc limiting to be applied 101 | time.Sleep(time.Second) 102 | 103 | fmt.Printf("isTcActive: %t rate: %f\n", s.isTcActive, s.tcRate) 104 | 105 | sess, err := s.inner.Upgrade(w, r) 106 | if err != nil { 107 | http.Error(w, "failed to upgrade session", 500) 108 | return 109 | } 110 | 111 | err = s.serve(r.Context(), conn, sess) 112 | if err != nil { 113 | log.Println(err) 114 | } 115 | }) 116 | 117 | return s, nil 118 | } 119 | 120 | func (s *Server) runTcProfile(ctx context.Context) (err error) { 121 | // profiles: profile_cascade, profile_lte, profile_twitch 122 | // set profile name to the one of the options above to run tc netem. 123 | // TODO: get initial value from a configuration file, allow player to set this remotely 124 | profile_name := "" 125 | if profile_name == "" { 126 | return nil 127 | } 128 | 129 | data, err := ioutil.ReadFile("./tc_scripts/" + profile_name) 130 | if err != nil { 131 | fmt.Println(err.Error()) 132 | log.Fatal(err) 133 | } 134 | 135 | lines := strings.Split(string(data), "\n") 136 | i := -1 137 | for i = 0; i < len(lines); i++ { 138 | // don't change tc rate if streaming is paused 139 | if !s.continueStreaming { 140 | i = i - 1 141 | time.Sleep(time.Millisecond * 50) 142 | continue 143 | } 144 | 145 | // -1 means, reset tc 146 | if s.tcRate == -1.0 { 147 | fmt.Printf("resetting tc | isTcActive: %t rate: %f\n", s.isTcActive, s.tcRate) 148 | cmd := exec.Command("bash", "./tc_scripts/tc_reset.sh") 149 | stdout, err := cmd.Output() 150 | if err == nil { 151 | fmt.Printf("tc reset output: %s", string(stdout)) 152 | } else { 153 | fmt.Println(err.Error()) 154 | return err 155 | } 156 | s.tcRate = 0 157 | } 158 | 159 | if !s.isTcActive { 160 | // reset line counter 161 | i = -1 162 | time.Sleep(time.Millisecond * 100) 163 | continue 164 | } 165 | 166 | line := lines[i] 167 | parts := strings.Split(line, " ") 168 | 169 | if len(parts) == 2 { 170 | 171 | action := parts[0] 172 | value := parts[1] 173 | 174 | if action == "rate" { 175 | fmt.Printf("rate %s\n", value) 176 | 177 | if err == nil { 178 | float_val, err := strconv.ParseFloat(value, 64) 179 | 180 | if err == nil { 181 | s.tcRate = float_val / 1024 // Mbps 182 | 183 | // pass Mpbs to the script 184 | cmd := exec.Command("bash", "./tc_scripts/throttle.sh", fmt.Sprintf("%.1f", s.tcRate)) 185 | stdout, err := cmd.Output() 186 | if err == nil { 187 | fmt.Printf("tc command: %s", string(stdout)) 188 | } else { 189 | fmt.Println(err.Error()) 190 | return err 191 | } 192 | 193 | } else { 194 | continue 195 | } 196 | } else { 197 | fmt.Println(err.Error()) 198 | return err 199 | } 200 | 201 | } else if action == "wait" { 202 | fmt.Printf("wait %s\n", value) 203 | if err == nil { 204 | float_val, err := strconv.ParseFloat(value, 64) 205 | if err == nil { 206 | passed_duration_ms := 0.0 207 | sleep_interval := 10 208 | for passed_duration_ms < float_val*1000 { 209 | // if stream is paused, hold tc rate 210 | if s.continueStreaming { 211 | passed_duration_ms += float64(sleep_interval) 212 | } 213 | err = invoker.Sleep(time.Millisecond * time.Duration(sleep_interval))(ctx) 214 | if err != nil { 215 | fmt.Println(err.Error()) 216 | return err 217 | } 218 | 219 | } 220 | 221 | } else { 222 | continue 223 | } 224 | } else { 225 | fmt.Println(err.Error()) 226 | return err 227 | } 228 | } else { 229 | continue 230 | } 231 | } 232 | 233 | if i == len(lines)-1 { 234 | // loop 235 | i = -1 236 | } 237 | } 238 | return nil 239 | } 240 | 241 | func (s *Server) runServe(ctx context.Context) (err error) { 242 | return s.inner.ListenAndServe() 243 | } 244 | 245 | func (s *Server) runShutdown(ctx context.Context) (err error) { 246 | <-ctx.Done() 247 | s.inner.Close() 248 | return ctx.Err() 249 | } 250 | 251 | func (s *Server) Run(ctx context.Context) (err error) { 252 | return invoker.Run(ctx, s.runServe, s.runTcProfile, s.runShutdown, s.sessions.Repeat) 253 | } 254 | 255 | func (s *Server) serve(ctx context.Context, conn quic.Connection, sess *webtransport.Session) (err error) { 256 | defer func() { 257 | if err != nil { 258 | sess.CloseWithError(1, err.Error()) 259 | } else { 260 | sess.CloseWithError(0, "end of broadcast") 261 | } 262 | }() 263 | 264 | ss, err := NewSession(conn, sess, s.media, s) 265 | if err != nil { 266 | return fmt.Errorf("failed to create session: %w", err) 267 | } 268 | 269 | err = ss.Run(ctx) 270 | if err != nil { 271 | return fmt.Errorf("terminated session: %w", err) 272 | } 273 | 274 | return nil 275 | } 276 | -------------------------------------------------------------------------------- /server/internal/warp/session.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math" 12 | "time" 13 | 14 | "github.com/kixelated/invoker" 15 | "github.com/kixelated/quic-go" 16 | "github.com/kixelated/webtransport-go" 17 | ) 18 | 19 | // A single WebTransport session 20 | type Session struct { 21 | conn quic.Connection 22 | inner *webtransport.Session 23 | 24 | media *Media 25 | inits map[string]*MediaInit 26 | audio *MediaStream 27 | video *MediaStream 28 | 29 | server *Server 30 | 31 | streams invoker.Tasks 32 | 33 | prefs map[string]string 34 | 35 | continueStreaming bool 36 | audioTimeOffset time.Duration 37 | videoTimeOffset time.Duration 38 | } 39 | 40 | func NewSession(connection quic.Connection, session *webtransport.Session, media *Media, server *Server) (s *Session, err error) { 41 | s = new(Session) 42 | s.server = server 43 | s.conn = connection 44 | s.inner = session 45 | s.media = media 46 | s.continueStreaming = true 47 | s.server.continueStreaming = true 48 | return s, nil 49 | } 50 | 51 | func (s *Session) Run(ctx context.Context) (err error) { 52 | s.inits, s.audio, s.video, err = s.media.Start(s.conn.GetMaxBandwidth) 53 | s.prefs = make(map[string]string) 54 | if err != nil { 55 | return fmt.Errorf("failed to start media: %w", err) 56 | } 57 | 58 | // Once we've validated the session, now we can start accessing the streams 59 | return invoker.Run(ctx, s.runAccept, s.runAcceptUni, s.runInit, s.runAudio, s.runVideo, s.streams.Repeat) 60 | } 61 | 62 | func (s *Session) runAccept(ctx context.Context) (err error) { 63 | for { 64 | stream, err := s.inner.AcceptStream(ctx) 65 | if err != nil { 66 | return fmt.Errorf("failed to accept bidirectional stream: %w", err) 67 | } 68 | 69 | // Warp doesn't utilize bidirectional streams so just close them immediately. 70 | // We might use them in the future so don't close the connection with an error. 71 | stream.CancelRead(1) 72 | } 73 | } 74 | 75 | func (s *Session) runAcceptUni(ctx context.Context) (err error) { 76 | for { 77 | stream, err := s.inner.AcceptUniStream(ctx) 78 | if err != nil { 79 | return fmt.Errorf("failed to accept unidirectional stream: %w", err) 80 | } 81 | 82 | s.streams.Add(func(ctx context.Context) (err error) { 83 | return s.handleStream(ctx, stream) 84 | }) 85 | } 86 | } 87 | 88 | func (s *Session) handleStream(ctx context.Context, stream webtransport.ReceiveStream) (err error) { 89 | defer func() { 90 | if err != nil { 91 | stream.CancelRead(1) 92 | } 93 | }() 94 | 95 | var header [8]byte 96 | for { 97 | _, err = io.ReadFull(stream, header[:]) 98 | if errors.Is(io.EOF, err) { 99 | return nil 100 | } else if err != nil { 101 | return fmt.Errorf("failed to read atom header: %w", err) 102 | } 103 | 104 | size := binary.BigEndian.Uint32(header[0:4]) 105 | name := string(header[4:8]) 106 | 107 | if size < 8 { 108 | return fmt.Errorf("atom size is too small") 109 | } else if size > 42069 { // arbitrary limit 110 | return fmt.Errorf("atom size is too large") 111 | } else if name != "warp" { 112 | return fmt.Errorf("only warp atoms are supported") 113 | } 114 | 115 | payload := make([]byte, size-8) 116 | 117 | _, err = io.ReadFull(stream, payload) 118 | if err != nil { 119 | return fmt.Errorf("failed to read atom payload: %w", err) 120 | } 121 | 122 | log.Println("received message:", string(payload)) 123 | 124 | msg := Message{} 125 | 126 | err = json.Unmarshal(payload, &msg) 127 | if err != nil { 128 | return fmt.Errorf("failed to decode json payload: %w", err) 129 | } 130 | 131 | if msg.Debug != nil { 132 | s.setDebug(msg.Debug) 133 | } 134 | 135 | if msg.Pref != nil { 136 | fmt.Printf("* Pref received name: %s value: %s\n", msg.Pref.Name, msg.Pref.Value) 137 | s.setPref(msg.Pref) 138 | } 139 | 140 | if msg.Ping != nil { 141 | println("Ping received") 142 | err := s.sendPong(msg.Ping, ctx) 143 | if err != nil { 144 | return err 145 | } 146 | } 147 | } 148 | } 149 | 150 | func (s *Session) runInit(ctx context.Context) (err error) { 151 | for _, init := range s.inits { 152 | err = s.writeInit(ctx, init) 153 | if err != nil { 154 | return fmt.Errorf("failed to write init stream: %w", err) 155 | } 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func (s *Session) runAudio(ctx context.Context) (err error) { 162 | start := time.Now() 163 | for { 164 | if !s.continueStreaming { 165 | // Sleep to let cpu off 166 | err := invoker.Sleep(10 * time.Millisecond)(ctx) 167 | if err != nil { 168 | return fmt.Errorf("failed in runAudio: %w", err) 169 | } 170 | s.audioTimeOffset += time.Since(start) 171 | continue 172 | } else { 173 | // reset start 174 | start = time.Now() 175 | } 176 | 177 | segment, err := s.audio.Next(ctx, s, s.audioTimeOffset) 178 | if err != nil { 179 | return fmt.Errorf("failed to get next segment: %w", err) 180 | } 181 | 182 | if segment == nil { 183 | return nil 184 | } 185 | 186 | err = s.writeSegment(ctx, segment) 187 | if err != nil { 188 | return fmt.Errorf("failed to write segment stream: %w", err) 189 | } 190 | } 191 | } 192 | 193 | func (s *Session) runVideo(ctx context.Context) (err error) { 194 | start := time.Now() 195 | for { 196 | if !s.continueStreaming { 197 | // Sleep to let cpu off 198 | err := invoker.Sleep(10 * time.Millisecond)(ctx) 199 | if err != nil { 200 | return fmt.Errorf("failed in runAudio: %w", err) 201 | } 202 | s.videoTimeOffset += time.Since(start) 203 | continue 204 | } else { 205 | // reset start 206 | start = time.Now() 207 | } 208 | 209 | segment, err := s.video.Next(ctx, s, s.videoTimeOffset) 210 | if err != nil { 211 | return fmt.Errorf("failed to get next segment: %w", err) 212 | } 213 | 214 | if segment == nil { 215 | return nil 216 | } 217 | 218 | err = s.writeSegment(ctx, segment) 219 | if err != nil { 220 | return fmt.Errorf("failed to write segment stream: %w", err) 221 | } 222 | } 223 | } 224 | 225 | // Create a stream for an INIT segment and write the container. 226 | func (s *Session) writeInit(ctx context.Context, init *MediaInit) (err error) { 227 | temp, err := s.inner.OpenUniStreamSync(ctx) 228 | if err != nil { 229 | return fmt.Errorf("failed to create stream: %w", err) 230 | } 231 | 232 | // Wrap the stream in an object that buffers writes instead of blocking. 233 | stream := NewStream(temp) 234 | s.streams.Add(stream.Run) 235 | 236 | defer func() { 237 | if err != nil { 238 | stream.WriteCancel(1) 239 | } 240 | }() 241 | 242 | stream.SetPriority(math.MaxInt) 243 | 244 | err = stream.WriteMessage(Message{ 245 | Init: &MessageInit{Id: init.ID}, 246 | }) 247 | 248 | if err != nil { 249 | return fmt.Errorf("failed to write init header: %w", err) 250 | } 251 | 252 | _, err = stream.Write(init.Raw) 253 | if err != nil { 254 | return fmt.Errorf("failed to write init data: %w", err) 255 | } 256 | 257 | return nil 258 | } 259 | 260 | // Create a stream for a segment and write the contents, chunk by chunk. 261 | func (s *Session) writeSegment(ctx context.Context, segment *MediaSegment) (err error) { 262 | temp, err := s.inner.OpenUniStreamSync(ctx) 263 | if err != nil { 264 | return fmt.Errorf("failed to create stream: %w", err) 265 | } 266 | 267 | // Wrap the stream in an object that buffers writes instead of blocking. 268 | stream := NewStream(temp) 269 | s.streams.Add(stream.Run) 270 | 271 | defer func() { 272 | if err != nil { 273 | stream.WriteCancel(1) 274 | } 275 | }() 276 | 277 | ms := int(segment.timestamp / time.Millisecond) 278 | 279 | // newer segments take priority 280 | stream.SetPriority(ms) 281 | 282 | tcRate := s.server.tcRate 283 | if tcRate == -1 { 284 | tcRate = 0 285 | } 286 | 287 | init_message := Message{ 288 | Segment: &MessageSegment{ 289 | Init: segment.Init.ID, 290 | Timestamp: ms, 291 | ETP: int(s.conn.GetMaxBandwidth() / 1024), 292 | TcRate: tcRate * 1024, 293 | AvailabilityTime: int(time.Now().UnixMilli()), 294 | }, 295 | } 296 | /* 297 | 298 | Segments on the Wire 299 | ------------------------------------------------------ 300 | [chunk_S1_N] ... [chunk_S1_1] [segment 1 init] 301 | ------------------------------------------------------ 302 | 303 | Stream multiplexing in QUIC: 304 | ----------------------------------------------------------- 305 | [chunk_S1_N] ..[chunk_S2_M] .. [chunk_S1_2]...[chunk_S1_1] 306 | ---------------------------------------------------------- 307 | 308 | Head of Line Blocking Problem in TCP: 309 | ------------------------------------ 310 | TCP Buffer 311 | Pipeline 312 | | x | c_s1_1 Head of line blocking 313 | | c_s1_2 | 314 | | c_s1_3 | 315 | | c_s2_1 | 316 | | c_s1_4 | 317 | 318 | Quic treats each stream differently 319 | ----------------------------------- 320 | Stream 1 321 | | x | 322 | | c_s1_2 | 323 | | c_s1_3 | 324 | | c_s1_4 | 325 | 326 | Stream 2 327 | | c_s2_1 | 328 | | | 329 | 330 | */ 331 | 332 | err = stream.WriteMessage(init_message) 333 | 334 | if err != nil { 335 | return fmt.Errorf("failed to write segment header: %w", err) 336 | } 337 | 338 | segment_size := 0 339 | box_count := 0 340 | chunk_count := 0 341 | 342 | print_moof_sizes := false 343 | 344 | last_moof_size := 0 345 | 346 | for { 347 | // Get the next fragment 348 | start := time.Now().UnixMilli() 349 | 350 | buf, err := segment.Read(ctx) 351 | if errors.Is(err, io.EOF) { 352 | break 353 | } else if err != nil { 354 | return fmt.Errorf("failed to read segment data: %w", err) 355 | } 356 | 357 | segment_size += len(buf) 358 | box_count++ 359 | 360 | if print_moof_sizes { 361 | if string(buf[4:8]) == "moof" { 362 | last_moof_size = len(buf) 363 | chunk_count++ 364 | } else if string(buf[4:8]) == "mdat" { 365 | chunk_size := last_moof_size + len(buf) 366 | fmt.Printf("* chunk: %d size: %d time offset: %d\n", chunk_count, chunk_size, time.Now().UnixMilli()-start) 367 | } 368 | } 369 | 370 | // NOTE: This won't block because of our wrapper 371 | _, err = stream.Write(buf) 372 | if err != nil { 373 | return fmt.Errorf("failed to write segment data: %w", err) 374 | } 375 | 376 | } 377 | 378 | // for debug purposes 379 | fmt.Printf("* id: %s ts: %d etp: %d segment size: %d box count:%d chunk count: %d\n", init_message.Segment.Init, init_message.Segment.Timestamp, init_message.Segment.ETP, segment_size, box_count, chunk_count) 380 | 381 | err = stream.Close() 382 | if err != nil { 383 | return fmt.Errorf("failed to close segemnt stream: %w", err) 384 | } 385 | 386 | return nil 387 | } 388 | 389 | func (s *Session) setDebug(msg *MessageDebug) { 390 | if msg.MaxBitrate != nil { 391 | s.conn.SetMaxBandwidth(uint64(*msg.MaxBitrate)) 392 | } else if msg.ContinueStreaming != nil { 393 | s.continueStreaming = *msg.ContinueStreaming 394 | s.server.continueStreaming = *msg.ContinueStreaming 395 | } else if *msg.TcReset { 396 | // setting tcRate to -1 is a signal to reset tc rate 397 | s.server.tcRate = -1 398 | s.server.isTcActive = false 399 | s.server.continueStreaming = true 400 | } 401 | } 402 | 403 | func (s *Session) setPref(msg *MessagePref) { 404 | s.prefs[msg.Name] = msg.Value 405 | } 406 | 407 | func (s *Session) sendPong(msg *MessagePing, ctx context.Context) (err error) { 408 | temp, err := s.inner.OpenUniStreamSync(ctx) 409 | if err != nil { 410 | return fmt.Errorf("failed to create stream: %w", err) 411 | } 412 | 413 | // Wrap the stream in an object that buffers writes instead of blocking. 414 | stream := NewStream(temp) 415 | s.streams.Add(stream.Run) 416 | 417 | defer func() { 418 | if err != nil { 419 | stream.WriteCancel(1) 420 | } 421 | }() 422 | 423 | err = stream.WriteMessage( 424 | Message{ 425 | Pong: &MessagePong{}, 426 | }) 427 | if err != nil { 428 | return fmt.Errorf("failed to write init header: %w", err) 429 | } 430 | return nil 431 | } 432 | -------------------------------------------------------------------------------- /server/internal/warp/stream.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/json" 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/kixelated/webtransport-go" 11 | ) 12 | 13 | // Wrapper around quic.SendStream to make Write non-blocking. 14 | // Otherwise we can't write to multiple concurrent streams in the same goroutine. 15 | type Stream struct { 16 | inner webtransport.SendStream 17 | 18 | chunks [][]byte 19 | closed bool 20 | err error 21 | 22 | notify chan struct{} 23 | mutex sync.Mutex 24 | } 25 | 26 | func NewStream(inner webtransport.SendStream) (s *Stream) { 27 | s = new(Stream) 28 | s.inner = inner 29 | s.notify = make(chan struct{}) 30 | return s 31 | } 32 | 33 | func (s *Stream) Run(ctx context.Context) (err error) { 34 | defer func() { 35 | s.mutex.Lock() 36 | s.err = err 37 | s.mutex.Unlock() 38 | }() 39 | 40 | for { 41 | s.mutex.Lock() 42 | 43 | chunks := s.chunks 44 | notify := s.notify 45 | closed := s.closed 46 | 47 | s.chunks = s.chunks[len(s.chunks):] 48 | s.mutex.Unlock() 49 | 50 | for _, chunk := range chunks { 51 | _, err = s.inner.Write(chunk) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | 57 | if closed { 58 | return s.inner.Close() 59 | } 60 | 61 | if len(chunks) == 0 { 62 | select { 63 | case <-ctx.Done(): 64 | return ctx.Err() 65 | case <-notify: 66 | } 67 | } 68 | } 69 | } 70 | 71 | func (s *Stream) Write(buf []byte) (n int, err error) { 72 | s.mutex.Lock() 73 | defer s.mutex.Unlock() 74 | 75 | if s.err != nil { 76 | return 0, s.err 77 | } 78 | 79 | if s.closed { 80 | return 0, fmt.Errorf("closed") 81 | } 82 | 83 | // Make a copy of the buffer so it's long lived 84 | buf = append([]byte{}, buf...) 85 | s.chunks = append(s.chunks, buf) 86 | 87 | // Wake up the writer 88 | close(s.notify) 89 | s.notify = make(chan struct{}) 90 | 91 | return len(buf), nil 92 | } 93 | 94 | func (s *Stream) WriteMessage(msg Message) (err error) { 95 | payload, err := json.Marshal(msg) 96 | if err != nil { 97 | return fmt.Errorf("failed to marshal message: %w", err) 98 | } 99 | 100 | var size [4]byte 101 | binary.BigEndian.PutUint32(size[:], uint32(len(payload)+8)) 102 | 103 | _, err = s.Write(size[:]) 104 | if err != nil { 105 | return fmt.Errorf("failed to write size: %w", err) 106 | } 107 | 108 | _, err = s.Write([]byte("warp")) 109 | if err != nil { 110 | return fmt.Errorf("failed to write atom header: %w", err) 111 | } 112 | 113 | _, err = s.Write(payload) 114 | if err != nil { 115 | return fmt.Errorf("failed to write payload: %w", err) 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (s *Stream) WriteCancel(code webtransport.StreamErrorCode) { 122 | s.inner.CancelWrite(code) 123 | } 124 | 125 | func (s *Stream) SetPriority(prio int) { 126 | s.inner.SetPriority(prio) 127 | } 128 | 129 | func (s *Stream) Close() (err error) { 130 | s.mutex.Lock() 131 | defer s.mutex.Unlock() 132 | 133 | if s.err != nil { 134 | return s.err 135 | } 136 | 137 | s.closed = true 138 | 139 | // Wake up the writer 140 | close(s.notify) 141 | s.notify = make(chan struct{}) 142 | 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/kixelated/invoker" 11 | "github.com/kixelated/warp-demo/server/internal/warp" 12 | ) 13 | 14 | func main() { 15 | err := run(context.Background()) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | func run(ctx context.Context) (err error) { 22 | addr := flag.String("addr", ":4443", "HTTPS server address") 23 | cert := flag.String("tls-cert", "../cert/cert.pem", "TLS certificate file path") 24 | key := flag.String("tls-key", "../cert/privkey.pem", "TLS certificate file path") 25 | logDir := flag.String("log-dir", "", "logs will be written to the provided directory") 26 | 27 | dash := flag.String("dash", "../media/playlist.mpd", "DASH playlist path") 28 | 29 | flag.Parse() 30 | 31 | media, err := warp.NewMedia(*dash) 32 | if err != nil { 33 | return fmt.Errorf("failed to open media: %w", err) 34 | } 35 | 36 | tlsCert, err := tls.LoadX509KeyPair(*cert, *key) 37 | if err != nil { 38 | return fmt.Errorf("failed to load TLS certificate: %w", err) 39 | } 40 | 41 | config := warp.ServerConfig{ 42 | Addr: *addr, 43 | Cert: &tlsCert, 44 | LogDir: *logDir, 45 | } 46 | 47 | ws, err := warp.NewServer(config, media) 48 | if err != nil { 49 | return fmt.Errorf("failed to create warp server: %w", err) 50 | } 51 | 52 | log.Printf("listening on %s", *addr) 53 | 54 | return invoker.Run(ctx, invoker.Interrupt, ws.Run) 55 | } 56 | -------------------------------------------------------------------------------- /server/start.sh: -------------------------------------------------------------------------------- 1 | source .env 2 | if [[ -z $SERVER_LISTEN_ADDRESS ]]; then 3 | SERVER_LISTEN_ADDRESS=0.0.0.0:8443 4 | fi 5 | 6 | if [[ ! -d ./logs ]]; then 7 | mkdir logs 8 | fi 9 | 10 | echo "Starting server on $SERVER_LISTEN_ADDRESS" 11 | sudo sysctl -w net.core.rmem_max=2500000 && /usr/local/go/bin/go run main.go -log-dir ./logs -addr $SERVER_LISTEN_ADDRESS -------------------------------------------------------------------------------- /server/tc_scripts/profile_cascade: -------------------------------------------------------------------------------- 1 | rate 1200 2 | wait 30 3 | rate 800 4 | wait 30 5 | rate 400 6 | wait 30 7 | rate 800 8 | wait 30 9 | rate 1200 10 | wait 30 11 | -------------------------------------------------------------------------------- /server/tc_scripts/profile_fast_jitters: -------------------------------------------------------------------------------- 1 | PROFILE_FAST_JITTERS = [ 2 | { 3 | speed: 500, 4 | duration: 0.250, 5 | }, 6 | { 7 | speed: 1200, 8 | duration: 5, 9 | }, 10 | { 11 | speed: 500, 12 | duration: 0.1, 13 | }, 14 | { 15 | speed: 1200, 16 | duration: 1, 17 | }, 18 | { 19 | speed: 500, 20 | duration: 0.250, 21 | }, 22 | { 23 | speed: 1200, 24 | duration: 5, 25 | }, 26 | ]; -------------------------------------------------------------------------------- /server/tc_scripts/profile_intra_cascade: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streaming-university/public-moq-demo/2071137a5967e645ab97b6e914dc4f30c8c47d95/server/tc_scripts/profile_intra_cascade -------------------------------------------------------------------------------- /server/tc_scripts/profile_lte: -------------------------------------------------------------------------------- 1 | rate 693 2 | wait 1 3 | rate 1208 4 | wait 1 5 | rate 1496 6 | wait 1 7 | rate 891 8 | wait 1 9 | rate 2624 10 | wait 1 11 | rate 1370 12 | wait 1 13 | rate 1537 14 | wait 1 15 | rate 1566 16 | wait 1 17 | rate 4046 18 | wait 1 19 | rate 2590 20 | wait 1 21 | rate 2813 22 | wait 1 23 | rate 3539 24 | wait 1 25 | rate 2033 26 | wait 1 27 | rate 1014 28 | wait 1 29 | rate 1875 30 | wait 1 31 | rate 3163 32 | wait 1 33 | rate 1362 34 | wait 1 35 | rate 1787 36 | wait 1 37 | rate 2566 38 | wait 1 39 | rate 3392 40 | wait 1 41 | rate 2758 42 | wait 1 43 | rate 5188 44 | wait 1 45 | rate 5106 46 | wait 1 47 | rate 6575 48 | wait 1 49 | rate 6557 50 | wait 1 51 | rate 4684 52 | wait 1 53 | rate 1737 54 | wait 1 55 | rate 1148 56 | wait 1 57 | rate 5168 58 | wait 1 59 | rate 6224 60 | wait 1 61 | rate 5358 62 | wait 1 63 | rate 4200 64 | wait 1 65 | rate 1563 66 | wait 1 67 | rate 4428 68 | wait 1 69 | rate 2541 70 | wait 1 71 | rate 235 72 | wait 1 73 | rate 2532 74 | wait 1 75 | rate 1977 76 | wait 1 77 | rate 887 78 | wait 1 79 | rate 1015 80 | wait 1 81 | rate 1281 82 | wait 1 83 | rate 850 84 | wait 1 85 | rate 991 86 | wait 1 87 | rate 417 88 | wait 1 89 | rate 503 90 | wait 1 91 | rate 613 92 | wait 1 93 | rate 1088 94 | wait 1 95 | rate 3107 96 | wait 1 97 | rate 4133 98 | wait 1 99 | rate 3768 100 | wait 1 101 | rate 2970 102 | wait 1 103 | rate 3578 104 | wait 1 105 | rate 2480 106 | wait 1 107 | rate 2330 108 | wait 1 109 | rate 2501 110 | wait 1 111 | rate 2425 112 | wait 1 113 | rate 2619 114 | wait 1 115 | rate 4849 116 | wait 1 117 | rate 4248 118 | wait 1 119 | rate 4604 120 | wait 1 121 | rate 4882 122 | wait 1 123 | rate 4669 124 | wait 1 125 | rate 3506 126 | wait 1 127 | rate 1589 128 | wait 1 129 | rate 7086 130 | wait 1 131 | rate 7569 132 | wait 1 133 | rate 4642 134 | wait 1 135 | rate 3961 136 | wait 1 137 | rate 3505 138 | wait 1 139 | rate 2316 140 | wait 1 141 | rate 3957 142 | wait 1 143 | rate 5290 144 | wait 1 145 | rate 4554 146 | wait 1 147 | rate 4830 148 | wait 1 149 | rate 3942 150 | wait 1 151 | rate 4253 152 | wait 1 153 | rate 3917 154 | wait 1 155 | rate 3481 156 | wait 1 157 | rate 6457 158 | wait 1 159 | rate 5634 160 | wait 1 161 | rate 4860 162 | wait 1 163 | rate 4906 164 | wait 1 165 | rate 4073 166 | wait 1 167 | rate 5197 168 | wait 1 169 | rate 1839 170 | wait 1 171 | rate 6822 172 | wait 1 173 | rate 6068 174 | wait 1 175 | rate 3313 176 | wait 1 177 | rate 2943 178 | wait 1 179 | rate 3648 180 | wait 1 181 | rate 3085 182 | wait 1 183 | rate 2550 184 | wait 1 185 | rate 2596 186 | wait 1 187 | rate 2393 188 | wait 1 189 | rate 2015 190 | wait 1 191 | rate 3069 192 | wait 1 193 | rate 2737 194 | wait 1 195 | rate 856 196 | wait 1 197 | rate 1264 198 | wait 1 199 | rate 1827 200 | wait 1 201 | rate 3503 202 | wait 1 203 | rate 2497 204 | wait 1 205 | rate 2601 206 | wait 1 207 | rate 4287 208 | wait 1 209 | rate 4130 210 | wait 1 211 | rate 2219 212 | wait 1 213 | rate 3460 214 | wait 1 215 | rate 4906 216 | wait 1 217 | rate 5746 218 | wait 1 219 | rate 5863 220 | wait 1 221 | rate 5786 222 | wait 1 223 | rate 6777 224 | wait 1 225 | rate 3238 226 | wait 1 227 | rate 4151 228 | wait 1 229 | rate 6081 230 | wait 1 231 | rate 4198 232 | wait 1 233 | rate 5497 234 | wait 1 235 | rate 3597 236 | wait 1 237 | rate 5485 238 | wait 1 239 | rate 4777 240 | wait 1 241 | rate 4625 242 | wait 1 243 | rate 4541 244 | wait 1 245 | rate 3194 246 | wait 1 247 | rate 5389 248 | wait 1 249 | rate 4849 250 | wait 1 251 | rate 5170 252 | wait 1 253 | rate 4772 254 | wait 1 255 | rate 3363 256 | wait 1 257 | rate 4982 258 | wait 1 259 | rate 4671 260 | wait 1 261 | rate 6679 262 | wait 1 263 | rate 4481 264 | wait 1 265 | rate 4420 266 | wait 1 267 | rate 3727 268 | wait 1 269 | rate 5800 270 | wait 1 271 | rate 5666 272 | wait 1 273 | rate 4044 274 | wait 1 275 | rate 4984 276 | wait 1 277 | rate 3895 278 | wait 1 279 | rate 5075 280 | wait 1 281 | rate 2355 282 | wait 1 283 | rate 2439 284 | wait 1 285 | rate 1608 286 | wait 1 287 | rate 800 288 | wait 1 289 | rate 800 290 | wait 1 291 | rate 800 292 | wait 1 293 | rate 800 294 | wait 1 295 | rate 3601 296 | wait 1 297 | rate 664 298 | wait 1 299 | rate 764 300 | wait 1 301 | rate 781 302 | wait 1 303 | rate 2390 304 | wait 1 305 | rate 544 306 | wait 1 307 | rate 899 308 | wait 1 309 | rate 2247 310 | wait 1 311 | rate 2837 312 | wait 1 313 | rate 1689 314 | wait 1 315 | rate 2106 316 | wait 1 317 | rate 3321 318 | wait 1 319 | rate 1401 320 | wait 1 321 | rate 340 322 | wait 1 323 | rate 329 324 | wait 1 325 | rate 121 326 | wait 1 327 | rate 22 328 | wait 1 329 | rate 303 330 | wait 1 331 | rate 389 332 | wait 1 333 | rate 120 334 | wait 1 335 | rate 8 336 | wait 1 337 | rate 47 338 | wait 1 339 | rate 920 340 | wait 1 341 | rate 124 342 | wait 1 343 | rate 11 344 | wait 1 345 | rate 85 346 | wait 1 347 | rate 39 348 | wait 1 349 | rate 39 350 | wait 1 351 | rate 39 352 | wait 1 353 | rate 39 354 | wait 1 355 | rate 194 356 | wait 1 357 | rate 5225 358 | wait 1 359 | rate 3820 360 | wait 1 361 | rate 4770 362 | wait 1 363 | rate 3614 364 | wait 1 365 | rate 4837 366 | wait 1 367 | rate 4931 368 | wait 1 369 | rate 4023 370 | wait 1 371 | rate 4234 372 | wait 1 373 | rate 4303 374 | wait 1 375 | rate 2143 376 | wait 1 377 | rate 2180 378 | wait 1 379 | rate 3072 380 | wait 1 381 | rate 828 382 | wait 1 383 | rate 2177 384 | wait 1 385 | rate 1892 386 | wait 1 387 | rate 855 388 | wait 1 389 | rate 2338 390 | wait 1 391 | rate 1755 392 | wait 1 393 | rate 2313 394 | wait 1 395 | rate 1523 396 | wait 1 397 | rate 428 398 | wait 1 399 | rate 203 400 | wait 1 401 | rate 728 402 | wait 1 403 | rate 328 404 | wait 1 405 | rate 3903 406 | wait 1 407 | rate 2499 408 | wait 1 409 | rate 3864 410 | wait 1 411 | rate 3351 412 | wait 1 413 | rate 4325 414 | wait 1 415 | rate 3951 416 | wait 1 417 | rate 1876 418 | wait 1 419 | rate 1582 420 | wait 1 421 | rate 1624 422 | wait 1 423 | rate 865 424 | wait 1 425 | rate 477 426 | wait 1 427 | rate 477 428 | wait 1 429 | rate 477 430 | wait 1 431 | rate 477 432 | wait 1 433 | rate 692 434 | wait 1 435 | rate 5396 436 | wait 1 437 | rate 3864 438 | wait 1 439 | rate 5317 440 | wait 1 441 | rate 3542 442 | wait 1 443 | rate 6376 444 | wait 1 445 | rate 3918 446 | wait 1 447 | rate 4299 448 | wait 1 449 | rate 4002 450 | wait 1 451 | rate 4674 452 | wait 1 453 | rate 5747 454 | wait 1 455 | rate 2925 456 | wait 1 457 | rate 1498 458 | wait 1 459 | rate 4018 460 | wait 1 461 | rate 4206 462 | wait 1 463 | rate 3494 464 | wait 1 465 | rate 2168 466 | wait 1 467 | rate 4291 468 | wait 1 469 | rate 3843 470 | wait 1 471 | rate 3536 472 | wait 1 473 | rate 4461 474 | wait 1 475 | rate 6009 476 | wait 1 477 | rate 6853 478 | wait 1 479 | rate 3979 480 | wait 1 481 | rate 5464 482 | wait 1 483 | rate 5033 484 | wait 1 485 | rate 3843 486 | wait 1 487 | rate 3049 488 | wait 1 489 | rate 3162 490 | wait 1 491 | rate 3252 492 | wait 1 493 | rate 2864 494 | wait 1 495 | rate 2961 496 | wait 1 497 | rate 2469 498 | wait 1 499 | rate 2033 500 | wait 1 501 | rate 2072 502 | wait 1 503 | rate 872 504 | wait 1 505 | rate 2401 506 | wait 1 507 | rate 2480 508 | wait 1 509 | rate 4526 510 | wait 1 511 | rate 3205 512 | wait 1 513 | rate 2562 514 | wait 1 515 | rate 2833 516 | wait 1 517 | rate 1915 518 | wait 1 519 | rate 1817 520 | wait 1 521 | rate 845 522 | wait 1 523 | rate 18 524 | wait 1 525 | rate 18 526 | wait 1 527 | rate 335 528 | wait 1 529 | rate 3067 530 | wait 1 531 | rate 3067 532 | wait 1 533 | rate 247 534 | wait 1 535 | rate 3406 536 | wait 1 537 | rate 820 538 | wait 1 539 | rate 488 540 | wait 1 541 | rate 2334 542 | wait 1 543 | rate 1132 544 | wait 1 545 | rate 675 546 | wait 1 547 | rate 884 548 | wait 1 549 | rate 3045 550 | wait 1 551 | rate 2895 552 | wait 1 553 | rate 791 554 | wait 1 555 | rate 616 556 | wait 1 557 | rate 338 558 | wait 1 559 | rate 419 560 | wait 1 561 | rate 578 562 | wait 1 563 | rate 815 564 | wait 1 565 | rate 721 566 | wait 1 567 | rate 598 568 | wait 1 569 | rate 900 570 | wait 1 571 | rate 1274 572 | wait 1 573 | rate 874 574 | wait 1 575 | rate 1402 576 | wait 1 577 | rate 1704 578 | wait 1 579 | rate 2162 580 | wait 1 581 | rate 1570 582 | wait 1 583 | rate 2219 584 | wait 1 585 | rate 3420 586 | wait 1 587 | rate 3915 588 | wait 1 589 | rate 4575 590 | wait 1 591 | rate 3874 592 | wait 1 593 | rate 2638 594 | wait 1 595 | rate 1388 596 | wait 1 597 | rate 1608 598 | wait 1 599 | rate 2773 600 | wait 1 601 | rate 1242 602 | wait 1 603 | rate 1224 604 | wait 1 605 | rate 2073 606 | wait 1 607 | rate 3226 608 | wait 1 609 | rate 2437 610 | wait 1 611 | rate 3308 612 | wait 1 613 | rate 1962 614 | wait 1 615 | rate 771 616 | wait 1 617 | rate 405 618 | wait 1 619 | rate 876 620 | wait 1 621 | rate 896 622 | wait 1 623 | rate 1325 624 | wait 1 625 | rate 1590 626 | wait 1 627 | rate 827 628 | wait 1 629 | rate 1341 630 | wait 1 631 | rate 1440 632 | wait 1 633 | rate 1440 634 | wait 1 635 | rate 69 636 | wait 1 637 | rate 3453 638 | wait 1 639 | rate 4905 640 | wait 1 641 | rate 3695 642 | wait 1 643 | rate 2203 644 | wait 1 645 | rate 3264 646 | wait 1 647 | rate 2917 648 | wait 1 649 | rate 2811 650 | wait 1 651 | rate 5188 652 | wait 1 653 | rate 4855 654 | wait 1 655 | rate 2909 656 | wait 1 657 | rate 1943 658 | wait 1 659 | rate 3245 660 | wait 1 661 | rate 1516 662 | wait 1 663 | rate 2183 664 | wait 1 665 | rate 1323 666 | wait 1 667 | rate 2253 668 | wait 1 669 | rate 2235 670 | wait 1 671 | rate 1916 672 | wait 1 673 | rate 1655 674 | wait 1 675 | rate 2656 676 | wait 1 677 | rate 4314 678 | wait 1 679 | rate 3214 680 | wait 1 681 | rate 3963 682 | wait 1 683 | rate 2939 684 | wait 1 685 | rate 5132 686 | wait 1 687 | rate 2330 688 | wait 1 689 | rate 1555 690 | wait 1 691 | rate 2687 692 | wait 1 693 | rate 4240 694 | wait 1 695 | rate 2678 696 | wait 1 697 | rate 5395 698 | wait 1 699 | rate 4863 700 | wait 1 701 | rate 4190 702 | wait 1 703 | rate 4484 704 | wait 1 705 | rate 3808 706 | wait 1 707 | rate 3144 708 | wait 1 709 | rate 4501 710 | wait 1 711 | rate 3478 712 | wait 1 713 | rate 4416 714 | wait 1 715 | rate 3105 716 | wait 1 717 | rate 4026 718 | wait 1 719 | rate 4619 720 | wait 1 721 | rate 2955 722 | wait 1 723 | rate 4802 724 | wait 1 725 | rate 4049 726 | wait 1 727 | rate 5244 728 | wait 1 729 | rate 7086 730 | wait 1 731 | rate 4491 732 | wait 1 733 | rate 4090 734 | wait 1 735 | rate 7144 736 | wait 1 737 | rate 5474 738 | wait 1 739 | rate 4374 740 | wait 1 741 | rate 3601 742 | wait 1 743 | rate 5039 744 | wait 1 745 | rate 3976 746 | wait 1 747 | rate 3772 748 | wait 1 749 | rate 3879 750 | wait 1 751 | rate 3638 752 | wait 1 753 | rate 4023 754 | wait 1 755 | rate 3676 756 | wait 1 757 | rate 3375 758 | wait 1 759 | rate 2653 760 | wait 1 761 | rate 1756 762 | wait 1 763 | rate 1916 764 | wait 1 765 | rate 815 766 | wait 1 767 | rate 815 768 | wait 1 769 | rate 1135 770 | wait 1 771 | rate 120 772 | wait 1 773 | rate 120 774 | wait 1 775 | rate 3085 776 | wait 1 777 | rate 1817 778 | wait 1 779 | rate 2724 780 | wait 1 781 | rate 1987 782 | wait 1 783 | rate 876 784 | wait 1 785 | rate 2378 786 | wait 1 787 | rate 1988 788 | wait 1 789 | rate 2383 790 | wait 1 791 | rate 2346 792 | wait 1 793 | rate 2023 794 | wait 1 795 | rate 3505 796 | wait 1 797 | rate 3578 798 | wait 1 799 | rate 1953 800 | wait 1 801 | rate 1908 802 | wait 1 803 | rate 2694 804 | wait 1 805 | rate 2398 806 | wait 1 807 | rate 2373 808 | wait 1 809 | rate 2580 810 | wait 1 811 | rate 2824 812 | wait 1 813 | rate 2074 814 | wait 1 815 | rate 2613 816 | wait 1 817 | rate 2254 818 | wait 1 819 | rate 2529 820 | wait 1 821 | rate 1510 822 | wait 1 823 | rate 973 824 | wait 1 825 | rate 520 826 | wait 1 827 | rate 776 828 | wait 1 829 | rate 301 830 | wait 1 831 | rate 144 832 | wait 1 833 | rate 508 834 | wait 1 835 | rate 576 836 | wait 1 837 | rate 1275 838 | wait 1 839 | rate 1876 840 | wait 1 841 | rate 2138 842 | wait 1 843 | rate 1433 844 | wait 1 845 | rate 1224 846 | wait 1 847 | rate 960 848 | wait 1 849 | rate 2422 850 | wait 1 851 | rate 1827 852 | wait 1 853 | rate 2433 854 | wait 1 855 | rate 2945 856 | wait 1 857 | rate 3360 858 | wait 1 859 | rate 2849 860 | wait 1 861 | rate 2056 862 | wait 1 863 | rate 2611 864 | wait 1 865 | rate 2274 866 | wait 1 867 | rate 2012 868 | wait 1 869 | rate 1998 870 | wait 1 871 | rate 2647 872 | wait 1 873 | rate 2172 874 | wait 1 875 | rate 2575 876 | wait 1 877 | rate 2398 878 | wait 1 879 | rate 3442 880 | wait 1 881 | rate 2633 882 | wait 1 883 | rate 1069 884 | wait 1 885 | rate 1212 886 | wait 1 887 | rate 1472 888 | wait 1 889 | rate 199 890 | wait 1 891 | rate 2170 892 | wait 1 893 | rate 233 894 | wait 1 895 | rate 1591 896 | wait 1 897 | rate 2069 898 | wait 1 899 | rate 1733 900 | wait 1 901 | rate 2254 902 | wait 1 903 | rate 2167 904 | wait 1 905 | rate 2264 906 | wait 1 907 | rate 1343 908 | wait 1 909 | rate 3402 910 | wait 1 911 | rate 2365 912 | wait 1 913 | rate 1125 914 | wait 1 915 | rate 7008 916 | wait 1 917 | rate 2684 918 | wait 1 919 | rate 3804 920 | wait 1 921 | rate 2670 922 | wait 1 923 | rate 4154 924 | wait 1 925 | rate 3311 926 | wait 1 927 | rate 4237 928 | wait 1 929 | rate 4055 930 | wait 1 931 | rate 4182 932 | wait 1 933 | rate 3899 934 | wait 1 935 | rate 4096 936 | wait 1 937 | rate 3607 938 | wait 1 939 | rate 4687 940 | wait 1 941 | rate 4165 942 | wait 1 943 | rate 3630 944 | wait 1 945 | rate 4959 946 | wait 1 947 | rate 4680 948 | wait 1 949 | rate 3237 950 | wait 1 951 | rate 5280 952 | wait 1 953 | rate 3764 954 | wait 1 955 | rate 3776 956 | wait 1 957 | rate 2280 958 | wait 1 959 | rate 4496 960 | wait 1 961 | rate 3791 962 | wait 1 963 | rate 3994 964 | wait 1 965 | rate 5692 966 | wait 1 967 | rate 5422 968 | wait 1 969 | rate 5629 970 | wait 1 971 | rate 3578 972 | wait 1 973 | rate 4140 974 | wait 1 975 | rate 2809 976 | wait 1 977 | rate 5159 978 | wait 1 979 | rate 3990 980 | wait 1 981 | rate 4904 982 | wait 1 983 | rate 3882 984 | wait 1 985 | rate 3190 986 | wait 1 987 | rate 6746 988 | wait 1 989 | rate 6785 990 | wait 1 991 | rate 6605 992 | wait 1 993 | rate 5648 994 | wait 1 995 | rate 5935 996 | wait 1 997 | rate 3636 998 | wait 1 999 | rate 6320 1000 | wait 1 1001 | rate 5037 1002 | wait 1 1003 | rate 4868 1004 | wait 1 1005 | rate 5456 1006 | wait 1 1007 | rate 3720 1008 | wait 1 1009 | rate 4511 1010 | wait 1 1011 | rate 5512 1012 | wait 1 1013 | rate 693 1014 | wait 1 1015 | rate 1208 1016 | wait 1 1017 | rate 1496 1018 | wait 1 1019 | rate 891 1020 | wait 1 1021 | rate 2624 1022 | wait 1 1023 | rate 1370 1024 | wait 1 1025 | rate 1537 1026 | wait 1 1027 | rate 1566 1028 | wait 1 1029 | rate 4046 1030 | wait 1 1031 | rate 2590 1032 | wait 1 1033 | rate 2813 1034 | wait 1 1035 | rate 3539 1036 | wait 1 1037 | rate 2033 1038 | wait 1 1039 | rate 1014 1040 | wait 1 1041 | rate 1875 1042 | wait 1 1043 | rate 3163 1044 | wait 1 1045 | rate 1362 1046 | wait 1 1047 | rate 1787 1048 | wait 1 1049 | rate 2566 1050 | wait 1 1051 | rate 3392 1052 | wait 1 1053 | rate 2758 1054 | wait 1 1055 | rate 5188 1056 | wait 1 1057 | rate 5106 1058 | wait 1 1059 | rate 6575 1060 | wait 1 1061 | rate 6557 1062 | wait 1 1063 | rate 4684 1064 | wait 1 1065 | rate 1737 1066 | wait 1 1067 | rate 1148 1068 | wait 1 1069 | rate 5168 1070 | wait 1 1071 | rate 6224 1072 | wait 1 1073 | rate 5358 1074 | wait 1 1075 | rate 4200 1076 | wait 1 1077 | rate 1563 1078 | wait 1 1079 | rate 4428 1080 | wait 1 1081 | rate 2541 1082 | wait 1 1083 | rate 235 1084 | wait 1 1085 | rate 2532 1086 | wait 1 1087 | rate 1977 1088 | wait 1 1089 | rate 887 1090 | wait 1 1091 | rate 1015 1092 | wait 1 1093 | rate 1281 1094 | wait 1 1095 | rate 850 1096 | wait 1 1097 | rate 991 1098 | wait 1 1099 | rate 417 1100 | wait 1 1101 | rate 503 1102 | wait 1 1103 | rate 613 1104 | wait 1 1105 | rate 1088 1106 | wait 1 1107 | rate 3107 1108 | wait 1 1109 | rate 4133 1110 | wait 1 1111 | rate 3768 1112 | wait 1 1113 | rate 2970 1114 | wait 1 1115 | rate 3578 1116 | wait 1 1117 | rate 2480 1118 | wait 1 1119 | rate 2330 1120 | wait 1 1121 | rate 2501 1122 | wait 1 1123 | rate 2425 1124 | wait 1 1125 | rate 2619 1126 | wait 1 1127 | rate 4849 1128 | wait 1 1129 | rate 4248 1130 | wait 1 1131 | rate 4604 1132 | wait 1 1133 | rate 4882 1134 | wait 1 1135 | rate 4669 1136 | wait 1 1137 | rate 3506 1138 | wait 1 1139 | rate 1589 1140 | wait 1 1141 | rate 7086 1142 | wait 1 1143 | rate 7569 1144 | wait 1 1145 | rate 4642 1146 | wait 1 1147 | rate 3961 1148 | wait 1 1149 | rate 3505 1150 | wait 1 1151 | rate 2316 1152 | wait 1 1153 | rate 3957 1154 | wait 1 1155 | rate 5290 1156 | wait 1 1157 | rate 4554 1158 | wait 1 1159 | rate 4830 1160 | wait 1 1161 | rate 3942 1162 | wait 1 1163 | rate 4253 1164 | wait 1 1165 | rate 3917 1166 | wait 1 1167 | rate 3481 1168 | wait 1 1169 | rate 6457 1170 | wait 1 1171 | rate 5634 1172 | wait 1 1173 | rate 4860 1174 | wait 1 1175 | rate 4906 1176 | wait 1 1177 | rate 4073 1178 | wait 1 1179 | rate 5197 1180 | wait 1 1181 | rate 1839 1182 | wait 1 1183 | rate 6822 1184 | wait 1 1185 | rate 6068 1186 | wait 1 1187 | rate 3313 1188 | wait 1 1189 | rate 2943 1190 | wait 1 1191 | rate 3648 1192 | wait 1 1193 | rate 3085 1194 | wait 1 1195 | rate 2550 1196 | wait 1 1197 | rate 2596 1198 | wait 1 1199 | rate 2393 1200 | wait 1 -------------------------------------------------------------------------------- /server/tc_scripts/profile_slow_jitters: -------------------------------------------------------------------------------- 1 | const PROFILE_SLOW_JITTERS = [ 2 | { 3 | speed: 500, 4 | duration: 5, 5 | }, 6 | { 7 | speed: 1200, 8 | duration: 5, 9 | }, 10 | { 11 | speed: 500, 12 | duration: 5, 13 | }, 14 | { 15 | speed: 1200, 16 | duration: 5, 17 | }, 18 | { 19 | speed: 500, 20 | duration: 5, 21 | }, 22 | { 23 | speed: 1200, 24 | duration: 5, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /server/tc_scripts/profile_spike: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streaming-university/public-moq-demo/2071137a5967e645ab97b6e914dc4f30c8c47d95/server/tc_scripts/profile_spike -------------------------------------------------------------------------------- /server/tc_scripts/profile_twitch: -------------------------------------------------------------------------------- 1 | rate 440 2 | wait 5 3 | rate 370 4 | wait 5 5 | rate 370 6 | wait 5 7 | rate 2598 8 | wait 5 9 | rate 2022 10 | wait 5 11 | rate 2022 12 | wait 5 13 | rate 2367 14 | wait 5 15 | rate 2367 16 | wait 5 17 | rate 1831 18 | wait 5 19 | rate 1831 20 | wait 5 21 | rate 1541 22 | wait 5 23 | rate 1541 24 | wait 5 25 | rate 2158 26 | wait 5 27 | rate 2158 28 | wait 5 29 | rate 295 30 | wait 5 31 | rate 295 32 | wait 5 33 | rate 2544 34 | wait 5 35 | rate 1780 36 | wait 5 37 | rate 1780 38 | wait 5 39 | rate 1330 40 | wait 5 41 | rate 1330 42 | wait 5 43 | rate 108 44 | wait 5 45 | rate 108 46 | wait 5 47 | rate 246 48 | wait 5 49 | rate 246 50 | wait 5 51 | rate 242 52 | wait 5 53 | rate 242 54 | wait 5 55 | rate 457 56 | wait 5 57 | rate 457 58 | wait 5 59 | rate 306 60 | wait 5 61 | rate 306 62 | wait 5 63 | rate 1806 64 | wait 5 65 | rate 1806 66 | wait 5 67 | rate 2379 68 | wait 5 69 | rate 2379 70 | wait 5 71 | rate 1066 72 | wait 5 73 | rate 2260 74 | wait 5 75 | rate 2260 76 | wait 5 -------------------------------------------------------------------------------- /server/tc_scripts/tc_reset.sh: -------------------------------------------------------------------------------- 1 | # Put your interface name here 2 | INTERFACE=enp0s31f6 3 | 4 | if tc qdisc show dev $INTERFACE | grep netem; then 5 | sudo tc qdisc del dev $INTERFACE root 6 | else 7 | echo "no netem rule" 8 | fi 9 | -------------------------------------------------------------------------------- /server/tc_scripts/test_profile: -------------------------------------------------------------------------------- 1 | rate 1200 2 | wait 30 3 | rate 800 4 | wait 30 5 | rate 400 6 | wait 30 7 | rate 800 8 | wait 30 9 | rate 1200 10 | wait 30 11 | -------------------------------------------------------------------------------- /server/tc_scripts/test_profile copy: -------------------------------------------------------------------------------- 1 | rate 5 2 | wait 11 3 | rate 1 4 | wait 11 5 | rate 2 6 | wait 11 7 | rate 3 8 | wait 11 9 | rate 5 10 | wait 11 11 | rate 10 12 | wait 11 13 | -------------------------------------------------------------------------------- /server/tc_scripts/throttle.sh: -------------------------------------------------------------------------------- 1 | # Put your interface name here 2 | INTERFACE=enp0s31f6 3 | RATE_MBIT=$1 4 | 5 | if [[ -z $RATE_MBIT ]]; then 6 | echo "missing rate" 7 | exit 1 8 | fi 9 | 10 | DELAY_MS=40 11 | BUF_PKTS=33 12 | BDP_BYTES=$(echo "($DELAY_MS/1000.0)*($RATE_MBIT*1000000.0/8.0)" | bc -q -l) 13 | BDP_PKTS=$(echo "$BDP_BYTES/1500" | bc -q) 14 | LIMIT_PKTS=$(echo "$BDP_PKTS+$BUF_PKTS" | bc -q) 15 | 16 | echo "tc qdisc replace dev $INTERFACE root netem delay ${DELAY_MS}ms rate ${RATE_MBIT}Mbit limit ${LIMIT_PKTS}" 17 | sudo tc qdisc replace dev $INTERFACE root netem delay ${DELAY_MS}ms rate ${RATE_MBIT}Mbit limit ${LIMIT_PKTS} -------------------------------------------------------------------------------- /side-load/1MB-chunk.m4s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streaming-university/public-moq-demo/2071137a5967e645ab97b6e914dc4f30c8c47d95/side-load/1MB-chunk.m4s -------------------------------------------------------------------------------- /tc_profiles/bandwidth_scale.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streaming-university/public-moq-demo/2071137a5967e645ab97b6e914dc4f30c8c47d95/tc_profiles/bandwidth_scale.ods -------------------------------------------------------------------------------- /tc_profiles/cascade_profile: -------------------------------------------------------------------------------- 1 | rate 1200kbit 2 | wait 5s 3 | rate 800kbit 4 | wait 5s 5 | rate 400kbit 6 | wait 5s 7 | rate 800kbit 8 | wait 5s 9 | rate 1200kbit 10 | wait 5s 11 | -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x0.05: -------------------------------------------------------------------------------- 1 | rate 60kbit 2 | wait 30s 3 | rate 40kbit 4 | wait 30s 5 | rate 20kbit 6 | wait 30s 7 | rate 40kbit 8 | wait 30s 9 | rate 60kbit 10 | wait 30s 11 | -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x0.25: -------------------------------------------------------------------------------- 1 | rate 300kbit 2 | wait 5s 3 | rate 200kbit 4 | wait 5s 5 | rate 100kbit 6 | wait 5s 7 | rate 200kbit 8 | wait 5s 9 | rate 300kbit 10 | wait 5s 11 | -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x1.25: -------------------------------------------------------------------------------- 1 | rate 1500kbit 2 | wait 5s 3 | rate 1000kbit 4 | wait 5s 5 | rate 500kbit 6 | wait 5s 7 | rate 1000kbit 8 | wait 5s 9 | rate 1500kbit 10 | wait 5s -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x3: -------------------------------------------------------------------------------- 1 | rate 3600kbit 2 | wait 30s 3 | rate 2400kbit 4 | wait 30s 5 | rate 1200kbit 6 | wait 30s 7 | rate 2400kbit 8 | wait 30s 9 | rate 3600kbit 10 | wait 30s -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x4: -------------------------------------------------------------------------------- 1 | rate 4800kbit 2 | wait 30s 3 | rate 3200kbit 4 | wait 30s 5 | rate 1600kbit 6 | wait 30s 7 | rate 3200kbit 8 | wait 30s 9 | rate 4800kbit 10 | wait 30s -------------------------------------------------------------------------------- /tc_profiles/cascade_profile_x4.5: -------------------------------------------------------------------------------- 1 | rate 5400kbit 2 | wait 30s 3 | rate 3600kbit 4 | wait 30s 5 | rate 1800kbit 6 | wait 30s 7 | rate 3600kbit 8 | wait 30s 9 | rate 5400kbit 10 | wait 30s -------------------------------------------------------------------------------- /tc_profiles/lte_profile: -------------------------------------------------------------------------------- 1 | rate 693kbit 2 | wait 1s 3 | rate 1208kbit 4 | wait 1s 5 | rate 1496kbit 6 | wait 1s 7 | rate 891kbit 8 | wait 1s 9 | rate 2624kbit 10 | wait 1s 11 | rate 1370kbit 12 | wait 1s 13 | rate 1537kbit 14 | wait 1s 15 | rate 1566kbit 16 | wait 1s 17 | rate 4046kbit 18 | wait 1s 19 | rate 2590kbit 20 | wait 1s 21 | rate 2813kbit 22 | wait 1s 23 | rate 3539kbit 24 | wait 1s 25 | rate 2033kbit 26 | wait 1s 27 | rate 1014kbit 28 | wait 1s 29 | rate 1875kbit 30 | wait 1s 31 | rate 3163kbit 32 | wait 1s 33 | rate 1362kbit 34 | wait 1s 35 | rate 1787kbit 36 | wait 1s 37 | rate 2566kbit 38 | wait 1s 39 | rate 3392kbit 40 | wait 1s 41 | rate 2758kbit 42 | wait 1s 43 | rate 5188kbit 44 | wait 1s 45 | rate 5106kbit 46 | wait 1s 47 | rate 6575kbit 48 | wait 1s 49 | rate 6557kbit 50 | wait 1s 51 | rate 4684kbit 52 | wait 1s 53 | rate 1737kbit 54 | wait 1s 55 | rate 1148kbit 56 | wait 1s 57 | rate 5168kbit 58 | wait 1s 59 | rate 6224kbit 60 | wait 1s 61 | rate 5358kbit 62 | wait 1s 63 | rate 4200kbit 64 | wait 1s 65 | rate 1563kbit 66 | wait 1s 67 | rate 4428kbit 68 | wait 1s 69 | rate 2541kbit 70 | wait 1s 71 | rate 300kbit 72 | wait 1s 73 | rate 2532kbit 74 | wait 1s 75 | rate 1977kbit 76 | wait 1s 77 | rate 887kbit 78 | wait 1s 79 | rate 1015kbit 80 | wait 1s 81 | rate 1281kbit 82 | wait 1s 83 | rate 850kbit 84 | wait 1s 85 | rate 991kbit 86 | wait 1s 87 | rate 417kbit 88 | wait 1s 89 | rate 503kbit 90 | wait 1s 91 | rate 613kbit 92 | wait 1s 93 | rate 1088kbit 94 | wait 1s 95 | rate 3107kbit 96 | wait 1s 97 | rate 4133kbit 98 | wait 1s 99 | rate 3768kbit 100 | wait 1s 101 | rate 2970kbit 102 | wait 1s 103 | rate 3578kbit 104 | wait 1s 105 | rate 2480kbit 106 | wait 1s 107 | rate 2330kbit 108 | wait 1s 109 | rate 2501kbit 110 | wait 1s 111 | rate 2425kbit 112 | wait 1s 113 | rate 2619kbit 114 | wait 1s 115 | rate 4849kbit 116 | wait 1s 117 | rate 4248kbit 118 | wait 1s 119 | rate 4604kbit 120 | wait 1s 121 | rate 4882kbit 122 | wait 1s 123 | rate 4669kbit 124 | wait 1s 125 | rate 3506kbit 126 | wait 1s 127 | rate 1589kbit 128 | wait 1s 129 | rate 7086kbit 130 | wait 1s 131 | rate 7569kbit 132 | wait 1s 133 | rate 4642kbit 134 | wait 1s 135 | rate 3961kbit 136 | wait 1s 137 | rate 3505kbit 138 | wait 1s 139 | rate 2316kbit 140 | wait 1s 141 | rate 3957kbit 142 | wait 1s 143 | rate 5290kbit 144 | wait 1s 145 | rate 4554kbit 146 | wait 1s 147 | rate 4830kbit 148 | wait 1s 149 | rate 3942kbit 150 | wait 1s 151 | rate 4253kbit 152 | wait 1s 153 | rate 3917kbit 154 | wait 1s 155 | rate 3481kbit 156 | wait 1s 157 | rate 6457kbit 158 | wait 1s 159 | rate 5634kbit 160 | wait 1s 161 | rate 4860kbit 162 | wait 1s 163 | rate 4906kbit 164 | wait 1s 165 | rate 4073kbit 166 | wait 1s 167 | rate 5197kbit 168 | wait 1s 169 | rate 1839kbit 170 | wait 1s 171 | rate 6822kbit 172 | wait 1s 173 | rate 6068kbit 174 | wait 1s 175 | rate 3313kbit 176 | wait 1s 177 | rate 2943kbit 178 | wait 1s 179 | rate 3648kbit 180 | wait 1s 181 | rate 3085kbit 182 | wait 1s 183 | rate 2550kbit 184 | wait 1s 185 | rate 2596kbit 186 | wait 1s 187 | rate 2393kbit 188 | wait 1s 189 | rate 2015kbit 190 | wait 1s 191 | rate 3069kbit 192 | wait 1s 193 | rate 2737kbit 194 | wait 1s 195 | rate 856kbit 196 | wait 1s 197 | rate 1264kbit 198 | wait 1s 199 | rate 1827kbit 200 | wait 1s 201 | rate 3503kbit 202 | wait 1s 203 | rate 2497kbit 204 | wait 1s 205 | rate 2601kbit 206 | wait 1s 207 | rate 4287kbit 208 | wait 1s 209 | rate 4130kbit 210 | wait 1s 211 | rate 2219kbit 212 | wait 1s 213 | rate 3460kbit 214 | wait 1s 215 | rate 4906kbit 216 | wait 1s 217 | rate 5746kbit 218 | wait 1s 219 | rate 5863kbit 220 | wait 1s 221 | rate 5786kbit 222 | wait 1s 223 | rate 6777kbit 224 | wait 1s 225 | rate 3238kbit 226 | wait 1s 227 | rate 4151kbit 228 | wait 1s 229 | rate 6081kbit 230 | wait 1s 231 | rate 4198kbit 232 | wait 1s 233 | rate 5497kbit 234 | wait 1s 235 | rate 3597kbit 236 | wait 1s 237 | rate 5485kbit 238 | wait 1s 239 | rate 4777kbit 240 | wait 1s 241 | rate 4625kbit 242 | wait 1s 243 | rate 4541kbit 244 | wait 1s 245 | rate 3194kbit 246 | wait 1s 247 | rate 5389kbit 248 | wait 1s 249 | rate 4849kbit 250 | wait 1s 251 | rate 5170kbit 252 | wait 1s 253 | rate 4772kbit 254 | wait 1s 255 | rate 3363kbit 256 | wait 1s 257 | rate 4982kbit 258 | wait 1s 259 | rate 4671kbit 260 | wait 1s 261 | rate 6679kbit 262 | wait 1s 263 | rate 4481kbit 264 | wait 1s 265 | rate 4420kbit 266 | wait 1s 267 | rate 3727kbit 268 | wait 1s 269 | rate 5800kbit 270 | wait 1s 271 | rate 5666kbit 272 | wait 1s 273 | rate 4044kbit 274 | wait 1s 275 | rate 4984kbit 276 | wait 1s 277 | rate 3895kbit 278 | wait 1s 279 | rate 5075kbit 280 | wait 1s 281 | rate 2355kbit 282 | wait 1s 283 | rate 2439kbit 284 | wait 1s 285 | rate 1608kbit 286 | wait 1s 287 | rate 800kbit 288 | wait 1s 289 | rate 800kbit 290 | wait 1s 291 | rate 800kbit 292 | wait 1s 293 | rate 800kbit 294 | wait 1s 295 | rate 3601kbit 296 | wait 1s 297 | rate 664kbit 298 | wait 1s 299 | rate 764kbit 300 | wait 1s 301 | rate 781kbit 302 | wait 1s 303 | rate 2390kbit 304 | wait 1s 305 | rate 544kbit 306 | wait 1s 307 | rate 899kbit 308 | wait 1s 309 | rate 2247kbit 310 | wait 1s 311 | rate 2837kbit 312 | wait 1s 313 | rate 1689kbit 314 | wait 1s 315 | rate 2106kbit 316 | wait 1s 317 | rate 3321kbit 318 | wait 1s 319 | rate 1401kbit 320 | wait 1s 321 | rate 340kbit 322 | wait 1s 323 | rate 329kbit 324 | wait 1s 325 | rate 300kbit 326 | wait 1s 327 | rate 300kbit 328 | wait 1s 329 | rate 303kbit 330 | wait 1s 331 | rate 389kbit 332 | wait 1s 333 | rate 300kbit 334 | wait 1s 335 | rate 300kbit 336 | wait 1s 337 | rate 300kbit 338 | wait 1s 339 | rate 920kbit 340 | wait 1s 341 | rate 300kbit 342 | wait 1s 343 | rate 300kbit 344 | wait 1s 345 | rate 300kbit 346 | wait 1s 347 | rate 300kbit 348 | wait 1s 349 | rate 300kbit 350 | wait 1s 351 | rate 300kbit 352 | wait 1s 353 | rate 300kbit 354 | wait 1s 355 | rate 300kbit 356 | wait 1s 357 | rate 5225kbit 358 | wait 1s 359 | rate 3820kbit 360 | wait 1s 361 | rate 4770kbit 362 | wait 1s 363 | rate 3614kbit 364 | wait 1s 365 | rate 4837kbit 366 | wait 1s 367 | rate 4931kbit 368 | wait 1s 369 | rate 4023kbit 370 | wait 1s 371 | rate 4234kbit 372 | wait 1s 373 | rate 4303kbit 374 | wait 1s 375 | rate 2143kbit 376 | wait 1s 377 | rate 2180kbit 378 | wait 1s 379 | rate 3072kbit 380 | wait 1s 381 | rate 828kbit 382 | wait 1s 383 | rate 2177kbit 384 | wait 1s 385 | rate 1892kbit 386 | wait 1s 387 | rate 855kbit 388 | wait 1s 389 | rate 2338kbit 390 | wait 1s 391 | rate 1755kbit 392 | wait 1s 393 | rate 2313kbit 394 | wait 1s 395 | rate 1523kbit 396 | wait 1s 397 | rate 428kbit 398 | wait 1s 399 | rate 300kbit 400 | wait 1s 401 | rate 728kbit 402 | wait 1s 403 | rate 328kbit 404 | wait 1s 405 | rate 3903kbit 406 | wait 1s 407 | rate 2499kbit 408 | wait 1s 409 | rate 3864kbit 410 | wait 1s 411 | rate 3351kbit 412 | wait 1s 413 | rate 4325kbit 414 | wait 1s 415 | rate 3951kbit 416 | wait 1s 417 | rate 1876kbit 418 | wait 1s 419 | rate 1582kbit 420 | wait 1s 421 | rate 1624kbit 422 | wait 1s 423 | rate 865kbit 424 | wait 1s 425 | rate 477kbit 426 | wait 1s 427 | rate 477kbit 428 | wait 1s 429 | rate 477kbit 430 | wait 1s 431 | rate 477kbit 432 | wait 1s 433 | rate 692kbit 434 | wait 1s 435 | rate 5396kbit 436 | wait 1s 437 | rate 3864kbit 438 | wait 1s 439 | rate 5317kbit 440 | wait 1s 441 | rate 3542kbit 442 | wait 1s 443 | rate 6376kbit 444 | wait 1s 445 | rate 3918kbit 446 | wait 1s 447 | rate 4299kbit 448 | wait 1s 449 | rate 4002kbit 450 | wait 1s 451 | rate 4674kbit 452 | wait 1s 453 | rate 5747kbit 454 | wait 1s 455 | rate 2925kbit 456 | wait 1s 457 | rate 1498kbit 458 | wait 1s 459 | rate 4018kbit 460 | wait 1s 461 | rate 4206kbit 462 | wait 1s 463 | rate 3494kbit 464 | wait 1s 465 | rate 2168kbit 466 | wait 1s 467 | rate 4291kbit 468 | wait 1s 469 | rate 3843kbit 470 | wait 1s 471 | rate 3536kbit 472 | wait 1s 473 | rate 4461kbit 474 | wait 1s 475 | rate 6009kbit 476 | wait 1s 477 | rate 6853kbit 478 | wait 1s 479 | rate 3979kbit 480 | wait 1s 481 | rate 5464kbit 482 | wait 1s 483 | rate 5033kbit 484 | wait 1s 485 | rate 3843kbit 486 | wait 1s 487 | rate 3049kbit 488 | wait 1s 489 | rate 3162kbit 490 | wait 1s 491 | rate 3252kbit 492 | wait 1s 493 | rate 2864kbit 494 | wait 1s 495 | rate 2961kbit 496 | wait 1s 497 | rate 2469kbit 498 | wait 1s 499 | rate 2033kbit 500 | wait 1s 501 | rate 2072kbit 502 | wait 1s 503 | rate 872kbit 504 | wait 1s 505 | rate 2401kbit 506 | wait 1s 507 | rate 2480kbit 508 | wait 1s 509 | rate 4526kbit 510 | wait 1s 511 | rate 3205kbit 512 | wait 1s 513 | rate 2562kbit 514 | wait 1s 515 | rate 2833kbit 516 | wait 1s 517 | rate 1915kbit 518 | wait 1s 519 | rate 1817kbit 520 | wait 1s 521 | rate 845kbit 522 | wait 1s 523 | rate 300kbit 524 | wait 1s 525 | rate 300kbit 526 | wait 1s 527 | rate 335kbit 528 | wait 1s 529 | rate 3067kbit 530 | wait 1s 531 | rate 3067kbit 532 | wait 1s 533 | rate 300kbit 534 | wait 1s 535 | rate 3406kbit 536 | wait 1s 537 | rate 820kbit 538 | wait 1s 539 | rate 488kbit 540 | wait 1s 541 | rate 2334kbit 542 | wait 1s 543 | rate 1132kbit 544 | wait 1s 545 | rate 675kbit 546 | wait 1s 547 | rate 884kbit 548 | wait 1s 549 | rate 3045kbit 550 | wait 1s 551 | rate 2895kbit 552 | wait 1s 553 | rate 791kbit 554 | wait 1s 555 | rate 616kbit 556 | wait 1s 557 | rate 338kbit 558 | wait 1s 559 | rate 419kbit 560 | wait 1s 561 | rate 578kbit 562 | wait 1s 563 | rate 815kbit 564 | wait 1s 565 | rate 721kbit 566 | wait 1s 567 | rate 598kbit 568 | wait 1s 569 | rate 900kbit 570 | wait 1s 571 | rate 1274kbit 572 | wait 1s 573 | rate 874kbit 574 | wait 1s 575 | rate 1402kbit 576 | wait 1s 577 | rate 1704kbit 578 | wait 1s 579 | rate 2162kbit 580 | wait 1s 581 | rate 1570kbit 582 | wait 1s 583 | rate 2219kbit 584 | wait 1s 585 | rate 3420kbit 586 | wait 1s 587 | rate 3915kbit 588 | wait 1s 589 | rate 4575kbit 590 | wait 1s 591 | rate 3874kbit 592 | wait 1s 593 | rate 2638kbit 594 | wait 1s 595 | rate 1388kbit 596 | wait 1s 597 | rate 1608kbit 598 | wait 1s 599 | rate 2773kbit 600 | wait 1s 601 | rate 1242kbit 602 | wait 1s 603 | rate 1224kbit 604 | wait 1s 605 | rate 2073kbit 606 | wait 1s 607 | rate 3226kbit 608 | wait 1s 609 | rate 2437kbit 610 | wait 1s 611 | rate 3308kbit 612 | wait 1s 613 | rate 1962kbit 614 | wait 1s 615 | rate 771kbit 616 | wait 1s 617 | rate 405kbit 618 | wait 1s 619 | rate 876kbit 620 | wait 1s 621 | rate 896kbit 622 | wait 1s 623 | rate 1325kbit 624 | wait 1s 625 | rate 1590kbit 626 | wait 1s 627 | rate 827kbit 628 | wait 1s 629 | rate 1341kbit 630 | wait 1s 631 | rate 1440kbit 632 | wait 1s 633 | rate 1440kbit 634 | wait 1s 635 | rate 300kbit 636 | wait 1s 637 | rate 3453kbit 638 | wait 1s 639 | rate 4905kbit 640 | wait 1s 641 | rate 3695kbit 642 | wait 1s 643 | rate 2203kbit 644 | wait 1s 645 | rate 3264kbit 646 | wait 1s 647 | rate 2917kbit 648 | wait 1s 649 | rate 2811kbit 650 | wait 1s 651 | rate 5188kbit 652 | wait 1s 653 | rate 4855kbit 654 | wait 1s 655 | rate 2909kbit 656 | wait 1s 657 | rate 1943kbit 658 | wait 1s 659 | rate 3245kbit 660 | wait 1s 661 | rate 1516kbit 662 | wait 1s 663 | rate 2183kbit 664 | wait 1s 665 | rate 1323kbit 666 | wait 1s 667 | rate 2253kbit 668 | wait 1s 669 | rate 2235kbit 670 | wait 1s 671 | rate 1916kbit 672 | wait 1s 673 | rate 1655kbit 674 | wait 1s 675 | rate 2656kbit 676 | wait 1s 677 | rate 4314kbit 678 | wait 1s 679 | rate 3214kbit 680 | wait 1s 681 | rate 3963kbit 682 | wait 1s 683 | rate 2939kbit 684 | wait 1s 685 | rate 5132kbit 686 | wait 1s 687 | rate 2330kbit 688 | wait 1s 689 | rate 1555kbit 690 | wait 1s 691 | rate 2687kbit 692 | wait 1s 693 | rate 4240kbit 694 | wait 1s 695 | rate 2678kbit 696 | wait 1s 697 | rate 5395kbit 698 | wait 1s 699 | rate 4863kbit 700 | wait 1s 701 | rate 4190kbit 702 | wait 1s 703 | rate 4484kbit 704 | wait 1s 705 | rate 3808kbit 706 | wait 1s 707 | rate 3144kbit 708 | wait 1s 709 | rate 4501kbit 710 | wait 1s 711 | rate 3478kbit 712 | wait 1s 713 | rate 4416kbit 714 | wait 1s 715 | rate 3105kbit 716 | wait 1s 717 | rate 4026kbit 718 | wait 1s 719 | rate 4619kbit 720 | wait 1s 721 | rate 2955kbit 722 | wait 1s 723 | rate 4802kbit 724 | wait 1s 725 | rate 4049kbit 726 | wait 1s 727 | rate 5244kbit 728 | wait 1s 729 | rate 7086kbit 730 | wait 1s 731 | rate 4491kbit 732 | wait 1s 733 | rate 4090kbit 734 | wait 1s 735 | rate 7144kbit 736 | wait 1s 737 | rate 5474kbit 738 | wait 1s 739 | rate 4374kbit 740 | wait 1s 741 | rate 3601kbit 742 | wait 1s 743 | rate 5039kbit 744 | wait 1s 745 | rate 3976kbit 746 | wait 1s 747 | rate 3772kbit 748 | wait 1s 749 | rate 3879kbit 750 | wait 1s 751 | rate 3638kbit 752 | wait 1s 753 | rate 4023kbit 754 | wait 1s 755 | rate 3676kbit 756 | wait 1s 757 | rate 3375kbit 758 | wait 1s 759 | rate 2653kbit 760 | wait 1s 761 | rate 1756kbit 762 | wait 1s 763 | rate 1916kbit 764 | wait 1s 765 | rate 815kbit 766 | wait 1s 767 | rate 815kbit 768 | wait 1s 769 | rate 1135kbit 770 | wait 1s 771 | rate 300kbit 772 | wait 1s 773 | rate 300kbit 774 | wait 1s 775 | rate 3085kbit 776 | wait 1s 777 | rate 1817kbit 778 | wait 1s 779 | rate 2724kbit 780 | wait 1s 781 | rate 1987kbit 782 | wait 1s 783 | rate 876kbit 784 | wait 1s 785 | rate 2378kbit 786 | wait 1s 787 | rate 1988kbit 788 | wait 1s 789 | rate 2383kbit 790 | wait 1s 791 | rate 2346kbit 792 | wait 1s 793 | rate 2023kbit 794 | wait 1s 795 | rate 3505kbit 796 | wait 1s 797 | rate 3578kbit 798 | wait 1s 799 | rate 1953kbit 800 | wait 1s 801 | rate 1908kbit 802 | wait 1s 803 | rate 2694kbit 804 | wait 1s 805 | rate 2398kbit 806 | wait 1s 807 | rate 2373kbit 808 | wait 1s 809 | rate 2580kbit 810 | wait 1s 811 | rate 2824kbit 812 | wait 1s 813 | rate 2074kbit 814 | wait 1s 815 | rate 2613kbit 816 | wait 1s 817 | rate 2254kbit 818 | wait 1s 819 | rate 2529kbit 820 | wait 1s 821 | rate 1510kbit 822 | wait 1s 823 | rate 973kbit 824 | wait 1s 825 | rate 520kbit 826 | wait 1s 827 | rate 776kbit 828 | wait 1s 829 | rate 301kbit 830 | wait 1s 831 | rate 300kbit 832 | wait 1s 833 | rate 508kbit 834 | wait 1s 835 | rate 576kbit 836 | wait 1s 837 | rate 1275kbit 838 | wait 1s 839 | rate 1876kbit 840 | wait 1s 841 | rate 2138kbit 842 | wait 1s 843 | rate 1433kbit 844 | wait 1s 845 | rate 1224kbit 846 | wait 1s 847 | rate 960kbit 848 | wait 1s 849 | rate 2422kbit 850 | wait 1s 851 | rate 1827kbit 852 | wait 1s 853 | rate 2433kbit 854 | wait 1s 855 | rate 2945kbit 856 | wait 1s 857 | rate 3360kbit 858 | wait 1s 859 | rate 2849kbit 860 | wait 1s 861 | rate 2056kbit 862 | wait 1s 863 | rate 2611kbit 864 | wait 1s 865 | rate 2274kbit 866 | wait 1s 867 | rate 2012kbit 868 | wait 1s 869 | rate 1998kbit 870 | wait 1s 871 | rate 2647kbit 872 | wait 1s 873 | rate 2172kbit 874 | wait 1s 875 | rate 2575kbit 876 | wait 1s 877 | rate 2398kbit 878 | wait 1s 879 | rate 3442kbit 880 | wait 1s 881 | rate 2633kbit 882 | wait 1s 883 | rate 1069kbit 884 | wait 1s 885 | rate 1212kbit 886 | wait 1s 887 | rate 1472kbit 888 | wait 1s 889 | rate 300kbit 890 | wait 1s 891 | rate 2170kbit 892 | wait 1s 893 | rate 300kbit 894 | wait 1s 895 | rate 1591kbit 896 | wait 1s 897 | rate 2069kbit 898 | wait 1s 899 | rate 1733kbit 900 | wait 1s 901 | rate 2254kbit 902 | wait 1s 903 | rate 2167kbit 904 | wait 1s 905 | rate 2264kbit 906 | wait 1s 907 | rate 1343kbit 908 | wait 1s 909 | rate 3402kbit 910 | wait 1s 911 | rate 2365kbit 912 | wait 1s 913 | rate 1125kbit 914 | wait 1s 915 | rate 7008kbit 916 | wait 1s 917 | rate 2684kbit 918 | wait 1s 919 | rate 3804kbit 920 | wait 1s 921 | rate 2670kbit 922 | wait 1s 923 | rate 4154kbit 924 | wait 1s 925 | rate 3311kbit 926 | wait 1s 927 | rate 4237kbit 928 | wait 1s 929 | rate 4055kbit 930 | wait 1s 931 | rate 4182kbit 932 | wait 1s 933 | rate 3899kbit 934 | wait 1s 935 | rate 4096kbit 936 | wait 1s 937 | rate 3607kbit 938 | wait 1s 939 | rate 4687kbit 940 | wait 1s 941 | rate 4165kbit 942 | wait 1s 943 | rate 3630kbit 944 | wait 1s 945 | rate 4959kbit 946 | wait 1s 947 | rate 4680kbit 948 | wait 1s 949 | rate 3237kbit 950 | wait 1s 951 | rate 5280kbit 952 | wait 1s 953 | rate 3764kbit 954 | wait 1s 955 | rate 3776kbit 956 | wait 1s 957 | rate 2280kbit 958 | wait 1s 959 | rate 4496kbit 960 | wait 1s 961 | rate 3791kbit 962 | wait 1s 963 | rate 3994kbit 964 | wait 1s 965 | rate 5692kbit 966 | wait 1s 967 | rate 5422kbit 968 | wait 1s 969 | rate 5629kbit 970 | wait 1s 971 | rate 3578kbit 972 | wait 1s 973 | rate 4140kbit 974 | wait 1s 975 | rate 2809kbit 976 | wait 1s 977 | rate 5159kbit 978 | wait 1s 979 | rate 3990kbit 980 | wait 1s 981 | rate 4904kbit 982 | wait 1s 983 | rate 3882kbit 984 | wait 1s 985 | rate 3190kbit 986 | wait 1s 987 | rate 6746kbit 988 | wait 1s 989 | rate 6785kbit 990 | wait 1s 991 | rate 6605kbit 992 | wait 1s 993 | rate 5648kbit 994 | wait 1s 995 | rate 5935kbit 996 | wait 1s 997 | rate 3636kbit 998 | wait 1s 999 | rate 6320kbit 1000 | wait 1s 1001 | rate 5037kbit 1002 | wait 1s 1003 | rate 4868kbit 1004 | wait 1s 1005 | rate 5456kbit 1006 | wait 1s 1007 | rate 3720kbit 1008 | wait 1s 1009 | rate 4511kbit 1010 | wait 1s 1011 | rate 5512kbit 1012 | wait 1s 1013 | rate 693kbit 1014 | wait 1s 1015 | rate 1208kbit 1016 | wait 1s 1017 | rate 1496kbit 1018 | wait 1s 1019 | rate 891kbit 1020 | wait 1s 1021 | rate 2624kbit 1022 | wait 1s 1023 | rate 1370kbit 1024 | wait 1s 1025 | rate 1537kbit 1026 | wait 1s 1027 | rate 1566kbit 1028 | wait 1s 1029 | rate 4046kbit 1030 | wait 1s 1031 | rate 2590kbit 1032 | wait 1s 1033 | rate 2813kbit 1034 | wait 1s 1035 | rate 3539kbit 1036 | wait 1s 1037 | rate 2033kbit 1038 | wait 1s 1039 | rate 1014kbit 1040 | wait 1s 1041 | rate 1875kbit 1042 | wait 1s 1043 | rate 3163kbit 1044 | wait 1s 1045 | rate 1362kbit 1046 | wait 1s 1047 | rate 1787kbit 1048 | wait 1s 1049 | rate 2566kbit 1050 | wait 1s 1051 | rate 3392kbit 1052 | wait 1s 1053 | rate 2758kbit 1054 | wait 1s 1055 | rate 5188kbit 1056 | wait 1s 1057 | rate 5106kbit 1058 | wait 1s 1059 | rate 6575kbit 1060 | wait 1s 1061 | rate 6557kbit 1062 | wait 1s 1063 | rate 4684kbit 1064 | wait 1s 1065 | rate 1737kbit 1066 | wait 1s 1067 | rate 1148kbit 1068 | wait 1s 1069 | rate 5168kbit 1070 | wait 1s 1071 | rate 6224kbit 1072 | wait 1s 1073 | rate 5358kbit 1074 | wait 1s 1075 | rate 4200kbit 1076 | wait 1s 1077 | rate 1563kbit 1078 | wait 1s 1079 | rate 4428kbit 1080 | wait 1s 1081 | rate 2541kbit 1082 | wait 1s 1083 | rate 300kbit 1084 | wait 1s 1085 | rate 2532kbit 1086 | wait 1s 1087 | rate 1977kbit 1088 | wait 1s 1089 | rate 887kbit 1090 | wait 1s 1091 | rate 1015kbit 1092 | wait 1s 1093 | rate 1281kbit 1094 | wait 1s 1095 | rate 850kbit 1096 | wait 1s 1097 | rate 991kbit 1098 | wait 1s 1099 | rate 417kbit 1100 | wait 1s 1101 | rate 503kbit 1102 | wait 1s 1103 | rate 613kbit 1104 | wait 1s 1105 | rate 1088kbit 1106 | wait 1s 1107 | rate 3107kbit 1108 | wait 1s 1109 | rate 4133kbit 1110 | wait 1s 1111 | rate 3768kbit 1112 | wait 1s 1113 | rate 2970kbit 1114 | wait 1s 1115 | rate 3578kbit 1116 | wait 1s 1117 | rate 2480kbit 1118 | wait 1s 1119 | rate 2330kbit 1120 | wait 1s 1121 | rate 2501kbit 1122 | wait 1s 1123 | rate 2425kbit 1124 | wait 1s 1125 | rate 2619kbit 1126 | wait 1s 1127 | rate 4849kbit 1128 | wait 1s 1129 | rate 4248kbit 1130 | wait 1s 1131 | rate 4604kbit 1132 | wait 1s 1133 | rate 4882kbit 1134 | wait 1s 1135 | rate 4669kbit 1136 | wait 1s 1137 | rate 3506kbit 1138 | wait 1s 1139 | rate 1589kbit 1140 | wait 1s 1141 | rate 7086kbit 1142 | wait 1s 1143 | rate 7569kbit 1144 | wait 1s 1145 | rate 4642kbit 1146 | wait 1s 1147 | rate 3961kbit 1148 | wait 1s 1149 | rate 3505kbit 1150 | wait 1s 1151 | rate 2316kbit 1152 | wait 1s 1153 | rate 3957kbit 1154 | wait 1s 1155 | rate 5290kbit 1156 | wait 1s 1157 | rate 4554kbit 1158 | wait 1s 1159 | rate 4830kbit 1160 | wait 1s 1161 | rate 3942kbit 1162 | wait 1s 1163 | rate 4253kbit 1164 | wait 1s 1165 | rate 3917kbit 1166 | wait 1s 1167 | rate 3481kbit 1168 | wait 1s 1169 | rate 6457kbit 1170 | wait 1s 1171 | rate 5634kbit 1172 | wait 1s 1173 | rate 4860kbit 1174 | wait 1s 1175 | rate 4906kbit 1176 | wait 1s 1177 | rate 4073kbit 1178 | wait 1s 1179 | rate 5197kbit 1180 | wait 1s 1181 | rate 1839kbit 1182 | wait 1s 1183 | rate 6822kbit 1184 | wait 1s 1185 | rate 6068kbit 1186 | wait 1s 1187 | rate 3313kbit 1188 | wait 1s 1189 | rate 2943kbit 1190 | wait 1s 1191 | rate 3648kbit 1192 | wait 1s 1193 | rate 3085kbit 1194 | wait 1s 1195 | rate 2550kbit 1196 | wait 1s 1197 | rate 2596kbit 1198 | wait 1s 1199 | rate 2393kbit 1200 | wait 1s -------------------------------------------------------------------------------- /tc_profiles/lte_profile_x0.25: -------------------------------------------------------------------------------- 1 | rate 173kbit 2 | wait 1s 3 | rate 302kbit 4 | wait 1s 5 | rate 374kbit 6 | wait 1s 7 | rate 222kbit 8 | wait 1s 9 | rate 656kbit 10 | wait 1s 11 | rate 342kbit 12 | wait 1s 13 | rate 384kbit 14 | wait 1s 15 | rate 391kbit 16 | wait 1s 17 | rate 1011kbit 18 | wait 1s 19 | rate 647kbit 20 | wait 1s 21 | rate 703kbit 22 | wait 1s 23 | rate 884kbit 24 | wait 1s 25 | rate 508kbit 26 | wait 1s 27 | rate 253kbit 28 | wait 1s 29 | rate 468kbit 30 | wait 1s 31 | rate 790kbit 32 | wait 1s 33 | rate 340kbit 34 | wait 1s 35 | rate 446kbit 36 | wait 1s 37 | rate 641kbit 38 | wait 1s 39 | rate 848kbit 40 | wait 1s 41 | rate 689kbit 42 | wait 1s 43 | rate 1297kbit 44 | wait 1s 45 | rate 1276kbit 46 | wait 1s 47 | rate 1643kbit 48 | wait 1s 49 | rate 1639kbit 50 | wait 1s 51 | rate 1171kbit 52 | wait 1s 53 | rate 434kbit 54 | wait 1s 55 | rate 287kbit 56 | wait 1s 57 | rate 1292kbit 58 | wait 1s 59 | rate 1556kbit 60 | wait 1s 61 | rate 1339kbit 62 | wait 1s 63 | rate 1050kbit 64 | wait 1s 65 | rate 390kbit 66 | wait 1s 67 | rate 1107kbit 68 | wait 1s 69 | rate 635kbit 70 | wait 1s 71 | rate 75kbit 72 | wait 1s 73 | rate 633kbit 74 | wait 1s 75 | rate 494kbit 76 | wait 1s 77 | rate 221kbit 78 | wait 1s 79 | rate 253kbit 80 | wait 1s 81 | rate 320kbit 82 | wait 1s 83 | rate 212kbit 84 | wait 1s 85 | rate 247kbit 86 | wait 1s 87 | rate 104kbit 88 | wait 1s 89 | rate 125kbit 90 | wait 1s 91 | rate 153kbit 92 | wait 1s 93 | rate 272kbit 94 | wait 1s 95 | rate 776kbit 96 | wait 1s 97 | rate 1033kbit 98 | wait 1s 99 | rate 942kbit 100 | wait 1s 101 | rate 742kbit 102 | wait 1s 103 | rate 894kbit 104 | wait 1s 105 | rate 620kbit 106 | wait 1s 107 | rate 582kbit 108 | wait 1s 109 | rate 625kbit 110 | wait 1s 111 | rate 606kbit 112 | wait 1s 113 | rate 654kbit 114 | wait 1s 115 | rate 1212kbit 116 | wait 1s 117 | rate 1062kbit 118 | wait 1s 119 | rate 1151kbit 120 | wait 1s 121 | rate 1220kbit 122 | wait 1s 123 | rate 1167kbit 124 | wait 1s 125 | rate 876kbit 126 | wait 1s 127 | rate 397kbit 128 | wait 1s 129 | rate 1771kbit 130 | wait 1s 131 | rate 1892kbit 132 | wait 1s 133 | rate 1160kbit 134 | wait 1s 135 | rate 990kbit 136 | wait 1s 137 | rate 876kbit 138 | wait 1s 139 | rate 579kbit 140 | wait 1s 141 | rate 989kbit 142 | wait 1s 143 | rate 1322kbit 144 | wait 1s 145 | rate 1138kbit 146 | wait 1s 147 | rate 1207kbit 148 | wait 1s 149 | rate 985kbit 150 | wait 1s 151 | rate 1063kbit 152 | wait 1s 153 | rate 979kbit 154 | wait 1s 155 | rate 870kbit 156 | wait 1s 157 | rate 1614kbit 158 | wait 1s 159 | rate 1408kbit 160 | wait 1s 161 | rate 1215kbit 162 | wait 1s 163 | rate 1226kbit 164 | wait 1s 165 | rate 1018kbit 166 | wait 1s 167 | rate 1299kbit 168 | wait 1s 169 | rate 459kbit 170 | wait 1s 171 | rate 1705kbit 172 | wait 1s 173 | rate 1517kbit 174 | wait 1s 175 | rate 828kbit 176 | wait 1s 177 | rate 735kbit 178 | wait 1s 179 | rate 912kbit 180 | wait 1s 181 | rate 771kbit 182 | wait 1s 183 | rate 637kbit 184 | wait 1s 185 | rate 649kbit 186 | wait 1s 187 | rate 598kbit 188 | wait 1s 189 | rate 503kbit 190 | wait 1s 191 | rate 767kbit 192 | wait 1s 193 | rate 684kbit 194 | wait 1s 195 | rate 214kbit 196 | wait 1s 197 | rate 316kbit 198 | wait 1s 199 | rate 456kbit 200 | wait 1s 201 | rate 875kbit 202 | wait 1s 203 | rate 624kbit 204 | wait 1s 205 | rate 650kbit 206 | wait 1s 207 | rate 1071kbit 208 | wait 1s 209 | rate 1032kbit 210 | wait 1s 211 | rate 554kbit 212 | wait 1s 213 | rate 865kbit 214 | wait 1s 215 | rate 1226kbit 216 | wait 1s 217 | rate 1436kbit 218 | wait 1s 219 | rate 1465kbit 220 | wait 1s 221 | rate 1446kbit 222 | wait 1s 223 | rate 1694kbit 224 | wait 1s 225 | rate 809kbit 226 | wait 1s 227 | rate 1037kbit 228 | wait 1s 229 | rate 1520kbit 230 | wait 1s 231 | rate 1049kbit 232 | wait 1s 233 | rate 1374kbit 234 | wait 1s 235 | rate 899kbit 236 | wait 1s 237 | rate 1371kbit 238 | wait 1s 239 | rate 1194kbit 240 | wait 1s 241 | rate 1156kbit 242 | wait 1s 243 | rate 1135kbit 244 | wait 1s 245 | rate 798kbit 246 | wait 1s 247 | rate 1347kbit 248 | wait 1s 249 | rate 1212kbit 250 | wait 1s 251 | rate 1292kbit 252 | wait 1s 253 | rate 1193kbit 254 | wait 1s 255 | rate 840kbit 256 | wait 1s 257 | rate 1245kbit 258 | wait 1s 259 | rate 1167kbit 260 | wait 1s 261 | rate 1669kbit 262 | wait 1s 263 | rate 1120kbit 264 | wait 1s 265 | rate 1105kbit 266 | wait 1s 267 | rate 931kbit 268 | wait 1s 269 | rate 1450kbit 270 | wait 1s 271 | rate 1416kbit 272 | wait 1s 273 | rate 1011kbit 274 | wait 1s 275 | rate 1246kbit 276 | wait 1s 277 | rate 973kbit 278 | wait 1s 279 | rate 1268kbit 280 | wait 1s 281 | rate 588kbit 282 | wait 1s 283 | rate 609kbit 284 | wait 1s 285 | rate 402kbit 286 | wait 1s 287 | rate 200kbit 288 | wait 1s 289 | rate 200kbit 290 | wait 1s 291 | rate 200kbit 292 | wait 1s 293 | rate 200kbit 294 | wait 1s 295 | rate 900kbit 296 | wait 1s 297 | rate 166kbit 298 | wait 1s 299 | rate 191kbit 300 | wait 1s 301 | rate 195kbit 302 | wait 1s 303 | rate 597kbit 304 | wait 1s 305 | rate 136kbit 306 | wait 1s 307 | rate 224kbit 308 | wait 1s 309 | rate 561kbit 310 | wait 1s 311 | rate 709kbit 312 | wait 1s 313 | rate 422kbit 314 | wait 1s 315 | rate 526kbit 316 | wait 1s 317 | rate 830kbit 318 | wait 1s 319 | rate 350kbit 320 | wait 1s 321 | rate 85kbit 322 | wait 1s 323 | rate 82kbit 324 | wait 1s 325 | rate 75kbit 326 | wait 1s 327 | rate 75kbit 328 | wait 1s 329 | rate 75kbit 330 | wait 1s 331 | rate 97kbit 332 | wait 1s 333 | rate 75kbit 334 | wait 1s 335 | rate 75kbit 336 | wait 1s 337 | rate 75kbit 338 | wait 1s 339 | rate 230kbit 340 | wait 1s 341 | rate 75kbit 342 | wait 1s 343 | rate 75kbit 344 | wait 1s 345 | rate 75kbit 346 | wait 1s 347 | rate 75kbit 348 | wait 1s 349 | rate 75kbit 350 | wait 1s 351 | rate 75kbit 352 | wait 1s 353 | rate 75kbit 354 | wait 1s 355 | rate 75kbit 356 | wait 1s 357 | rate 1306kbit 358 | wait 1s 359 | rate 955kbit 360 | wait 1s 361 | rate 1192kbit 362 | wait 1s 363 | rate 903kbit 364 | wait 1s 365 | rate 1209kbit 366 | wait 1s 367 | rate 1232kbit 368 | wait 1s 369 | rate 1005kbit 370 | wait 1s 371 | rate 1058kbit 372 | wait 1s 373 | rate 1075kbit 374 | wait 1s 375 | rate 535kbit 376 | wait 1s 377 | rate 545kbit 378 | wait 1s 379 | rate 768kbit 380 | wait 1s 381 | rate 207kbit 382 | wait 1s 383 | rate 544kbit 384 | wait 1s 385 | rate 473kbit 386 | wait 1s 387 | rate 213kbit 388 | wait 1s 389 | rate 584kbit 390 | wait 1s 391 | rate 438kbit 392 | wait 1s 393 | rate 578kbit 394 | wait 1s 395 | rate 380kbit 396 | wait 1s 397 | rate 107kbit 398 | wait 1s 399 | rate 75kbit 400 | wait 1s 401 | rate 182kbit 402 | wait 1s 403 | rate 82kbit 404 | wait 1s 405 | rate 975kbit 406 | wait 1s 407 | rate 624kbit 408 | wait 1s 409 | rate 966kbit 410 | wait 1s 411 | rate 837kbit 412 | wait 1s 413 | rate 1081kbit 414 | wait 1s 415 | rate 987kbit 416 | wait 1s 417 | rate 469kbit 418 | wait 1s 419 | rate 395kbit 420 | wait 1s 421 | rate 406kbit 422 | wait 1s 423 | rate 216kbit 424 | wait 1s 425 | rate 119kbit 426 | wait 1s 427 | rate 119kbit 428 | wait 1s 429 | rate 119kbit 430 | wait 1s 431 | rate 119kbit 432 | wait 1s 433 | rate 173kbit 434 | wait 1s 435 | rate 1349kbit 436 | wait 1s 437 | rate 966kbit 438 | wait 1s 439 | rate 1329kbit 440 | wait 1s 441 | rate 885kbit 442 | wait 1s 443 | rate 1594kbit 444 | wait 1s 445 | rate 979kbit 446 | wait 1s 447 | rate 1074kbit 448 | wait 1s 449 | rate 1000kbit 450 | wait 1s 451 | rate 1168kbit 452 | wait 1s 453 | rate 1436kbit 454 | wait 1s 455 | rate 731kbit 456 | wait 1s 457 | rate 374kbit 458 | wait 1s 459 | rate 1004kbit 460 | wait 1s 461 | rate 1051kbit 462 | wait 1s 463 | rate 873kbit 464 | wait 1s 465 | rate 542kbit 466 | wait 1s 467 | rate 1072kbit 468 | wait 1s 469 | rate 960kbit 470 | wait 1s 471 | rate 884kbit 472 | wait 1s 473 | rate 1115kbit 474 | wait 1s 475 | rate 1502kbit 476 | wait 1s 477 | rate 1713kbit 478 | wait 1s 479 | rate 994kbit 480 | wait 1s 481 | rate 1366kbit 482 | wait 1s 483 | rate 1258kbit 484 | wait 1s 485 | rate 960kbit 486 | wait 1s 487 | rate 762kbit 488 | wait 1s 489 | rate 790kbit 490 | wait 1s 491 | rate 813kbit 492 | wait 1s 493 | rate 716kbit 494 | wait 1s 495 | rate 740kbit 496 | wait 1s 497 | rate 617kbit 498 | wait 1s 499 | rate 508kbit 500 | wait 1s 501 | rate 518kbit 502 | wait 1s 503 | rate 218kbit 504 | wait 1s 505 | rate 600kbit 506 | wait 1s 507 | rate 620kbit 508 | wait 1s 509 | rate 1131kbit 510 | wait 1s 511 | rate 801kbit 512 | wait 1s 513 | rate 640kbit 514 | wait 1s 515 | rate 708kbit 516 | wait 1s 517 | rate 478kbit 518 | wait 1s 519 | rate 454kbit 520 | wait 1s 521 | rate 211kbit 522 | wait 1s 523 | rate 75kbit 524 | wait 1s 525 | rate 75kbit 526 | wait 1s 527 | rate 83kbit 528 | wait 1s 529 | rate 766kbit 530 | wait 1s 531 | rate 766kbit 532 | wait 1s 533 | rate 75kbit 534 | wait 1s 535 | rate 851kbit 536 | wait 1s 537 | rate 205kbit 538 | wait 1s 539 | rate 122kbit 540 | wait 1s 541 | rate 583kbit 542 | wait 1s 543 | rate 283kbit 544 | wait 1s 545 | rate 168kbit 546 | wait 1s 547 | rate 221kbit 548 | wait 1s 549 | rate 761kbit 550 | wait 1s 551 | rate 723kbit 552 | wait 1s 553 | rate 197kbit 554 | wait 1s 555 | rate 154kbit 556 | wait 1s 557 | rate 84kbit 558 | wait 1s 559 | rate 104kbit 560 | wait 1s 561 | rate 144kbit 562 | wait 1s 563 | rate 203kbit 564 | wait 1s 565 | rate 180kbit 566 | wait 1s 567 | rate 149kbit 568 | wait 1s 569 | rate 225kbit 570 | wait 1s 571 | rate 318kbit 572 | wait 1s 573 | rate 218kbit 574 | wait 1s 575 | rate 350kbit 576 | wait 1s 577 | rate 426kbit 578 | wait 1s 579 | rate 540kbit 580 | wait 1s 581 | rate 392kbit 582 | wait 1s 583 | rate 554kbit 584 | wait 1s 585 | rate 855kbit 586 | wait 1s 587 | rate 978kbit 588 | wait 1s 589 | rate 1143kbit 590 | wait 1s 591 | rate 968kbit 592 | wait 1s 593 | rate 659kbit 594 | wait 1s 595 | rate 347kbit 596 | wait 1s 597 | rate 402kbit 598 | wait 1s 599 | rate 693kbit 600 | wait 1s 601 | rate 310kbit 602 | wait 1s 603 | rate 306kbit 604 | wait 1s 605 | rate 518kbit 606 | wait 1s 607 | rate 806kbit 608 | wait 1s 609 | rate 609kbit 610 | wait 1s 611 | rate 827kbit 612 | wait 1s 613 | rate 490kbit 614 | wait 1s 615 | rate 192kbit 616 | wait 1s 617 | rate 101kbit 618 | wait 1s 619 | rate 219kbit 620 | wait 1s 621 | rate 224kbit 622 | wait 1s 623 | rate 331kbit 624 | wait 1s 625 | rate 397kbit 626 | wait 1s 627 | rate 206kbit 628 | wait 1s 629 | rate 335kbit 630 | wait 1s 631 | rate 360kbit 632 | wait 1s 633 | rate 360kbit 634 | wait 1s 635 | rate 75kbit 636 | wait 1s 637 | rate 863kbit 638 | wait 1s 639 | rate 1226kbit 640 | wait 1s 641 | rate 923kbit 642 | wait 1s 643 | rate 550kbit 644 | wait 1s 645 | rate 816kbit 646 | wait 1s 647 | rate 729kbit 648 | wait 1s 649 | rate 702kbit 650 | wait 1s 651 | rate 1297kbit 652 | wait 1s 653 | rate 1213kbit 654 | wait 1s 655 | rate 727kbit 656 | wait 1s 657 | rate 485kbit 658 | wait 1s 659 | rate 811kbit 660 | wait 1s 661 | rate 379kbit 662 | wait 1s 663 | rate 545kbit 664 | wait 1s 665 | rate 330kbit 666 | wait 1s 667 | rate 563kbit 668 | wait 1s 669 | rate 558kbit 670 | wait 1s 671 | rate 479kbit 672 | wait 1s 673 | rate 413kbit 674 | wait 1s 675 | rate 664kbit 676 | wait 1s 677 | rate 1078kbit 678 | wait 1s 679 | rate 803kbit 680 | wait 1s 681 | rate 990kbit 682 | wait 1s 683 | rate 734kbit 684 | wait 1s 685 | rate 1283kbit 686 | wait 1s 687 | rate 582kbit 688 | wait 1s 689 | rate 388kbit 690 | wait 1s 691 | rate 671kbit 692 | wait 1s 693 | rate 1060kbit 694 | wait 1s 695 | rate 669kbit 696 | wait 1s 697 | rate 1348kbit 698 | wait 1s 699 | rate 1215kbit 700 | wait 1s 701 | rate 1047kbit 702 | wait 1s 703 | rate 1121kbit 704 | wait 1s 705 | rate 952kbit 706 | wait 1s 707 | rate 786kbit 708 | wait 1s 709 | rate 1125kbit 710 | wait 1s 711 | rate 869kbit 712 | wait 1s 713 | rate 1104kbit 714 | wait 1s 715 | rate 776kbit 716 | wait 1s 717 | rate 1006kbit 718 | wait 1s 719 | rate 1154kbit 720 | wait 1s 721 | rate 738kbit 722 | wait 1s 723 | rate 1200kbit 724 | wait 1s 725 | rate 1012kbit 726 | wait 1s 727 | rate 1311kbit 728 | wait 1s 729 | rate 1771kbit 730 | wait 1s 731 | rate 1122kbit 732 | wait 1s 733 | rate 1022kbit 734 | wait 1s 735 | rate 1786kbit 736 | wait 1s 737 | rate 1368kbit 738 | wait 1s 739 | rate 1093kbit 740 | wait 1s 741 | rate 900kbit 742 | wait 1s 743 | rate 1259kbit 744 | wait 1s 745 | rate 994kbit 746 | wait 1s 747 | rate 943kbit 748 | wait 1s 749 | rate 969kbit 750 | wait 1s 751 | rate 909kbit 752 | wait 1s 753 | rate 1005kbit 754 | wait 1s 755 | rate 919kbit 756 | wait 1s 757 | rate 843kbit 758 | wait 1s 759 | rate 663kbit 760 | wait 1s 761 | rate 439kbit 762 | wait 1s 763 | rate 479kbit 764 | wait 1s 765 | rate 203kbit 766 | wait 1s 767 | rate 203kbit 768 | wait 1s 769 | rate 283kbit 770 | wait 1s 771 | rate 75kbit 772 | wait 1s 773 | rate 75kbit 774 | wait 1s 775 | rate 771kbit 776 | wait 1s 777 | rate 454kbit 778 | wait 1s 779 | rate 681kbit 780 | wait 1s 781 | rate 496kbit 782 | wait 1s 783 | rate 219kbit 784 | wait 1s 785 | rate 594kbit 786 | wait 1s 787 | rate 497kbit 788 | wait 1s 789 | rate 595kbit 790 | wait 1s 791 | rate 586kbit 792 | wait 1s 793 | rate 505kbit 794 | wait 1s 795 | rate 876kbit 796 | wait 1s 797 | rate 894kbit 798 | wait 1s 799 | rate 488kbit 800 | wait 1s 801 | rate 477kbit 802 | wait 1s 803 | rate 673kbit 804 | wait 1s 805 | rate 599kbit 806 | wait 1s 807 | rate 593kbit 808 | wait 1s 809 | rate 645kbit 810 | wait 1s 811 | rate 706kbit 812 | wait 1s 813 | rate 518kbit 814 | wait 1s 815 | rate 653kbit 816 | wait 1s 817 | rate 563kbit 818 | wait 1s 819 | rate 632kbit 820 | wait 1s 821 | rate 377kbit 822 | wait 1s 823 | rate 243kbit 824 | wait 1s 825 | rate 130kbit 826 | wait 1s 827 | rate 194kbit 828 | wait 1s 829 | rate 75kbit 830 | wait 1s 831 | rate 75kbit 832 | wait 1s 833 | rate 127kbit 834 | wait 1s 835 | rate 144kbit 836 | wait 1s 837 | rate 318kbit 838 | wait 1s 839 | rate 469kbit 840 | wait 1s 841 | rate 534kbit 842 | wait 1s 843 | rate 358kbit 844 | wait 1s 845 | rate 306kbit 846 | wait 1s 847 | rate 240kbit 848 | wait 1s 849 | rate 605kbit 850 | wait 1s 851 | rate 456kbit 852 | wait 1s 853 | rate 608kbit 854 | wait 1s 855 | rate 736kbit 856 | wait 1s 857 | rate 840kbit 858 | wait 1s 859 | rate 712kbit 860 | wait 1s 861 | rate 514kbit 862 | wait 1s 863 | rate 652kbit 864 | wait 1s 865 | rate 568kbit 866 | wait 1s 867 | rate 503kbit 868 | wait 1s 869 | rate 499kbit 870 | wait 1s 871 | rate 661kbit 872 | wait 1s 873 | rate 543kbit 874 | wait 1s 875 | rate 643kbit 876 | wait 1s 877 | rate 599kbit 878 | wait 1s 879 | rate 860kbit 880 | wait 1s 881 | rate 658kbit 882 | wait 1s 883 | rate 267kbit 884 | wait 1s 885 | rate 303kbit 886 | wait 1s 887 | rate 368kbit 888 | wait 1s 889 | rate 75kbit 890 | wait 1s 891 | rate 542kbit 892 | wait 1s 893 | rate 75kbit 894 | wait 1s 895 | rate 397kbit 896 | wait 1s 897 | rate 517kbit 898 | wait 1s 899 | rate 433kbit 900 | wait 1s 901 | rate 563kbit 902 | wait 1s 903 | rate 541kbit 904 | wait 1s 905 | rate 566kbit 906 | wait 1s 907 | rate 335kbit 908 | wait 1s 909 | rate 850kbit 910 | wait 1s 911 | rate 591kbit 912 | wait 1s 913 | rate 281kbit 914 | wait 1s 915 | rate 1752kbit 916 | wait 1s 917 | rate 671kbit 918 | wait 1s 919 | rate 951kbit 920 | wait 1s 921 | rate 667kbit 922 | wait 1s 923 | rate 1038kbit 924 | wait 1s 925 | rate 827kbit 926 | wait 1s 927 | rate 1059kbit 928 | wait 1s 929 | rate 1013kbit 930 | wait 1s 931 | rate 1045kbit 932 | wait 1s 933 | rate 974kbit 934 | wait 1s 935 | rate 1024kbit 936 | wait 1s 937 | rate 901kbit 938 | wait 1s 939 | rate 1171kbit 940 | wait 1s 941 | rate 1041kbit 942 | wait 1s 943 | rate 907kbit 944 | wait 1s 945 | rate 1239kbit 946 | wait 1s 947 | rate 1170kbit 948 | wait 1s 949 | rate 809kbit 950 | wait 1s 951 | rate 1320kbit 952 | wait 1s 953 | rate 941kbit 954 | wait 1s 955 | rate 944kbit 956 | wait 1s 957 | rate 570kbit 958 | wait 1s 959 | rate 1124kbit 960 | wait 1s 961 | rate 947kbit 962 | wait 1s 963 | rate 998kbit 964 | wait 1s 965 | rate 1423kbit 966 | wait 1s 967 | rate 1355kbit 968 | wait 1s 969 | rate 1407kbit 970 | wait 1s 971 | rate 894kbit 972 | wait 1s 973 | rate 1035kbit 974 | wait 1s 975 | rate 702kbit 976 | wait 1s 977 | rate 1289kbit 978 | wait 1s 979 | rate 997kbit 980 | wait 1s 981 | rate 1226kbit 982 | wait 1s 983 | rate 970kbit 984 | wait 1s 985 | rate 797kbit 986 | wait 1s 987 | rate 1686kbit 988 | wait 1s 989 | rate 1696kbit 990 | wait 1s 991 | rate 1651kbit 992 | wait 1s 993 | rate 1412kbit 994 | wait 1s 995 | rate 1483kbit 996 | wait 1s 997 | rate 909kbit 998 | wait 1s 999 | rate 1580kbit 1000 | wait 1s 1001 | rate 1259kbit 1002 | wait 1s 1003 | rate 1217kbit 1004 | wait 1s 1005 | rate 1364kbit 1006 | wait 1s 1007 | rate 930kbit 1008 | wait 1s 1009 | rate 1127kbit 1010 | wait 1s 1011 | rate 1378kbit 1012 | wait 1s 1013 | rate 173kbit 1014 | wait 1s 1015 | rate 302kbit 1016 | wait 1s 1017 | rate 374kbit 1018 | wait 1s 1019 | rate 222kbit 1020 | wait 1s 1021 | rate 656kbit 1022 | wait 1s 1023 | rate 342kbit 1024 | wait 1s 1025 | rate 384kbit 1026 | wait 1s 1027 | rate 391kbit 1028 | wait 1s 1029 | rate 1011kbit 1030 | wait 1s 1031 | rate 647kbit 1032 | wait 1s 1033 | rate 703kbit 1034 | wait 1s 1035 | rate 884kbit 1036 | wait 1s 1037 | rate 508kbit 1038 | wait 1s 1039 | rate 253kbit 1040 | wait 1s 1041 | rate 468kbit 1042 | wait 1s 1043 | rate 790kbit 1044 | wait 1s 1045 | rate 340kbit 1046 | wait 1s 1047 | rate 446kbit 1048 | wait 1s 1049 | rate 641kbit 1050 | wait 1s 1051 | rate 848kbit 1052 | wait 1s 1053 | rate 689kbit 1054 | wait 1s 1055 | rate 1297kbit 1056 | wait 1s 1057 | rate 1276kbit 1058 | wait 1s 1059 | rate 1643kbit 1060 | wait 1s 1061 | rate 1639kbit 1062 | wait 1s 1063 | rate 1171kbit 1064 | wait 1s 1065 | rate 434kbit 1066 | wait 1s 1067 | rate 287kbit 1068 | wait 1s 1069 | rate 1292kbit 1070 | wait 1s 1071 | rate 1556kbit 1072 | wait 1s 1073 | rate 1339kbit 1074 | wait 1s 1075 | rate 1050kbit 1076 | wait 1s 1077 | rate 390kbit 1078 | wait 1s 1079 | rate 1107kbit 1080 | wait 1s 1081 | rate 635kbit 1082 | wait 1s 1083 | rate 75kbit 1084 | wait 1s 1085 | rate 633kbit 1086 | wait 1s 1087 | rate 494kbit 1088 | wait 1s 1089 | rate 221kbit 1090 | wait 1s 1091 | rate 253kbit 1092 | wait 1s 1093 | rate 320kbit 1094 | wait 1s 1095 | rate 212kbit 1096 | wait 1s 1097 | rate 247kbit 1098 | wait 1s 1099 | rate 104kbit 1100 | wait 1s 1101 | rate 125kbit 1102 | wait 1s 1103 | rate 153kbit 1104 | wait 1s 1105 | rate 272kbit 1106 | wait 1s 1107 | rate 776kbit 1108 | wait 1s 1109 | rate 1033kbit 1110 | wait 1s 1111 | rate 942kbit 1112 | wait 1s 1113 | rate 742kbit 1114 | wait 1s 1115 | rate 894kbit 1116 | wait 1s 1117 | rate 620kbit 1118 | wait 1s 1119 | rate 582kbit 1120 | wait 1s 1121 | rate 625kbit 1122 | wait 1s 1123 | rate 606kbit 1124 | wait 1s 1125 | rate 654kbit 1126 | wait 1s 1127 | rate 1212kbit 1128 | wait 1s 1129 | rate 1062kbit 1130 | wait 1s 1131 | rate 1151kbit 1132 | wait 1s 1133 | rate 1220kbit 1134 | wait 1s 1135 | rate 1167kbit 1136 | wait 1s 1137 | rate 876kbit 1138 | wait 1s 1139 | rate 397kbit 1140 | wait 1s 1141 | rate 1771kbit 1142 | wait 1s 1143 | rate 1892kbit 1144 | wait 1s 1145 | rate 1160kbit 1146 | wait 1s 1147 | rate 990kbit 1148 | wait 1s 1149 | rate 876kbit 1150 | wait 1s 1151 | rate 579kbit 1152 | wait 1s 1153 | rate 989kbit 1154 | wait 1s 1155 | rate 1322kbit 1156 | wait 1s 1157 | rate 1138kbit 1158 | wait 1s 1159 | rate 1207kbit 1160 | wait 1s 1161 | rate 985kbit 1162 | wait 1s 1163 | rate 1063kbit 1164 | wait 1s 1165 | rate 979kbit 1166 | wait 1s 1167 | rate 870kbit 1168 | wait 1s 1169 | rate 1614kbit 1170 | wait 1s 1171 | rate 1408kbit 1172 | wait 1s 1173 | rate 1215kbit 1174 | wait 1s 1175 | rate 1226kbit 1176 | wait 1s 1177 | rate 1018kbit 1178 | wait 1s 1179 | rate 1299kbit 1180 | wait 1s 1181 | rate 459kbit 1182 | wait 1s 1183 | rate 1705kbit 1184 | wait 1s 1185 | rate 1517kbit 1186 | wait 1s 1187 | rate 828kbit 1188 | wait 1s 1189 | rate 735kbit 1190 | wait 1s 1191 | rate 912kbit 1192 | wait 1s 1193 | rate 771kbit 1194 | wait 1s 1195 | rate 637kbit 1196 | wait 1s 1197 | rate 649kbit 1198 | wait 1s 1199 | rate 598kbit 1200 | wait 1s 1201 | -------------------------------------------------------------------------------- /tc_profiles/lte_profile_x3: -------------------------------------------------------------------------------- 1 | rate 2079kbit 2 | wait 1s 3 | rate 3624kbit 4 | wait 1s 5 | rate 4488kbit 6 | wait 1s 7 | rate 2673kbit 8 | wait 1s 9 | rate 7872kbit 10 | wait 1s 11 | rate 4110kbit 12 | wait 1s 13 | rate 4611kbit 14 | wait 1s 15 | rate 4698kbit 16 | wait 1s 17 | rate 12138kbit 18 | wait 1s 19 | rate 7770kbit 20 | wait 1s 21 | rate 8439kbit 22 | wait 1s 23 | rate 10617kbit 24 | wait 1s 25 | rate 6099kbit 26 | wait 1s 27 | rate 3042kbit 28 | wait 1s 29 | rate 5625kbit 30 | wait 1s 31 | rate 9489kbit 32 | wait 1s 33 | rate 4086kbit 34 | wait 1s 35 | rate 5361kbit 36 | wait 1s 37 | rate 7698kbit 38 | wait 1s 39 | rate 10176kbit 40 | wait 1s 41 | rate 8274kbit 42 | wait 1s 43 | rate 15564kbit 44 | wait 1s 45 | rate 15318kbit 46 | wait 1s 47 | rate 19725kbit 48 | wait 1s 49 | rate 19671kbit 50 | wait 1s 51 | rate 14052kbit 52 | wait 1s 53 | rate 5211kbit 54 | wait 1s 55 | rate 3444kbit 56 | wait 1s 57 | rate 15504kbit 58 | wait 1s 59 | rate 18672kbit 60 | wait 1s 61 | rate 16074kbit 62 | wait 1s 63 | rate 12600kbit 64 | wait 1s 65 | rate 4689kbit 66 | wait 1s 67 | rate 13284kbit 68 | wait 1s 69 | rate 7623kbit 70 | wait 1s 71 | rate 900kbit 72 | wait 1s 73 | rate 7596kbit 74 | wait 1s 75 | rate 5931kbit 76 | wait 1s 77 | rate 2661kbit 78 | wait 1s 79 | rate 3045kbit 80 | wait 1s 81 | rate 3843kbit 82 | wait 1s 83 | rate 2550kbit 84 | wait 1s 85 | rate 2973kbit 86 | wait 1s 87 | rate 1251kbit 88 | wait 1s 89 | rate 1509kbit 90 | wait 1s 91 | rate 1839kbit 92 | wait 1s 93 | rate 3264kbit 94 | wait 1s 95 | rate 9321kbit 96 | wait 1s 97 | rate 12399kbit 98 | wait 1s 99 | rate 11304kbit 100 | wait 1s 101 | rate 8910kbit 102 | wait 1s 103 | rate 10734kbit 104 | wait 1s 105 | rate 7440kbit 106 | wait 1s 107 | rate 6990kbit 108 | wait 1s 109 | rate 7503kbit 110 | wait 1s 111 | rate 7275kbit 112 | wait 1s 113 | rate 7857kbit 114 | wait 1s 115 | rate 14547kbit 116 | wait 1s 117 | rate 12744kbit 118 | wait 1s 119 | rate 13812kbit 120 | wait 1s 121 | rate 14646kbit 122 | wait 1s 123 | rate 14007kbit 124 | wait 1s 125 | rate 10518kbit 126 | wait 1s 127 | rate 4767kbit 128 | wait 1s 129 | rate 21258kbit 130 | wait 1s 131 | rate 22707kbit 132 | wait 1s 133 | rate 13926kbit 134 | wait 1s 135 | rate 11883kbit 136 | wait 1s 137 | rate 10515kbit 138 | wait 1s 139 | rate 6948kbit 140 | wait 1s 141 | rate 11871kbit 142 | wait 1s 143 | rate 15870kbit 144 | wait 1s 145 | rate 13662kbit 146 | wait 1s 147 | rate 14490kbit 148 | wait 1s 149 | rate 11826kbit 150 | wait 1s 151 | rate 12759kbit 152 | wait 1s 153 | rate 11751kbit 154 | wait 1s 155 | rate 10443kbit 156 | wait 1s 157 | rate 19371kbit 158 | wait 1s 159 | rate 16902kbit 160 | wait 1s 161 | rate 14580kbit 162 | wait 1s 163 | rate 14718kbit 164 | wait 1s 165 | rate 12219kbit 166 | wait 1s 167 | rate 15591kbit 168 | wait 1s 169 | rate 5517kbit 170 | wait 1s 171 | rate 20466kbit 172 | wait 1s 173 | rate 18204kbit 174 | wait 1s 175 | rate 9939kbit 176 | wait 1s 177 | rate 8829kbit 178 | wait 1s 179 | rate 10944kbit 180 | wait 1s 181 | rate 9255kbit 182 | wait 1s 183 | rate 7650kbit 184 | wait 1s 185 | rate 7788kbit 186 | wait 1s 187 | rate 7179kbit 188 | wait 1s 189 | rate 6045kbit 190 | wait 1s 191 | rate 9207kbit 192 | wait 1s 193 | rate 8211kbit 194 | wait 1s 195 | rate 2568kbit 196 | wait 1s 197 | rate 3792kbit 198 | wait 1s 199 | rate 5481kbit 200 | wait 1s 201 | rate 10509kbit 202 | wait 1s 203 | rate 7491kbit 204 | wait 1s 205 | rate 7803kbit 206 | wait 1s 207 | rate 12861kbit 208 | wait 1s 209 | rate 12390kbit 210 | wait 1s 211 | rate 6657kbit 212 | wait 1s 213 | rate 10380kbit 214 | wait 1s 215 | rate 14718kbit 216 | wait 1s 217 | rate 17238kbit 218 | wait 1s 219 | rate 17589kbit 220 | wait 1s 221 | rate 17358kbit 222 | wait 1s 223 | rate 20331kbit 224 | wait 1s 225 | rate 9714kbit 226 | wait 1s 227 | rate 12453kbit 228 | wait 1s 229 | rate 18243kbit 230 | wait 1s 231 | rate 12594kbit 232 | wait 1s 233 | rate 16491kbit 234 | wait 1s 235 | rate 10791kbit 236 | wait 1s 237 | rate 16455kbit 238 | wait 1s 239 | rate 14331kbit 240 | wait 1s 241 | rate 13875kbit 242 | wait 1s 243 | rate 13623kbit 244 | wait 1s 245 | rate 9582kbit 246 | wait 1s 247 | rate 16167kbit 248 | wait 1s 249 | rate 14547kbit 250 | wait 1s 251 | rate 15510kbit 252 | wait 1s 253 | rate 14316kbit 254 | wait 1s 255 | rate 10089kbit 256 | wait 1s 257 | rate 14946kbit 258 | wait 1s 259 | rate 14013kbit 260 | wait 1s 261 | rate 20037kbit 262 | wait 1s 263 | rate 13443kbit 264 | wait 1s 265 | rate 13260kbit 266 | wait 1s 267 | rate 11181kbit 268 | wait 1s 269 | rate 17400kbit 270 | wait 1s 271 | rate 16998kbit 272 | wait 1s 273 | rate 12132kbit 274 | wait 1s 275 | rate 14952kbit 276 | wait 1s 277 | rate 11685kbit 278 | wait 1s 279 | rate 15225kbit 280 | wait 1s 281 | rate 7065kbit 282 | wait 1s 283 | rate 7317kbit 284 | wait 1s 285 | rate 4824kbit 286 | wait 1s 287 | rate 2400kbit 288 | wait 1s 289 | rate 2400kbit 290 | wait 1s 291 | rate 2400kbit 292 | wait 1s 293 | rate 2400kbit 294 | wait 1s 295 | rate 10803kbit 296 | wait 1s 297 | rate 1992kbit 298 | wait 1s 299 | rate 2292kbit 300 | wait 1s 301 | rate 2343kbit 302 | wait 1s 303 | rate 7170kbit 304 | wait 1s 305 | rate 1632kbit 306 | wait 1s 307 | rate 2697kbit 308 | wait 1s 309 | rate 6741kbit 310 | wait 1s 311 | rate 8511kbit 312 | wait 1s 313 | rate 5067kbit 314 | wait 1s 315 | rate 6318kbit 316 | wait 1s 317 | rate 9963kbit 318 | wait 1s 319 | rate 4203kbit 320 | wait 1s 321 | rate 1020kbit 322 | wait 1s 323 | rate 987kbit 324 | wait 1s 325 | rate 900kbit 326 | wait 1s 327 | rate 900kbit 328 | wait 1s 329 | rate 909kbit 330 | wait 1s 331 | rate 1167kbit 332 | wait 1s 333 | rate 900kbit 334 | wait 1s 335 | rate 900kbit 336 | wait 1s 337 | rate 900kbit 338 | wait 1s 339 | rate 2760kbit 340 | wait 1s 341 | rate 900kbit 342 | wait 1s 343 | rate 900kbit 344 | wait 1s 345 | rate 900kbit 346 | wait 1s 347 | rate 900kbit 348 | wait 1s 349 | rate 900kbit 350 | wait 1s 351 | rate 900kbit 352 | wait 1s 353 | rate 900kbit 354 | wait 1s 355 | rate 900kbit 356 | wait 1s 357 | rate 15675kbit 358 | wait 1s 359 | rate 11460kbit 360 | wait 1s 361 | rate 14310kbit 362 | wait 1s 363 | rate 10842kbit 364 | wait 1s 365 | rate 14511kbit 366 | wait 1s 367 | rate 14793kbit 368 | wait 1s 369 | rate 12069kbit 370 | wait 1s 371 | rate 12702kbit 372 | wait 1s 373 | rate 12909kbit 374 | wait 1s 375 | rate 6429kbit 376 | wait 1s 377 | rate 6540kbit 378 | wait 1s 379 | rate 9216kbit 380 | wait 1s 381 | rate 2484kbit 382 | wait 1s 383 | rate 6531kbit 384 | wait 1s 385 | rate 5676kbit 386 | wait 1s 387 | rate 2565kbit 388 | wait 1s 389 | rate 7014kbit 390 | wait 1s 391 | rate 5265kbit 392 | wait 1s 393 | rate 6939kbit 394 | wait 1s 395 | rate 4569kbit 396 | wait 1s 397 | rate 1284kbit 398 | wait 1s 399 | rate 900kbit 400 | wait 1s 401 | rate 2184kbit 402 | wait 1s 403 | rate 984kbit 404 | wait 1s 405 | rate 11709kbit 406 | wait 1s 407 | rate 7497kbit 408 | wait 1s 409 | rate 11592kbit 410 | wait 1s 411 | rate 10053kbit 412 | wait 1s 413 | rate 12975kbit 414 | wait 1s 415 | rate 11853kbit 416 | wait 1s 417 | rate 5628kbit 418 | wait 1s 419 | rate 4746kbit 420 | wait 1s 421 | rate 4872kbit 422 | wait 1s 423 | rate 2595kbit 424 | wait 1s 425 | rate 1431kbit 426 | wait 1s 427 | rate 1431kbit 428 | wait 1s 429 | rate 1431kbit 430 | wait 1s 431 | rate 1431kbit 432 | wait 1s 433 | rate 2076kbit 434 | wait 1s 435 | rate 16188kbit 436 | wait 1s 437 | rate 11592kbit 438 | wait 1s 439 | rate 15951kbit 440 | wait 1s 441 | rate 10626kbit 442 | wait 1s 443 | rate 19128kbit 444 | wait 1s 445 | rate 11754kbit 446 | wait 1s 447 | rate 12897kbit 448 | wait 1s 449 | rate 12006kbit 450 | wait 1s 451 | rate 14022kbit 452 | wait 1s 453 | rate 17241kbit 454 | wait 1s 455 | rate 8775kbit 456 | wait 1s 457 | rate 4494kbit 458 | wait 1s 459 | rate 12054kbit 460 | wait 1s 461 | rate 12618kbit 462 | wait 1s 463 | rate 10482kbit 464 | wait 1s 465 | rate 6504kbit 466 | wait 1s 467 | rate 12873kbit 468 | wait 1s 469 | rate 11529kbit 470 | wait 1s 471 | rate 10608kbit 472 | wait 1s 473 | rate 13383kbit 474 | wait 1s 475 | rate 18027kbit 476 | wait 1s 477 | rate 20559kbit 478 | wait 1s 479 | rate 11937kbit 480 | wait 1s 481 | rate 16392kbit 482 | wait 1s 483 | rate 15099kbit 484 | wait 1s 485 | rate 11529kbit 486 | wait 1s 487 | rate 9147kbit 488 | wait 1s 489 | rate 9486kbit 490 | wait 1s 491 | rate 9756kbit 492 | wait 1s 493 | rate 8592kbit 494 | wait 1s 495 | rate 8883kbit 496 | wait 1s 497 | rate 7407kbit 498 | wait 1s 499 | rate 6099kbit 500 | wait 1s 501 | rate 6216kbit 502 | wait 1s 503 | rate 2616kbit 504 | wait 1s 505 | rate 7203kbit 506 | wait 1s 507 | rate 7440kbit 508 | wait 1s 509 | rate 13578kbit 510 | wait 1s 511 | rate 9615kbit 512 | wait 1s 513 | rate 7686kbit 514 | wait 1s 515 | rate 8499kbit 516 | wait 1s 517 | rate 5745kbit 518 | wait 1s 519 | rate 5451kbit 520 | wait 1s 521 | rate 2535kbit 522 | wait 1s 523 | rate 900kbit 524 | wait 1s 525 | rate 900kbit 526 | wait 1s 527 | rate 1005kbit 528 | wait 1s 529 | rate 9201kbit 530 | wait 1s 531 | rate 9201kbit 532 | wait 1s 533 | rate 900kbit 534 | wait 1s 535 | rate 10218kbit 536 | wait 1s 537 | rate 2460kbit 538 | wait 1s 539 | rate 1464kbit 540 | wait 1s 541 | rate 7002kbit 542 | wait 1s 543 | rate 3396kbit 544 | wait 1s 545 | rate 2025kbit 546 | wait 1s 547 | rate 2652kbit 548 | wait 1s 549 | rate 9135kbit 550 | wait 1s 551 | rate 8685kbit 552 | wait 1s 553 | rate 2373kbit 554 | wait 1s 555 | rate 1848kbit 556 | wait 1s 557 | rate 1014kbit 558 | wait 1s 559 | rate 1257kbit 560 | wait 1s 561 | rate 1734kbit 562 | wait 1s 563 | rate 2445kbit 564 | wait 1s 565 | rate 2163kbit 566 | wait 1s 567 | rate 1794kbit 568 | wait 1s 569 | rate 2700kbit 570 | wait 1s 571 | rate 3822kbit 572 | wait 1s 573 | rate 2622kbit 574 | wait 1s 575 | rate 4206kbit 576 | wait 1s 577 | rate 5112kbit 578 | wait 1s 579 | rate 6486kbit 580 | wait 1s 581 | rate 4710kbit 582 | wait 1s 583 | rate 6657kbit 584 | wait 1s 585 | rate 10260kbit 586 | wait 1s 587 | rate 11745kbit 588 | wait 1s 589 | rate 13725kbit 590 | wait 1s 591 | rate 11622kbit 592 | wait 1s 593 | rate 7914kbit 594 | wait 1s 595 | rate 4164kbit 596 | wait 1s 597 | rate 4824kbit 598 | wait 1s 599 | rate 8319kbit 600 | wait 1s 601 | rate 3726kbit 602 | wait 1s 603 | rate 3672kbit 604 | wait 1s 605 | rate 6219kbit 606 | wait 1s 607 | rate 9678kbit 608 | wait 1s 609 | rate 7311kbit 610 | wait 1s 611 | rate 9924kbit 612 | wait 1s 613 | rate 5886kbit 614 | wait 1s 615 | rate 2313kbit 616 | wait 1s 617 | rate 1215kbit 618 | wait 1s 619 | rate 2628kbit 620 | wait 1s 621 | rate 2688kbit 622 | wait 1s 623 | rate 3975kbit 624 | wait 1s 625 | rate 4770kbit 626 | wait 1s 627 | rate 2481kbit 628 | wait 1s 629 | rate 4023kbit 630 | wait 1s 631 | rate 4320kbit 632 | wait 1s 633 | rate 4320kbit 634 | wait 1s 635 | rate 900kbit 636 | wait 1s 637 | rate 10359kbit 638 | wait 1s 639 | rate 14715kbit 640 | wait 1s 641 | rate 11085kbit 642 | wait 1s 643 | rate 6609kbit 644 | wait 1s 645 | rate 9792kbit 646 | wait 1s 647 | rate 8751kbit 648 | wait 1s 649 | rate 8433kbit 650 | wait 1s 651 | rate 15564kbit 652 | wait 1s 653 | rate 14565kbit 654 | wait 1s 655 | rate 8727kbit 656 | wait 1s 657 | rate 5829kbit 658 | wait 1s 659 | rate 9735kbit 660 | wait 1s 661 | rate 4548kbit 662 | wait 1s 663 | rate 6549kbit 664 | wait 1s 665 | rate 3969kbit 666 | wait 1s 667 | rate 6759kbit 668 | wait 1s 669 | rate 6705kbit 670 | wait 1s 671 | rate 5748kbit 672 | wait 1s 673 | rate 4965kbit 674 | wait 1s 675 | rate 7968kbit 676 | wait 1s 677 | rate 12942kbit 678 | wait 1s 679 | rate 9642kbit 680 | wait 1s 681 | rate 11889kbit 682 | wait 1s 683 | rate 8817kbit 684 | wait 1s 685 | rate 15396kbit 686 | wait 1s 687 | rate 6990kbit 688 | wait 1s 689 | rate 4665kbit 690 | wait 1s 691 | rate 8061kbit 692 | wait 1s 693 | rate 12720kbit 694 | wait 1s 695 | rate 8034kbit 696 | wait 1s 697 | rate 16185kbit 698 | wait 1s 699 | rate 14589kbit 700 | wait 1s 701 | rate 12570kbit 702 | wait 1s 703 | rate 13452kbit 704 | wait 1s 705 | rate 11424kbit 706 | wait 1s 707 | rate 9432kbit 708 | wait 1s 709 | rate 13503kbit 710 | wait 1s 711 | rate 10434kbit 712 | wait 1s 713 | rate 13248kbit 714 | wait 1s 715 | rate 9315kbit 716 | wait 1s 717 | rate 12078kbit 718 | wait 1s 719 | rate 13857kbit 720 | wait 1s 721 | rate 8865kbit 722 | wait 1s 723 | rate 14406kbit 724 | wait 1s 725 | rate 12147kbit 726 | wait 1s 727 | rate 15732kbit 728 | wait 1s 729 | rate 21258kbit 730 | wait 1s 731 | rate 13473kbit 732 | wait 1s 733 | rate 12270kbit 734 | wait 1s 735 | rate 21432kbit 736 | wait 1s 737 | rate 16422kbit 738 | wait 1s 739 | rate 13122kbit 740 | wait 1s 741 | rate 10803kbit 742 | wait 1s 743 | rate 15117kbit 744 | wait 1s 745 | rate 11928kbit 746 | wait 1s 747 | rate 11316kbit 748 | wait 1s 749 | rate 11637kbit 750 | wait 1s 751 | rate 10914kbit 752 | wait 1s 753 | rate 12069kbit 754 | wait 1s 755 | rate 11028kbit 756 | wait 1s 757 | rate 10125kbit 758 | wait 1s 759 | rate 7959kbit 760 | wait 1s 761 | rate 5268kbit 762 | wait 1s 763 | rate 5748kbit 764 | wait 1s 765 | rate 2445kbit 766 | wait 1s 767 | rate 2445kbit 768 | wait 1s 769 | rate 3405kbit 770 | wait 1s 771 | rate 900kbit 772 | wait 1s 773 | rate 900kbit 774 | wait 1s 775 | rate 9255kbit 776 | wait 1s 777 | rate 5451kbit 778 | wait 1s 779 | rate 8172kbit 780 | wait 1s 781 | rate 5961kbit 782 | wait 1s 783 | rate 2628kbit 784 | wait 1s 785 | rate 7134kbit 786 | wait 1s 787 | rate 5964kbit 788 | wait 1s 789 | rate 7149kbit 790 | wait 1s 791 | rate 7038kbit 792 | wait 1s 793 | rate 6069kbit 794 | wait 1s 795 | rate 10515kbit 796 | wait 1s 797 | rate 10734kbit 798 | wait 1s 799 | rate 5859kbit 800 | wait 1s 801 | rate 5724kbit 802 | wait 1s 803 | rate 8082kbit 804 | wait 1s 805 | rate 7194kbit 806 | wait 1s 807 | rate 7119kbit 808 | wait 1s 809 | rate 7740kbit 810 | wait 1s 811 | rate 8472kbit 812 | wait 1s 813 | rate 6222kbit 814 | wait 1s 815 | rate 7839kbit 816 | wait 1s 817 | rate 6762kbit 818 | wait 1s 819 | rate 7587kbit 820 | wait 1s 821 | rate 4530kbit 822 | wait 1s 823 | rate 2919kbit 824 | wait 1s 825 | rate 1560kbit 826 | wait 1s 827 | rate 2328kbit 828 | wait 1s 829 | rate 903kbit 830 | wait 1s 831 | rate 900kbit 832 | wait 1s 833 | rate 1524kbit 834 | wait 1s 835 | rate 1728kbit 836 | wait 1s 837 | rate 3825kbit 838 | wait 1s 839 | rate 5628kbit 840 | wait 1s 841 | rate 6414kbit 842 | wait 1s 843 | rate 4299kbit 844 | wait 1s 845 | rate 3672kbit 846 | wait 1s 847 | rate 2880kbit 848 | wait 1s 849 | rate 7266kbit 850 | wait 1s 851 | rate 5481kbit 852 | wait 1s 853 | rate 7299kbit 854 | wait 1s 855 | rate 8835kbit 856 | wait 1s 857 | rate 10080kbit 858 | wait 1s 859 | rate 8547kbit 860 | wait 1s 861 | rate 6168kbit 862 | wait 1s 863 | rate 7833kbit 864 | wait 1s 865 | rate 6822kbit 866 | wait 1s 867 | rate 6036kbit 868 | wait 1s 869 | rate 5994kbit 870 | wait 1s 871 | rate 7941kbit 872 | wait 1s 873 | rate 6516kbit 874 | wait 1s 875 | rate 7725kbit 876 | wait 1s 877 | rate 7194kbit 878 | wait 1s 879 | rate 10326kbit 880 | wait 1s 881 | rate 7899kbit 882 | wait 1s 883 | rate 3207kbit 884 | wait 1s 885 | rate 3636kbit 886 | wait 1s 887 | rate 4416kbit 888 | wait 1s 889 | rate 900kbit 890 | wait 1s 891 | rate 6510kbit 892 | wait 1s 893 | rate 900kbit 894 | wait 1s 895 | rate 4773kbit 896 | wait 1s 897 | rate 6207kbit 898 | wait 1s 899 | rate 5199kbit 900 | wait 1s 901 | rate 6762kbit 902 | wait 1s 903 | rate 6501kbit 904 | wait 1s 905 | rate 6792kbit 906 | wait 1s 907 | rate 4029kbit 908 | wait 1s 909 | rate 10206kbit 910 | wait 1s 911 | rate 7095kbit 912 | wait 1s 913 | rate 3375kbit 914 | wait 1s 915 | rate 21024kbit 916 | wait 1s 917 | rate 8052kbit 918 | wait 1s 919 | rate 11412kbit 920 | wait 1s 921 | rate 8010kbit 922 | wait 1s 923 | rate 12462kbit 924 | wait 1s 925 | rate 9933kbit 926 | wait 1s 927 | rate 12711kbit 928 | wait 1s 929 | rate 12165kbit 930 | wait 1s 931 | rate 12546kbit 932 | wait 1s 933 | rate 11697kbit 934 | wait 1s 935 | rate 12288kbit 936 | wait 1s 937 | rate 10821kbit 938 | wait 1s 939 | rate 14061kbit 940 | wait 1s 941 | rate 12495kbit 942 | wait 1s 943 | rate 10890kbit 944 | wait 1s 945 | rate 14877kbit 946 | wait 1s 947 | rate 14040kbit 948 | wait 1s 949 | rate 9711kbit 950 | wait 1s 951 | rate 15840kbit 952 | wait 1s 953 | rate 11292kbit 954 | wait 1s 955 | rate 11328kbit 956 | wait 1s 957 | rate 6840kbit 958 | wait 1s 959 | rate 13488kbit 960 | wait 1s 961 | rate 11373kbit 962 | wait 1s 963 | rate 11982kbit 964 | wait 1s 965 | rate 17076kbit 966 | wait 1s 967 | rate 16266kbit 968 | wait 1s 969 | rate 16887kbit 970 | wait 1s 971 | rate 10734kbit 972 | wait 1s 973 | rate 12420kbit 974 | wait 1s 975 | rate 8427kbit 976 | wait 1s 977 | rate 15477kbit 978 | wait 1s 979 | rate 11970kbit 980 | wait 1s 981 | rate 14712kbit 982 | wait 1s 983 | rate 11646kbit 984 | wait 1s 985 | rate 9570kbit 986 | wait 1s 987 | rate 20238kbit 988 | wait 1s 989 | rate 20355kbit 990 | wait 1s 991 | rate 19815kbit 992 | wait 1s 993 | rate 16944kbit 994 | wait 1s 995 | rate 17805kbit 996 | wait 1s 997 | rate 10908kbit 998 | wait 1s 999 | rate 18960kbit 1000 | wait 1s 1001 | rate 15111kbit 1002 | wait 1s 1003 | rate 14604kbit 1004 | wait 1s 1005 | rate 16368kbit 1006 | wait 1s 1007 | rate 11160kbit 1008 | wait 1s 1009 | rate 13533kbit 1010 | wait 1s 1011 | rate 16536kbit 1012 | wait 1s 1013 | rate 2079kbit 1014 | wait 1s 1015 | rate 3624kbit 1016 | wait 1s 1017 | rate 4488kbit 1018 | wait 1s 1019 | rate 2673kbit 1020 | wait 1s 1021 | rate 7872kbit 1022 | wait 1s 1023 | rate 4110kbit 1024 | wait 1s 1025 | rate 4611kbit 1026 | wait 1s 1027 | rate 4698kbit 1028 | wait 1s 1029 | rate 12138kbit 1030 | wait 1s 1031 | rate 7770kbit 1032 | wait 1s 1033 | rate 8439kbit 1034 | wait 1s 1035 | rate 10617kbit 1036 | wait 1s 1037 | rate 6099kbit 1038 | wait 1s 1039 | rate 3042kbit 1040 | wait 1s 1041 | rate 5625kbit 1042 | wait 1s 1043 | rate 9489kbit 1044 | wait 1s 1045 | rate 4086kbit 1046 | wait 1s 1047 | rate 5361kbit 1048 | wait 1s 1049 | rate 7698kbit 1050 | wait 1s 1051 | rate 10176kbit 1052 | wait 1s 1053 | rate 8274kbit 1054 | wait 1s 1055 | rate 15564kbit 1056 | wait 1s 1057 | rate 15318kbit 1058 | wait 1s 1059 | rate 19725kbit 1060 | wait 1s 1061 | rate 19671kbit 1062 | wait 1s 1063 | rate 14052kbit 1064 | wait 1s 1065 | rate 5211kbit 1066 | wait 1s 1067 | rate 3444kbit 1068 | wait 1s 1069 | rate 15504kbit 1070 | wait 1s 1071 | rate 18672kbit 1072 | wait 1s 1073 | rate 16074kbit 1074 | wait 1s 1075 | rate 12600kbit 1076 | wait 1s 1077 | rate 4689kbit 1078 | wait 1s 1079 | rate 13284kbit 1080 | wait 1s 1081 | rate 7623kbit 1082 | wait 1s 1083 | rate 900kbit 1084 | wait 1s 1085 | rate 7596kbit 1086 | wait 1s 1087 | rate 5931kbit 1088 | wait 1s 1089 | rate 2661kbit 1090 | wait 1s 1091 | rate 3045kbit 1092 | wait 1s 1093 | rate 3843kbit 1094 | wait 1s 1095 | rate 2550kbit 1096 | wait 1s 1097 | rate 2973kbit 1098 | wait 1s 1099 | rate 1251kbit 1100 | wait 1s 1101 | rate 1509kbit 1102 | wait 1s 1103 | rate 1839kbit 1104 | wait 1s 1105 | rate 3264kbit 1106 | wait 1s 1107 | rate 9321kbit 1108 | wait 1s 1109 | rate 12399kbit 1110 | wait 1s 1111 | rate 11304kbit 1112 | wait 1s 1113 | rate 8910kbit 1114 | wait 1s 1115 | rate 10734kbit 1116 | wait 1s 1117 | rate 7440kbit 1118 | wait 1s 1119 | rate 6990kbit 1120 | wait 1s 1121 | rate 7503kbit 1122 | wait 1s 1123 | rate 7275kbit 1124 | wait 1s 1125 | rate 7857kbit 1126 | wait 1s 1127 | rate 14547kbit 1128 | wait 1s 1129 | rate 12744kbit 1130 | wait 1s 1131 | rate 13812kbit 1132 | wait 1s 1133 | rate 14646kbit 1134 | wait 1s 1135 | rate 14007kbit 1136 | wait 1s 1137 | rate 10518kbit 1138 | wait 1s 1139 | rate 4767kbit 1140 | wait 1s 1141 | rate 21258kbit 1142 | wait 1s 1143 | rate 22707kbit 1144 | wait 1s 1145 | rate 13926kbit 1146 | wait 1s 1147 | rate 11883kbit 1148 | wait 1s 1149 | rate 10515kbit 1150 | wait 1s 1151 | rate 6948kbit 1152 | wait 1s 1153 | rate 11871kbit 1154 | wait 1s 1155 | rate 15870kbit 1156 | wait 1s 1157 | rate 13662kbit 1158 | wait 1s 1159 | rate 14490kbit 1160 | wait 1s 1161 | rate 11826kbit 1162 | wait 1s 1163 | rate 12759kbit 1164 | wait 1s 1165 | rate 11751kbit 1166 | wait 1s 1167 | rate 10443kbit 1168 | wait 1s 1169 | rate 19371kbit 1170 | wait 1s 1171 | rate 16902kbit 1172 | wait 1s 1173 | rate 14580kbit 1174 | wait 1s 1175 | rate 14718kbit 1176 | wait 1s 1177 | rate 12219kbit 1178 | wait 1s 1179 | rate 15591kbit 1180 | wait 1s 1181 | rate 5517kbit 1182 | wait 1s 1183 | rate 20466kbit 1184 | wait 1s 1185 | rate 18204kbit 1186 | wait 1s 1187 | rate 9939kbit 1188 | wait 1s 1189 | rate 8829kbit 1190 | wait 1s 1191 | rate 10944kbit 1192 | wait 1s 1193 | rate 9255kbit 1194 | wait 1s 1195 | rate 7650kbit 1196 | wait 1s 1197 | rate 7788kbit 1198 | wait 1s 1199 | rate 7179kbit 1200 | wait 1s -------------------------------------------------------------------------------- /tc_profiles/tc_clear.sh: -------------------------------------------------------------------------------- 1 | TC='/sbin/tc' 2 | INTERFACE_1=enp0s31f6 #$1 3 | 4 | if [ -z $INTERFACE_1 ]; then 5 | echo "interface has to be specified" 6 | exit 1; 7 | fi 8 | 9 | killall tc_policy.sh 1>/dev/null 2>&1 10 | killall sleep 1>/dev/null 2>&1 11 | killall tc 1>/dev/null 2>&1 12 | 13 | $TC qdisc del dev $INTERFACE_1 root handle 1:0 1>/dev/null 2>&1 14 | $TC qdisc del dev $INTERFACE_1 root 1>/dev/null 2>&1 15 | $TC qdisc del dev lo root 1>/dev/null 2>&1 16 | -------------------------------------------------------------------------------- /tc_profiles/tc_limit.sh: -------------------------------------------------------------------------------- 1 | sudo tc qdisc add dev enp0s31f6 root tbf rate 1500kbit burst 16kbit latency 10ms -------------------------------------------------------------------------------- /tc_profiles/tc_netem.sh: -------------------------------------------------------------------------------- 1 | INTERFACE=enp0s31f6 2 | DELAY_MS=40 3 | RATE_MBIT=10 4 | BUF_PKTS=33 5 | BDP_BYTES=$(echo "($DELAY_MS/1000.0)*($RATE_MBIT*1000000.0/8.0)" | bc -q -l) 6 | BDP_PKTS=$(echo "$BDP_BYTES/1500" | bc -q) 7 | LIMIT_PKTS=$(echo "$BDP_PKTS+$BUF_PKTS" | bc -q) 8 | tc qdisc replace dev $INTERFACE root netem delay ${DELAY_MS}ms rate ${RATE_MBIT}Mbit limit ${LIMIT_PKTS} -------------------------------------------------------------------------------- /tc_profiles/tc_policy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TC="/sbin/tc" 3 | PORT_1=8443 4 | INTERFACE_1=enp0s31f6 #$1 5 | FILE_1=zafer_profile #$2 6 | 7 | #PORT=443 8 | #INTERFACE_2=$INTERFACE_1 9 | #FILE_2=$FILE_1 10 | 11 | 12 | if [ -z $INTERFACE_1 ]; then 13 | echo "interface has to be specified" 14 | exit 1; 15 | fi 16 | 17 | if [ -z $FILE_1 ]; then 18 | echo "policy file name has to be specified" 19 | exit 1; 20 | fi 21 | 22 | parsePolicyFile () { 23 | device=$1 24 | filename=$2 25 | classId=$3 26 | childClassId=$4 27 | if [ -z "$filename" ] || [ -z "$classId" ];then 28 | echo "filename and classid paramters required" 29 | else 30 | latestLoss="0%"; 31 | latestDelay="0ms"; 32 | while read -r line; do 33 | if [[ $line == \#* ]];then 34 | continue; 35 | else 36 | keys=($line) 37 | comm=${keys[0]} 38 | value=${keys[1]} 39 | case $comm in 40 | rate) 41 | echo "setting rate on $device $classId $value" 42 | burst=`awk "BEGIN {print $value/800*1000}"` 43 | $TC class change dev $device parent 1: classid 1:$classId htb rate $value burst ${burst} cburst ${burst} 44 | $TC class change dev $device parent 1: classid 1:$childClassId htb rate $value burst ${burst} cburst ${burst} 45 | #$TC qdisc del dev $device root 1>/dev/null 2>&1 46 | #$TC qdisc change dev $device root tbf rate $value burst $burst latency 1ms 47 | ;; 48 | loss) 49 | latestLoss=$value; 50 | echo "setting loss on $device $classId $value" 51 | $TC qdisc change dev $device parent 1:$classId netem loss $latestLoss delay $latestDelay 52 | ;; 53 | delay) 54 | latestDelay=$value; 55 | echo "setting delay on $device $classId $value" 56 | $TC qdisc change dev $device parent 1:$classId netem loss $latestLoss delay $latestDelay 57 | ;; 58 | wait) 59 | echo "waiting for $device $value seconds" 60 | sleep $value 61 | ;; 62 | esac 63 | fi 64 | done < "$filename" 65 | fi 66 | } 67 | 68 | policyLoop () { 69 | device=$1 70 | filename=$2 71 | classId=$3 72 | childClassId=$4 73 | while true; do 74 | parsePolicyFile $device $filename $classId $childClassId 75 | done 76 | } 77 | 78 | currentIfNo=1 79 | while [[ -v INTERFACE_$currentIfNo ]]; do 80 | interface=INTERFACE_$currentIfNo 81 | interface="${!interface}" 82 | $TC qdisc del dev $interface root 1>/dev/null 2>&1 83 | $TC qdisc add dev $interface root handle 1: htb default 10 84 | ((currentIfNo++)) 85 | done 86 | 87 | currentIfNo=1 88 | while [[ -v PORT_$currentIfNo ]]; do 89 | interface=INTERFACE_$currentIfNo 90 | interface="${!interface}" 91 | port=PORT_$currentIfNo 92 | port="${!port}" 93 | file=FILE_$currentIfNo 94 | file="${!file}" 95 | 96 | childIfNo=${currentIfNo}0 97 | $TC class add dev $interface parent 1: classid 1:$currentIfNo htb rate 1024Mbps 98 | $TC class add dev $interface parent 1:$currentIfNo classid 1:$childIfNo htb rate 1024Mbps 99 | $TC qdisc add dev $interface parent 1:$childIfNo handle 10: sfq perturb 10 100 | $TC filter add dev $interface parent 1:0 protocol ip prio 1 u32 match ip sport $port 0xffff flowid 1:$childIfNo 101 | policyLoop $interface $file $currentIfNo $childIfNo & 102 | ((currentIfNo++)) 103 | done 104 | 105 | wait 106 | 107 | #$TC qdisc del dev $device root 1>/dev/null 2>&1 108 | #$TC qdisc add dev $device root tbf rate $value burst $burst latency 1ms 109 | -------------------------------------------------------------------------------- /tc_profiles/tc_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | INTERFACE_1="enp0s31f6" 3 | PROFILE="lte_profile" 4 | 5 | sudo bash "tc_clear.sh" $INTERFACE_1 6 | sudo bash "tc_policy.sh" $INTERFACE_1 $PROFILE -------------------------------------------------------------------------------- /tc_profiles/twitch_profile: -------------------------------------------------------------------------------- 1 | rate 440kbit 2 | wait 10s 3 | rate 370kbit 4 | wait 5s 5 | rate 370kbit 6 | wait 5s 7 | rate 2598kbit 8 | wait 5s 9 | rate 2022kbit 10 | wait 5s 11 | rate 2022kbit 12 | wait 5s 13 | rate 2367kbit 14 | wait 5s 15 | rate 2367kbit 16 | wait 5s 17 | rate 1831kbit 18 | wait 5s 19 | rate 1831kbit 20 | wait 5s 21 | rate 1541kbit 22 | wait 5s 23 | rate 1541kbit 24 | wait 5s 25 | rate 2158kbit 26 | wait 5s 27 | rate 2158kbit 28 | wait 5s 29 | rate 295kbit 30 | wait 5s 31 | rate 295kbit 32 | wait 5s 33 | rate 2544kbit 34 | wait 5s 35 | rate 1780kbit 36 | wait 5s 37 | rate 1780kbit 38 | wait 5s 39 | rate 1330kbit 40 | wait 5s 41 | rate 1330kbit 42 | wait 5s 43 | rate 300kbit 44 | wait 5s 45 | rate 300kbit 46 | wait 5s 47 | rate 346kbit 48 | wait 5s 49 | rate 346kbit 50 | wait 5s 51 | rate 346kbit 52 | wait 5s 53 | rate 346kbit 54 | wait 5s 55 | rate 457kbit 56 | wait 5s 57 | rate 457kbit 58 | wait 5s 59 | rate 306kbit 60 | wait 5s 61 | rate 306kbit 62 | wait 5s 63 | rate 1806kbit 64 | wait 5s 65 | rate 1806kbit 66 | wait 5s 67 | rate 2379kbit 68 | wait 5s 69 | rate 2379kbit 70 | wait 5s 71 | rate 1066kbit 72 | wait 5s 73 | rate 2260kbit 74 | wait 5s 75 | rate 2260kbit 76 | wait 5s 77 | -------------------------------------------------------------------------------- /tc_profiles/twitch_profile_x0.25: -------------------------------------------------------------------------------- 1 | rate 505kbit 2 | wait 5s 3 | rate 110kbit 4 | wait 5s 5 | rate 92kbit 6 | wait 5s 7 | rate 92kbit 8 | wait 5s 9 | rate 649kbit 10 | wait 5s 11 | rate 505kbit 12 | wait 5s 13 | rate 505kbit 14 | wait 5s 15 | rate 591kbit 16 | wait 5s 17 | rate 591kbit 18 | wait 5s 19 | rate 457kbit 20 | wait 5s 21 | rate 457kbit 22 | wait 5s 23 | rate 385kbit 24 | wait 5s 25 | rate 385kbit 26 | wait 5s 27 | rate 539kbit 28 | wait 5s 29 | rate 539kbit 30 | wait 5s 31 | rate 73kbit 32 | wait 5s 33 | rate 73kbit 34 | wait 5s 35 | rate 636kbit 36 | wait 5s 37 | rate 445kbit 38 | wait 5s 39 | rate 445kbit 40 | wait 5s 41 | rate 332kbit 42 | wait 5s 43 | rate 332kbit 44 | wait 5s 45 | rate 75kbit 46 | wait 5s 47 | rate 75kbit 48 | wait 5s 49 | rate 86kbit 50 | wait 5s 51 | rate 86kbit 52 | wait 5s 53 | rate 86kbit 54 | wait 5s 55 | rate 86kbit 56 | wait 5s 57 | rate 114kbit 58 | wait 5s 59 | rate 114kbit 60 | wait 5s 61 | rate 76kbit 62 | wait 5s 63 | rate 76kbit 64 | wait 5s 65 | rate 451kbit 66 | wait 5s 67 | rate 451kbit 68 | wait 5s 69 | rate 594kbit 70 | wait 5s 71 | rate 594kbit 72 | wait 5s 73 | rate 266kbit 74 | wait 5s 75 | rate 565kbit 76 | wait 5s 77 | rate 565kbit 78 | wait 5s 79 | -------------------------------------------------------------------------------- /tc_profiles/twitch_profile_x3: -------------------------------------------------------------------------------- 1 | rate 1320kbit 2 | wait 5s 3 | rate 1110kbit 4 | wait 5s 5 | rate 1110kbit 6 | wait 5s 7 | rate 7794kbit 8 | wait 5s 9 | rate 6066kbit 10 | wait 5s 11 | rate 6066kbit 12 | wait 5s 13 | rate 7101kbit 14 | wait 5s 15 | rate 7101kbit 16 | wait 5s 17 | rate 5493kbit 18 | wait 5s 19 | rate 5493kbit 20 | wait 5s 21 | rate 4623kbit 22 | wait 5s 23 | rate 4623kbit 24 | wait 5s 25 | rate 6474kbit 26 | wait 5s 27 | rate 6474kbit 28 | wait 5s 29 | rate 885kbit 30 | wait 5s 31 | rate 885kbit 32 | wait 5s 33 | rate 7632kbit 34 | wait 5s 35 | rate 5340kbit 36 | wait 5s 37 | rate 5340kbit 38 | wait 5s 39 | rate 3990kbit 40 | wait 5s 41 | rate 3990kbit 42 | wait 5s 43 | rate 900kbit 44 | wait 5s 45 | rate 900kbit 46 | wait 5s 47 | rate 1038kbit 48 | wait 5s 49 | rate 1038kbit 50 | wait 5s 51 | rate 1038kbit 52 | wait 5s 53 | rate 1038kbit 54 | wait 5s 55 | rate 1371kbit 56 | wait 5s 57 | rate 1371kbit 58 | wait 5s 59 | rate 918kbit 60 | wait 5s 61 | rate 918kbit 62 | wait 5s 63 | rate 5418kbit 64 | wait 5s 65 | rate 5418kbit 66 | wait 5s 67 | rate 7137kbit 68 | wait 5s 69 | rate 7137kbit 70 | wait 5s 71 | rate 3198kbit 72 | wait 5s 73 | rate 6780kbit 74 | wait 5s 75 | rate 6780kbit 76 | wait 5s -------------------------------------------------------------------------------- /tc_profiles/twitch_profile_x4: -------------------------------------------------------------------------------- 1 | rate 1760kbit 2 | wait 5s 3 | rate 1480kbit 4 | wait 5s 5 | rate 1480kbit 6 | wait 5s 7 | rate 10392kbit 8 | wait 5s 9 | rate 8088kbit 10 | wait 5s 11 | rate 8088kbit 12 | wait 5s 13 | rate 9468kbit 14 | wait 5s 15 | rate 9468kbit 16 | wait 5s 17 | rate 7324kbit 18 | wait 5s 19 | rate 7324kbit 20 | wait 5s 21 | rate 6164kbit 22 | wait 5s 23 | rate 6164kbit 24 | wait 5s 25 | rate 8632kbit 26 | wait 5s 27 | rate 8632kbit 28 | wait 5s 29 | rate 1180kbit 30 | wait 5s 31 | rate 1180kbit 32 | wait 5s 33 | rate 10176kbit 34 | wait 5s 35 | rate 7120kbit 36 | wait 5s 37 | rate 7120kbit 38 | wait 5s 39 | rate 5320kbit 40 | wait 5s 41 | rate 5320kbit 42 | wait 5s 43 | rate 1200kbit 44 | wait 5s 45 | rate 1200kbit 46 | wait 5s 47 | rate 1384kbit 48 | wait 5s 49 | rate 1384kbit 50 | wait 5s 51 | rate 1384kbit 52 | wait 5s 53 | rate 1384kbit 54 | wait 5s 55 | rate 1828kbit 56 | wait 5s 57 | rate 1828kbit 58 | wait 5s 59 | rate 1224kbit 60 | wait 5s 61 | rate 1224kbit 62 | wait 5s 63 | rate 7224kbit 64 | wait 5s 65 | rate 7224kbit 66 | wait 5s 67 | rate 9516kbit 68 | wait 5s 69 | rate 9516kbit 70 | wait 5s 71 | rate 4264kbit 72 | wait 5s 73 | rate 9040kbit 74 | wait 5s 75 | rate 9040kbit 76 | wait 5s --------------------------------------------------------------------------------