From 3d977ca51991ba8d603c2167725ec9b277a61126 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Thu, 29 May 2025 04:57:28 +0530 Subject: [PATCH 1/2] Try to implement sabr handling. --- .github/workflows/test.yml | 23 ++ Cargo.lock | 26 ++ Cargo.toml | 3 + run_sabr_test.sh | 91 +++++ sabr_test/.gitignore | 34 ++ sabr_test/bun.lock | 148 +++++++ sabr_test/index.ts | 375 +++++++++++++++++ sabr_test/package.json | 27 ++ sabr_test/tsconfig.json | 28 ++ sabr_test/utils.ts | 46 +++ src/main.rs | 29 +- src/sabr_handler.rs | 466 +++++++++++++++++++++ src/sabr_parser.rs | 811 +++++++++++++++++++++++++++++++++++++ src/sabr_request.rs | 473 +++++++++++++++++++++ test/sabr_response.bin | Bin 0 -> 746341 bytes 15 files changed, 2574 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100755 run_sabr_test.sh create mode 100644 sabr_test/.gitignore create mode 100644 sabr_test/bun.lock create mode 100644 sabr_test/index.ts create mode 100644 sabr_test/package.json create mode 100644 sabr_test/tsconfig.json create mode 100644 sabr_test/utils.ts create mode 100644 src/sabr_handler.rs create mode 100644 src/sabr_parser.rs create mode 100644 src/sabr_request.rs create mode 100644 test/sabr_response.bin diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6f0cf8a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Run Tests + +on: + push: + paths-ignore: + - "**.md" + branches: + - main + pull_request: + paths-ignore: + - "**.md" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + - uses: rui314/setup-mold@v1 + - name: Set up NASM + uses: ilammy/setup-nasm@v1.5.2 + - name: Run tests + run: cargo test diff --git a/Cargo.lock b/Cargo.lock index aff4194..4c4b547 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1541,6 +1541,7 @@ name = "piped-proxy" version = "0.1.0" dependencies = [ "actix-web", + "base64", "blake3", "bytes", "futures-util", @@ -1550,11 +1551,13 @@ dependencies = [ "listenfd", "mimalloc", "once_cell", + "prost", "qstring", "ravif", "regex", "reqwest", "rgb", + "serde_json", "tokio", ] @@ -1616,6 +1619,29 @@ dependencies = [ "syn", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "qstring" version = "0.7.2" diff --git a/Cargo.toml b/Cargo.toml index 73a5eca..8b3f7de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ tokio = { version = "1.37.0", features = ["full"] } actix-web = "4.5.1" reqwest = { version = "0.12.9", features = ["stream", "brotli", "gzip", "socks"], default-features = false } qstring = "0.7.2" +serde_json = "1.0" # Alternate Allocator mimalloc = { version = "0.1.41", optional = true } @@ -28,6 +29,8 @@ bytes = "1.9.0" futures-util = "0.3.30" listenfd = "1.0.1" http = "1.2.0" +prost = "0.13.5" +base64 = "0.22.1" [features] default = ["webp", "mimalloc", "reqwest-rustls", "qhash"] diff --git a/run_sabr_test.sh b/run_sabr_test.sh new file mode 100755 index 0000000..173ee6a --- /dev/null +++ b/run_sabr_test.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}๐Ÿš€ Starting SABR Test Environment${NC}" + +# Function to cleanup background processes +cleanup() { + echo -e "\n${YELLOW}๐Ÿงน Cleaning up...${NC}" + if [ ! -z "$SERVER_PID" ]; then + kill $SERVER_PID 2>/dev/null + echo -e "${YELLOW}Stopped server (PID: $SERVER_PID)${NC}" + fi + + # Show server logs if they exist + if [ -f "server.log" ]; then + echo -e "${BLUE}๐Ÿ“‹ Server logs:${NC}" + tail -20 server.log + fi + + exit 0 +} + +# Set trap to cleanup on script exit +trap cleanup EXIT INT TERM + +# Build and start the Rust server in the background +echo -e "${BLUE}๐Ÿ”จ Building Rust server (debug)...${NC}" +cargo build +if [ $? -ne 0 ]; then + echo -e "${RED}โŒ Failed to build server${NC}" + exit 1 +fi + +echo -e "${BLUE}๐ŸŒ Starting server on port 8080...${NC}" +RUST_LOG=debug ./target/debug/piped-proxy >server.log 2>&1 & +SERVER_PID=$! + +# Wait a moment for server to start +sleep 3 + +# Check if server is running +if ! kill -0 $SERVER_PID 2>/dev/null; then + echo -e "${RED}โŒ Server failed to start${NC}" + if [ -f "server.log" ]; then + echo -e "${RED}Server logs:${NC}" + cat server.log + fi + exit 1 +fi + +echo -e "${GREEN}โœ… Server started (PID: $SERVER_PID)${NC}" + +# Test server connectivity +echo -e "${BLUE}๐Ÿ” Testing server connectivity...${NC}" +if curl -s http://127.0.0.1:8080 >/dev/null; then + echo -e "${GREEN}โœ… Server is responding${NC}" +else + echo -e "${RED}โŒ Server is not responding${NC}" + exit 1 +fi + +# Change to sabr_test directory and run the test +echo -e "${BLUE}๐Ÿงช Running SABR test...${NC}" +cd sabr_test + +# Install dependencies if needed +if [ ! -d "node_modules" ]; then + echo -e "${BLUE}๐Ÿ“ฆ Installing dependencies...${NC}" + bun install +fi + +# Run the test with provided arguments or defaults +echo -e "${GREEN}๐ŸŽฏ Starting SABR test...${NC}" +timeout 30 bun run index.ts --verbose --duration 5 "$@" +TEST_EXIT_CODE=$? + +if [ $TEST_EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}โœ… SABR test completed successfully!${NC}" +elif [ $TEST_EXIT_CODE -eq 124 ]; then + echo -e "${YELLOW}โฐ Test timed out (this might be expected)${NC}" +else + echo -e "${RED}โŒ SABR test failed with exit code: $TEST_EXIT_CODE${NC}" +fi + +echo -e "${GREEN}๐Ÿ Test completed${NC}" diff --git a/sabr_test/.gitignore b/sabr_test/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/sabr_test/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/sabr_test/bun.lock b/sabr_test/bun.lock new file mode 100644 index 0000000..64a2f22 --- /dev/null +++ b/sabr_test/bun.lock @@ -0,0 +1,148 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "sabr_test", + "dependencies": { + "bgutils-js": "^3.2.0", + "cli-progress": "^3.12.0", + "commander": "^14.0.0", + "jsdom": "^26.1.0", + "youtubei.js": "^13.4.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/cli-progress": "^3.11.6", + "@types/jsdom": "^21.1.7", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.5.1", "", {}, "sha512-lut4UTvKL8tqtend0UDu7R79/n9jA7Jtxf77RNPbxtmWqfWI4qQ9bTjf7KCS4vfqLmpQbuHr1ciqJumAgJODdw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.10", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + + "@types/cli-progress": ["@types/cli-progress@3.11.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA=="], + + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], + + "@types/node": ["@types/node@22.15.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "bgutils-js": ["bgutils-js@3.2.0", "", {}, "sha512-CacO15JvxbclbLeCAAm9DETGlLuisRGWpPigoRvNsccSCPEC4pwYwA2g2x/pv7Om/sk79d4ib35V5HHmxPBpDg=="], + + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "cssstyle": ["cssstyle@4.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" } }, "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "jintr": ["jintr@3.3.1", "", { "dependencies": { "acorn": "^8.8.0" } }, "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA=="], + + "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "youtubei.js": ["youtubei.js@13.4.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "jintr": "^3.3.1", "tslib": "^2.5.0", "undici": "^5.19.1" } }, "sha512-+fmIZU/dWAjsROONrASy1REwVpy6umAPVuoNLr/4iNmZXl84LyBef0n3hrd1Vn9035EuINToGyQcBmifwUEemA=="], + } +} diff --git a/sabr_test/index.ts b/sabr_test/index.ts new file mode 100644 index 0000000..43c40b4 --- /dev/null +++ b/sabr_test/index.ts @@ -0,0 +1,375 @@ +#!/usr/bin/env bun + +import { program } from "commander"; +import cliProgress from "cli-progress"; +import { Innertube, UniversalCache } from "youtubei.js"; +import { generateWebPoToken } from "./utils.js"; + +interface SabrTestOptions { + proxy: string; + videoId: string; + duration?: number; + audioItag?: number; + videoItag?: number; + verbose: boolean; +} + +interface FormatId { + itag: number; + lastModified: number; + xtags?: string; +} + +interface BufferedRange { + formatId: FormatId; + startTimeMs: number; + durationMs: number; + startSegmentIndex: number; + endSegmentIndex: number; +} + +interface SabrRequestData { + playerTimeMs: number; + bandwidthEstimate: number; + clientViewportWidth: number; + clientViewportHeight: number; + playbackRate: number; + hasAudio: boolean; + selectedAudioFormatIds: FormatId[]; + selectedVideoFormatIds: FormatId[]; + bufferedRanges: BufferedRange[]; + videoPlaybackUstreamerConfig?: string; + poToken?: string; + playbackCookie?: string; +} + +class SabrTester { + private proxyUrl: string; + private innertube: Innertube | null = null; + private verbose: boolean; + + constructor(proxyUrl: string, verbose: boolean = false) { + this.proxyUrl = proxyUrl; + this.verbose = verbose; + } + + private log(message: string) { + if (this.verbose) { + console.log(`[SABR Test] ${message}`); + } + } + + private error(message: string) { + console.error(`[ERROR] ${message}`); + } + + async initialize() { + this.log("Initializing YouTube client..."); + try { + this.innertube = await Innertube.create({ + cache: new UniversalCache(true), + enable_session_cache: false, + }); + this.log("YouTube client initialized successfully"); + } catch (error) { + throw new Error(`Failed to initialize YouTube client: ${error}`); + } + } + + async getVideoInfo(videoId: string) { + if (!this.innertube) { + throw new Error("YouTube client not initialized"); + } + + this.log(`Fetching video info for: ${videoId}`); + try { + const info = await this.innertube.getBasicInfo(videoId); + + console.log(` +Video Information: + Title: ${info.basic_info.title} + Duration: ${info.basic_info.duration}s + Views: ${info.basic_info.view_count} + Author: ${info.basic_info.author} + Video ID: ${info.basic_info.id} + `); + + return info; + } catch (error) { + throw new Error(`Failed to get video info: ${error}`); + } + } + + async generatePoToken(): Promise { + if (!this.innertube) { + throw new Error("YouTube client not initialized"); + } + + try { + this.log("Generating PoToken..."); + const visitorData = this.innertube.session.context.client.visitorData; + if (!visitorData) { + this.log("No visitor data available, skipping PoToken generation"); + return undefined; + } + + const webPoTokenResult = await generateWebPoToken(visitorData); + this.log(`PoToken generated successfully: ${webPoTokenResult.poToken.substring(0, 20)}...`); + return webPoTokenResult.poToken; + } catch (error) { + this.log(`PoToken generation failed: ${error}`); + return undefined; + } + } + + async testSabrRequest(videoId: string, options: Partial = {}) { + if (!this.innertube) { + throw new Error("YouTube client not initialized"); + } + + const info = await this.getVideoInfo(videoId); + + // Get streaming data + const streamingData = info.streaming_data; + if (!streamingData) { + throw new Error("No streaming data available"); + } + + // Get server ABR streaming URL + const serverAbrStreamingUrl = this.innertube.session.player?.decipher(streamingData.server_abr_streaming_url); + + if (!serverAbrStreamingUrl) { + throw new Error("No server ABR streaming URL found"); + } + + this.log(`Server ABR URL: ${serverAbrStreamingUrl}`); + + // Get video playback ustreamer config + const videoPlaybackUstreamerConfig = + info.page?.[0]?.player_config?.media_common_config?.media_ustreamer_request_config + ?.video_playback_ustreamer_config; + + if (!videoPlaybackUstreamerConfig) { + throw new Error("No video playback ustreamer config found"); + } + + // Generate PoToken + const poToken = await this.generatePoToken(); + + // Find suitable formats + const audioFormat = streamingData.adaptive_formats.find( + (f: any) => + f.mime_type?.includes("audio") && (options.audioItag ? f.itag === options.audioItag : f.itag === 251), + ); + + const videoFormat = streamingData.adaptive_formats.find( + (f: any) => + f.mime_type?.includes("video") && (options.videoItag ? f.itag === options.videoItag : f.itag === 136), + ); + + let finalAudioFormat, finalVideoFormat; + + if (!audioFormat || !videoFormat) { + // If specific formats not found, try to find any audio/video formats + const fallbackAudio = streamingData.adaptive_formats.find((f: any) => f.mime_type?.includes("audio")); + const fallbackVideo = streamingData.adaptive_formats.find((f: any) => f.mime_type?.includes("video")); + + if (!fallbackAudio || !fallbackVideo) { + throw new Error("Could not find suitable audio/video formats"); + } + + console.log(` +Selected Formats (fallback): + Audio: itag=${fallbackAudio.itag}, mime=${fallbackAudio.mime_type} + Video: itag=${fallbackVideo.itag}, mime=${fallbackVideo.mime_type} + `); + + finalAudioFormat = fallbackAudio; + finalVideoFormat = fallbackVideo; + } else { + console.log(` +Selected Formats: + Audio: itag=${audioFormat.itag}, mime=${audioFormat.mime_type} + Video: itag=${videoFormat.itag}, mime=${videoFormat.mime_type} + `); + + finalAudioFormat = audioFormat; + finalVideoFormat = videoFormat; + } + + // Prepare SABR request data with correct structure matching Rust handler + const sabrData: SabrRequestData = { + playerTimeMs: 0, + bandwidthEstimate: 1000000, // 1 Mbps + clientViewportWidth: 1920, + clientViewportHeight: 1080, + playbackRate: 1.0, + hasAudio: true, + selectedAudioFormatIds: [ + { + itag: finalAudioFormat.itag!, + lastModified: parseInt(finalAudioFormat.last_modified_ms || "0"), + xtags: finalAudioFormat.xtags, + }, + ], + selectedVideoFormatIds: [ + { + itag: finalVideoFormat.itag!, + lastModified: parseInt(finalVideoFormat.last_modified_ms || "0"), + xtags: finalVideoFormat.xtags, + }, + ], + bufferedRanges: [], // Empty for initial request + videoPlaybackUstreamerConfig: Buffer.from(videoPlaybackUstreamerConfig).toString("base64"), + poToken: poToken ? Buffer.from(poToken, "utf-8").toString("base64") : undefined, + }; + + // Test SABR request through proxy + await this.testProxySabrRequest(serverAbrStreamingUrl, sabrData, options.duration || 10); + } + + async testProxySabrRequest(serverAbrUrl: string, sabrData: SabrRequestData, durationSeconds: number) { + const url = new URL(serverAbrUrl); + + // Add sabr parameter to indicate this is a SABR request + url.searchParams.set("sabr", "1"); + + // Replace the host with our proxy + const proxyUrl = new URL(this.proxyUrl); + url.searchParams.set("host", url.host); + + const finalUrl = `${proxyUrl.origin}/videoplayback?${url.searchParams.toString()}`; + + this.log(`Making SABR request to proxy: ${finalUrl}`); + + const progressBar = new cliProgress.SingleBar({ + format: "SABR Test [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}s", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true, + }); + + progressBar.start(durationSeconds, 0); + + try { + const response = await fetch(finalUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + }, + body: JSON.stringify(sabrData), + }); + + if (!response.ok) { + throw new Error(`SABR request failed: ${response.status} ${response.statusText}`); + } + + this.log(`SABR request successful: ${response.status}`); + this.log(`Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2)}`); + + // Check for playback cookie in response headers + const playbackCookie = response.headers.get("X-Playback-Cookie"); + if (playbackCookie) { + this.log(`Received playback cookie: ${playbackCookie.substring(0, 50)}...`); + } + + // Read response body (this would be the UMP stream) + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No response body reader available"); + } + + let totalBytes = 0; + let chunks = 0; + const startTime = Date.now(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + totalBytes += value.length; + chunks++; + + const elapsedSeconds = (Date.now() - startTime) / 1000; + progressBar.update(Math.min(elapsedSeconds, durationSeconds)); + + if (elapsedSeconds >= durationSeconds) { + this.log("Duration limit reached, stopping..."); + break; + } + + // Log first few chunks for debugging + if (chunks <= 3 && this.verbose) { + this.log(`Chunk ${chunks}: ${value.length} bytes`); + this.log( + `First 32 bytes: ${Array.from(value.slice(0, 32)) + .map((b) => (b as number).toString(16).padStart(2, "0")) + .join(" ")}`, + ); + } + } + + progressBar.stop(); + + console.log(` +SABR Test Results: + Total bytes received: ${totalBytes} + Total chunks: ${chunks} + Average chunk size: ${Math.round(totalBytes / chunks)} bytes + Duration: ${((Date.now() - startTime) / 1000).toFixed(2)}s + Throughput: ${(totalBytes / 1024 / 1024 / ((Date.now() - startTime) / 1000)).toFixed(2)} MB/s + `); + + return true; + } catch (error) { + progressBar.stop(); + throw error; + } + } +} + +// CLI setup +program.name("sabr-test").description("Test SABR functionality through piped-proxy").version("1.0.0"); + +program + .option("-p, --proxy ", "Proxy server URL", "http://127.0.0.1:8080") + .option("-v, --video-id ", "YouTube video ID to test with", "eg2g6FPsdLI") + .option("-d, --duration ", "Test duration in seconds", "10") + .option("-a, --audio-itag ", "Audio format itag to use") + .option("--video-itag ", "Video format itag to use") + .option("--verbose", "Enable verbose logging", false); + +program.action(async (options: any) => { + const tester = new SabrTester(options.proxy, options.verbose); + + try { + console.log(` +๐Ÿš€ SABR Proxy Tester +Proxy: ${options.proxy} +Video ID: ${options.videoId} +Duration: ${options.duration}s + `); + + await tester.initialize(); + + // Test SABR functionality + console.log("๐Ÿ”„ Testing SABR functionality..."); + await tester.testSabrRequest(options.videoId, { + duration: parseInt(options.duration), + audioItag: options.audioItag ? parseInt(options.audioItag) : undefined, + videoItag: options.videoItag ? parseInt(options.videoItag) : undefined, + verbose: options.verbose, + proxy: options.proxy, + videoId: options.videoId, + }); + + console.log("โœ… SABR test completed successfully!"); + } catch (error) { + console.error(`โŒ Test failed: ${error}`); + process.exit(1); + } +}); + +// Parse command line arguments +program.parse(); diff --git a/sabr_test/package.json b/sabr_test/package.json new file mode 100644 index 0000000..cff870c --- /dev/null +++ b/sabr_test/package.json @@ -0,0 +1,27 @@ +{ + "name": "sabr_test", + "module": "index.ts", + "type": "module", + "private": true, + "description": "SABR (Server-side Adaptive Bitrate) testing tool for piped-proxy", + "scripts": { + "start": "bun run index.ts", + "test": "bun run index.ts --verbose", + "help": "bun run index.ts --help" + }, + "dependencies": { + "youtubei.js": "^13.4.0", + "commander": "^14.0.0", + "cli-progress": "^3.12.0", + "bgutils-js": "^3.2.0", + "jsdom": "^26.1.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/cli-progress": "^3.11.6", + "@types/jsdom": "^21.1.7" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/sabr_test/tsconfig.json b/sabr_test/tsconfig.json new file mode 100644 index 0000000..9c62f74 --- /dev/null +++ b/sabr_test/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/sabr_test/utils.ts b/sabr_test/utils.ts new file mode 100644 index 0000000..52ee02a --- /dev/null +++ b/sabr_test/utils.ts @@ -0,0 +1,46 @@ +import { BG, type BgConfig } from "bgutils-js"; +import { JSDOM } from "jsdom"; + +export async function generateWebPoToken(visitorData: string) { + const requestKey = "O43z0dpjhgX20SCx4KAo"; + + if (!visitorData) throw new Error("Could not get visitor data"); + + const dom = new JSDOM(); + + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + }); + + const bgConfig: BgConfig = { + fetch: fetch as any, + globalObj: globalThis, + identifier: visitorData, + requestKey, + }; + + const bgChallenge = await BG.Challenge.create(bgConfig); + + if (!bgChallenge) throw new Error("Could not get challenge"); + + const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue; + + if (interpreterJavascript) { + new Function(interpreterJavascript)(); + } else throw new Error("Could not load VM"); + + const poTokenResult = await BG.PoToken.generate({ + program: bgChallenge.program, + globalName: bgChallenge.globalName, + bgConfig, + }); + + const placeholderPoToken = BG.PoToken.generateColdStartToken(visitorData); + + return { + visitorData, + placeholderPoToken, + poToken: poTokenResult.poToken, + }; +} diff --git a/src/main.rs b/src/main.rs index daed748..f34dee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +mod sabr_handler; +mod sabr_parser; +mod sabr_request; mod ump_stream; mod utils; @@ -176,12 +179,22 @@ fn is_header_allowed(header: &str) -> bool { ) } -async fn index(req: HttpRequest) -> Result> { +async fn index(req: HttpRequest, body: Option) -> Result> { if req.method() == actix_web::http::Method::OPTIONS { let mut response = HttpResponse::Ok(); add_headers(&mut response); return Ok(response.finish()); - } else if req.method() != actix_web::http::Method::GET + } + + // parse query string + let mut query = QString::from(req.query_string()); + + // Check if this is a SABR request + let is_sabr = req.path().eq("/videoplayback") && query.get("sabr").is_some(); + + // Allow POST for SABR requests, otherwise only GET and HEAD + if !is_sabr + && req.method() != actix_web::http::Method::GET && req.method() != actix_web::http::Method::HEAD { let mut response = HttpResponse::MethodNotAllowed(); @@ -189,9 +202,6 @@ async fn index(req: HttpRequest) -> Result> { return Ok(response.finish()); } - // parse query string - let mut query = QString::from(req.query_string()); - #[cfg(feature = "qhash")] { use std::collections::BTreeSet; @@ -281,13 +291,20 @@ async fn index(req: HttpRequest) -> Result> { return Err("Domain not allowed".into()); } + // Handle SABR requests for UMP streams + if is_sabr && req.method() == actix_web::http::Method::POST { + let request_body = body.map(|b| String::from_utf8_lossy(&b).to_string()); + return sabr_handler::handle_sabr_request(req, query, host, &CLIENT, request_body).await; + } + let video_playback = req.path().eq("/videoplayback"); if video_playback { if let Some(expiry) = query.get("expire") { let expiry = expiry.parse::()?; let now = SystemTime::now(); - let now = now.duration_since(UNIX_EPOCH) + let now = now + .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs() as i64; if now > expiry { diff --git a/src/sabr_handler.rs b/src/sabr_handler.rs new file mode 100644 index 0000000..1336d29 --- /dev/null +++ b/src/sabr_handler.rs @@ -0,0 +1,466 @@ +use crate::sabr_parser::SabrParser; +use crate::sabr_request::{ + create_buffered_range, create_format_id, BufferedRange, ClientInfo, FormatId, + SabrRequestBuilder, +}; +use actix_web::{HttpRequest, HttpResponse}; +use base64::{engine::general_purpose, Engine as _}; +use prost::Message; +use qstring::QString; +use reqwest::{Body, Client, Method, Request, Url}; +use serde_json::Value; +use std::error::Error; +use std::str::FromStr; + +#[derive(Debug)] +pub struct SabrRequestData { + pub player_time_ms: i64, + pub bandwidth_estimate: i64, + pub client_viewport_width: i32, + pub client_viewport_height: i32, + pub playback_rate: f32, + pub has_audio: bool, + pub selected_audio_format_ids: Vec, + pub selected_video_format_ids: Vec, + pub buffered_ranges: Vec, + pub video_playback_ustreamer_config: Option>, + pub po_token: Option>, + pub playback_cookie: Option>, +} + +impl SabrRequestData { + pub fn from_json_body(body: &str) -> Result> { + let json: Value = serde_json::from_str(body)?; + + let player_time_ms = json + .get("playerTimeMs") + .and_then(|v| v.as_f64()) + .map(|v| v as i64) + .unwrap_or(0); + + let bandwidth_estimate = json + .get("bandwidthEstimate") + .and_then(|v| v.as_f64()) + .map(|v| v as i64) + .unwrap_or(1000000); // Default 1Mbps + + let client_viewport_width = json + .get("clientViewportWidth") + .and_then(|v| v.as_f64()) + .map(|v| v as i32) + .unwrap_or(1920); + + let client_viewport_height = json + .get("clientViewportHeight") + .and_then(|v| v.as_f64()) + .map(|v| v as i32) + .unwrap_or(1080); + + let playback_rate = json + .get("playbackRate") + .and_then(|v| v.as_f64()) + .map(|v| v as f32) + .unwrap_or(1.0); + + let has_audio = json + .get("hasAudio") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + // Parse audio format IDs + let selected_audio_format_ids = json + .get("selectedAudioFormatIds") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| parse_format_id_from_json(item)) + .collect() + }) + .unwrap_or_default(); + + // Parse video format IDs + let selected_video_format_ids = json + .get("selectedVideoFormatIds") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| parse_format_id_from_json(item)) + .collect() + }) + .unwrap_or_default(); + + // Parse buffered ranges + let buffered_ranges = json + .get("bufferedRanges") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| parse_buffered_range_from_json(item)) + .collect() + }) + .unwrap_or_default(); + + // Parse base64 encoded fields + let video_playback_ustreamer_config = json + .get("videoPlaybackUstreamerConfig") + .and_then(|v| v.as_str()) + .and_then(|s| general_purpose::STANDARD.decode(s).ok()); + + let po_token = json + .get("poToken") + .and_then(|v| v.as_str()) + .and_then(|s| general_purpose::STANDARD.decode(s).ok()); + + let playback_cookie = json + .get("playbackCookie") + .and_then(|v| v.as_str()) + .and_then(|s| general_purpose::STANDARD.decode(s).ok()); + + Ok(SabrRequestData { + player_time_ms, + bandwidth_estimate, + client_viewport_width, + client_viewport_height, + playback_rate, + has_audio, + selected_audio_format_ids, + selected_video_format_ids, + buffered_ranges, + video_playback_ustreamer_config, + po_token, + playback_cookie, + }) + } +} + +fn parse_format_id_from_json(json: &Value) -> Option { + let itag = json.get("itag")?.as_i64()? as i32; + let last_modified = json.get("lastModified")?.as_u64()?; + let xtags = json + .get("xtags") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + Some(create_format_id(itag, last_modified, xtags)) +} + +fn parse_buffered_range_from_json(json: &Value) -> Option { + let format_id = json.get("formatId").and_then(parse_format_id_from_json)?; + let start_time_ms = json.get("startTimeMs")?.as_i64()?; + let duration_ms = json.get("durationMs")?.as_i64()?; + let start_segment_index = json.get("startSegmentIndex")?.as_i64()? as i32; + let end_segment_index = json.get("endSegmentIndex")?.as_i64()? as i32; + + Some(create_buffered_range( + format_id, + start_time_ms, + duration_ms, + start_segment_index, + end_segment_index, + )) +} + +fn get_client_info_from_query(query: &QString) -> ClientInfo { + // Extract client info from query parameters + let client_name = query.get("c").and_then(|c| match c { + "WEB" => Some(1), + "ANDROID" => Some(3), + "IOS" => Some(5), + _ => Some(1), // Default to WEB + }); + + let client_version = query + .get("cver") + .map(|v| v.to_string()) + .unwrap_or_else(|| "2.2040620.05.00".to_string()); + + ClientInfo { + device_make: None, + device_model: None, + client_name, + client_version: Some(client_version), + os_name: Some("Windows".to_string()), + os_version: Some("10.0".to_string()), + accept_language: None, + accept_region: None, + screen_width_points: None, + screen_height_points: None, + screen_width_inches: None, + screen_height_inches: None, + screen_pixel_density: None, + client_form_factor: None, + gmscore_version_code: None, + window_width_points: None, + window_height_points: None, + android_sdk_version: None, + screen_density_float: None, + utc_offset_minutes: None, + time_zone: None, + chipset: None, + } +} + +pub async fn handle_sabr_request( + req: HttpRequest, + mut query: QString, + host: String, + client: &Client, + request_body: Option, +) -> Result> { + // Remove ump parameter before proxying by filtering it out + let filtered_pairs: Vec<_> = query + .into_pairs() + .into_iter() + .filter(|(key, _)| key != "sabr") + .collect(); + query = QString::new(filtered_pairs); + + // Parse request body if provided + let sabr_data = if let Some(body) = request_body { + Some(SabrRequestData::from_json_body(&body)?) + } else { + None + }; + + // Build SABR request + let mut sabr_builder = SabrRequestBuilder::new(); + + // Set client info from query parameters + let client_info = get_client_info_from_query(&query); + sabr_builder = sabr_builder.with_client_info(client_info); + + if let Some(ref data) = sabr_data { + sabr_builder = sabr_builder + .with_player_time_ms(data.player_time_ms) + .with_bandwidth_estimate(data.bandwidth_estimate) + .with_viewport_size(data.client_viewport_width, data.client_viewport_height) + .with_playback_rate(data.playback_rate) + .with_enabled_track_types(if data.has_audio { 1 } else { 2 }) + .with_audio_formats(data.selected_audio_format_ids.clone()) + .with_video_formats(data.selected_video_format_ids.clone()); + + // If no buffered ranges provided, create initial ones like in the working example + let buffered_ranges = if data.buffered_ranges.is_empty() { + let mut ranges = Vec::new(); + + // Create buffered range for audio format (like the working example) + if let Some(audio_format) = data.selected_audio_format_ids.first() { + ranges.push(create_buffered_range( + audio_format.clone(), + 0, // start_time_ms + 20000, // duration_ms (20 seconds like working example) + 1, // start_segment_index + 2, // end_segment_index + )); + } + + // Create buffered ranges for video format (like the working example) + if let Some(video_format) = data.selected_video_format_ids.first() { + ranges.push(create_buffered_range( + video_format.clone(), + 0, // start_time_ms + 15021, // duration_ms (like working example) + 1, // start_segment_index + 3, // end_segment_index + )); + + ranges.push(create_buffered_range( + video_format.clone(), + 10014, // start_time_ms (like working example) + 10014, // duration_ms (like working example) + 3, // start_segment_index + 4, // end_segment_index + )); + } + + ranges + } else { + data.buffered_ranges.clone() + }; + + sabr_builder = sabr_builder.with_buffered_ranges(buffered_ranges); + + if let Some(ref config) = data.video_playback_ustreamer_config { + sabr_builder = sabr_builder.with_video_playback_ustreamer_config(config.clone()); + } + + if let Some(ref token) = data.po_token { + sabr_builder = sabr_builder.with_po_token(token.clone()); + } + + if let Some(ref cookie) = data.playback_cookie { + sabr_builder = sabr_builder.with_playback_cookie(cookie.clone()); + } + } + + // Build the protobuf request + let sabr_request = sabr_builder.build(); + let mut encoded_request = Vec::new(); + sabr_request.encode(&mut encoded_request)?; + + // Debug output + eprintln!("SABR request structure:"); + eprintln!( + " client_abr_state: {:?}", + sabr_request.client_abr_state.is_some() + ); + eprintln!( + " selected_format_ids: {} items", + sabr_request.selected_format_ids.len() + ); + eprintln!( + " selected_audio_format_ids: {} items", + sabr_request.selected_audio_format_ids.len() + ); + eprintln!( + " selected_video_format_ids: {} items", + sabr_request.selected_video_format_ids.len() + ); + eprintln!( + " buffered_ranges: {} items", + sabr_request.buffered_ranges.len() + ); + eprintln!( + " video_playback_ustreamer_config: {} bytes", + sabr_request + .video_playback_ustreamer_config + .as_ref() + .map(|v| v.len()) + .unwrap_or(0) + ); + eprintln!( + " streamer_context: {:?}", + sabr_request.streamer_context.is_some() + ); + if let Some(ref ctx) = sabr_request.streamer_context { + eprintln!( + " po_token: {} bytes", + ctx.po_token.as_ref().map(|v| v.len()).unwrap_or(0) + ); + eprintln!( + " playback_cookie: {} bytes", + ctx.playback_cookie.as_ref().map(|v| v.len()).unwrap_or(0) + ); + eprintln!(" client_info: {:?}", ctx.client_info.is_some()); + } + eprintln!(" field1000: {} items", sabr_request.field1000.len()); + + // Print first 100 bytes of the protobuf for debugging + eprintln!( + "First 100 bytes of protobuf: {:?}", + &encoded_request[..std::cmp::min(100, encoded_request.len())] + ); + + // Save protobuf to file for debugging + std::fs::write("my_request_proto.bin", &encoded_request).unwrap_or_else(|e| { + eprintln!("Failed to write protobuf to file: {}", e); + }); + + // Create the URL for the SABR request + let qs = { + let collected = query + .into_pairs() + .into_iter() + .filter(|(key, _)| !matches!(key.as_str(), "host" | "rewrite" | "qhash")) + .collect::>(); + eprintln!("Filtered query parameters: {:?}", collected); + QString::new(collected) + }; + + let mut url = Url::parse(&format!("https://{}{}", host, req.path()))?; + url.set_query(Some(qs.to_string().as_str())); + + // Debug output + eprintln!("SABR request URL: {}", url); + eprintln!("SABR request body length: {}", encoded_request.len()); + + // Create POST request with protobuf body + let mut request = Request::new(Method::POST, url); + request.body_mut().replace(Body::from(encoded_request)); + + // Add Content-Type header for protobuf + request + .headers_mut() + .insert("Content-Type", "application/x-protobuf".parse().unwrap()); + + // Execute the request + let resp = client.execute(request).await?; + let status = resp.status(); + + if !status.is_success() { + // Get the response body for debugging + let error_body = resp + .text() + .await + .unwrap_or_else(|_| "Failed to read error body".to_string()); + eprintln!("SABR request failed with status: {}", status); + eprintln!("Response body: {}", error_body); + return Err(format!( + "SABR request failed with status: {} - Body: {}", + status, error_body + ) + .into()); + } + + // Parse the SABR response + let response_bytes = resp.bytes().await?; + let mut parser = SabrParser::new(); + let sabr_response = parser.parse_response(&response_bytes)?; + + // Build the response + let mut response_builder = HttpResponse::Ok(); + + // Add CORS headers + response_builder + .append_header(("Access-Control-Allow-Origin", "*")) + .append_header(("Access-Control-Allow-Headers", "*")) + .append_header(("Access-Control-Allow-Methods", "*")) + .append_header(("Access-Control-Max-Age", "1728000")); + + // Add playback cookie to response headers if available + if let Some(cookie) = parser.get_playback_cookie() { + let mut encoded_cookie = Vec::new(); + cookie.encode(&mut encoded_cookie)?; + let encoded_cookie_b64 = general_purpose::STANDARD.encode(encoded_cookie); + response_builder.append_header(("X-Playback-Cookie", encoded_cookie_b64)); + } + + // Add format-specific content ranges + let mut audio_ranges = Vec::new(); + let mut video_ranges = Vec::new(); + + for format in &sabr_response.initialized_formats { + let is_audio = format + .mime_type + .as_ref() + .map(|mime| mime.starts_with("audio/")) + .unwrap_or(false); + + for chunk in &format.media_chunks { + let range = format!("bytes=0-{}", chunk.len() - 1); + if is_audio { + audio_ranges.push(range); + } else { + video_ranges.push(range); + } + } + } + + if !audio_ranges.is_empty() { + response_builder.append_header(("X-Audio-Content-Ranges", audio_ranges.join(","))); + } + + if !video_ranges.is_empty() { + response_builder.append_header(("X-Video-Content-Ranges", video_ranges.join(","))); + } + + // Combine all media chunks into a single response + let mut combined_data = Vec::new(); + for format in &sabr_response.initialized_formats { + for chunk in &format.media_chunks { + combined_data.extend_from_slice(chunk); + } + } + + Ok(response_builder.body(combined_data)) +} diff --git a/src/sabr_parser.rs b/src/sabr_parser.rs new file mode 100644 index 0000000..4ae0161 --- /dev/null +++ b/src/sabr_parser.rs @@ -0,0 +1,811 @@ +use bytes::Bytes; +use prost::Message; +use std::collections::HashMap; +use std::io; + +// SABR Part Types +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PartType { + OnesieHeader = 10, + OnesieData = 11, + MediaHeader = 20, + Media = 21, + MediaEnd = 22, + LiveMetadata = 31, + HostnameChangeHint = 32, + LiveMetadataPromise = 33, + LiveMetadataPromiseCancellation = 34, + NextRequestPolicy = 35, + UstreamerVideoAndFormatData = 36, + FormatSelectionConfig = 37, + UstreamerSelectedMediaStream = 38, + FormatInitializationMetadata = 42, + SabrRedirect = 43, + SabrError = 44, + SabrSeek = 45, + ReloadPlayerResponse = 46, + PlaybackStartPolicy = 47, + AllowedCachedFormats = 48, + StartBwSamplingHint = 49, + PauseBwSamplingHint = 50, + SelectableFormats = 51, + RequestIdentifier = 52, + RequestCancellationPolicy = 53, + OnesiePrefetchRejection = 54, + TimelineContext = 55, + RequestPipelining = 56, + SabrContextUpdate = 57, + StreamProtectionStatus = 58, + SabrContextSendingPolicy = 59, + LawnmowerPolicy = 60, + SabrAck = 61, + EndOfTrack = 62, + CacheLoadPolicy = 63, + LawnmowerMessagingPolicy = 64, + PrewarmConnection = 65, +} + +impl From for PartType { + fn from(value: i32) -> Self { + match value { + 10 => PartType::OnesieHeader, + 11 => PartType::OnesieData, + 20 => PartType::MediaHeader, + 21 => PartType::Media, + 22 => PartType::MediaEnd, + 31 => PartType::LiveMetadata, + 32 => PartType::HostnameChangeHint, + 33 => PartType::LiveMetadataPromise, + 34 => PartType::LiveMetadataPromiseCancellation, + 35 => PartType::NextRequestPolicy, + 36 => PartType::UstreamerVideoAndFormatData, + 37 => PartType::FormatSelectionConfig, + 38 => PartType::UstreamerSelectedMediaStream, + 42 => PartType::FormatInitializationMetadata, + 43 => PartType::SabrRedirect, + 44 => PartType::SabrError, + 45 => PartType::SabrSeek, + 46 => PartType::ReloadPlayerResponse, + 47 => PartType::PlaybackStartPolicy, + 48 => PartType::AllowedCachedFormats, + 49 => PartType::StartBwSamplingHint, + 50 => PartType::PauseBwSamplingHint, + 51 => PartType::SelectableFormats, + 52 => PartType::RequestIdentifier, + 53 => PartType::RequestCancellationPolicy, + 54 => PartType::OnesiePrefetchRejection, + 55 => PartType::TimelineContext, + 56 => PartType::RequestPipelining, + 57 => PartType::SabrContextUpdate, + 58 => PartType::StreamProtectionStatus, + 59 => PartType::SabrContextSendingPolicy, + 60 => PartType::LawnmowerPolicy, + 61 => PartType::SabrAck, + 62 => PartType::EndOfTrack, + 63 => PartType::CacheLoadPolicy, + 64 => PartType::LawnmowerMessagingPolicy, + 65 => PartType::PrewarmConnection, + _ => panic!("Invalid part type: {}", value), + } + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct FormatId { + #[prost(int32, optional, tag = "1")] + pub itag: Option, + #[prost(int64, optional, tag = "2")] + pub last_modified: Option, + #[prost(string, optional, tag = "3")] + pub xtags: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct TimeRange { + #[prost(int64, optional, tag = "1")] + pub start: Option, + #[prost(int64, optional, tag = "2")] + pub end: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct MediaHeader { + #[prost(uint32, optional, tag = "1")] + pub header_id: Option, + #[prost(string, optional, tag = "2")] + pub video_id: Option, + #[prost(int32, optional, tag = "3")] + pub itag: Option, + #[prost(uint64, optional, tag = "4")] + pub lmt: Option, + #[prost(string, optional, tag = "5")] + pub xtags: Option, + #[prost(int64, optional, tag = "6")] + pub start_range: Option, + #[prost(int32, optional, tag = "7")] + pub compression_algorithm: Option, + #[prost(bool, optional, tag = "8")] + pub is_init_seg: Option, + #[prost(int64, optional, tag = "9")] + pub sequence_number: Option, + #[prost(int64, optional, tag = "10")] + pub field10: Option, + #[prost(int64, optional, tag = "11")] + pub start_ms: Option, + #[prost(int64, optional, tag = "12")] + pub duration_ms: Option, + #[prost(message, optional, tag = "13")] + pub format_id: Option, + #[prost(int64, optional, tag = "14")] + pub content_length: Option, + #[prost(message, optional, tag = "15")] + pub time_range: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct SabrError { + #[prost(string, optional, tag = "1")] + pub error_type: Option, + #[prost(int32, optional, tag = "2")] + pub code: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct SabrRedirect { + #[prost(string, optional, tag = "1")] + pub url: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct StreamProtectionStatus { + #[prost(int32, optional, tag = "1")] + pub status: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct PlaybackCookie { + #[prost(int32, optional, tag = "1")] + pub field1: Option, + #[prost(int32, optional, tag = "2")] + pub field2: Option, + #[prost(message, optional, tag = "7")] + pub video_fmt: Option, + #[prost(message, optional, tag = "8")] + pub audio_fmt: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct NextRequestPolicy { + #[prost(int32, optional, tag = "1")] + pub target_audio_readahead_ms: Option, + #[prost(int32, optional, tag = "2")] + pub target_video_readahead_ms: Option, + #[prost(int32, optional, tag = "4")] + pub backoff_time_ms: Option, + #[prost(message, optional, tag = "7")] + pub playback_cookie: Option, + #[prost(string, optional, tag = "8")] + pub video_id: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct FormatInitializationMetadata { + #[prost(message, optional, tag = "1")] + pub format_id: Option, + #[prost(int64, optional, tag = "2")] + pub duration_ms: Option, + #[prost(string, optional, tag = "3")] + pub mime_type: Option, + #[prost(int64, optional, tag = "4")] + pub end_segment_number: Option, +} + +#[derive(Debug, Clone)] +pub struct Sequence { + pub itag: Option, + pub format_id: Option, + pub is_init_segment: Option, + pub duration_ms: Option, + pub start_ms: Option, + pub start_data_range: Option, + pub sequence_number: Option, + pub content_length: Option, + pub time_range: Option, +} + +#[derive(Debug, Clone)] +pub struct InitializedFormat { + pub format_id: FormatId, + pub format_key: String, + pub duration_ms: Option, + pub mime_type: Option, + pub sequence_count: Option, + pub sequence_list: Vec, + pub media_chunks: Vec, +} + +#[derive(Debug, Clone)] +pub struct SabrResponse { + pub initialized_formats: Vec, + pub stream_protection_status: Option, + pub sabr_redirect: Option, + pub sabr_error: Option, + pub next_request_policy: Option, +} + +#[derive(Debug)] +pub struct UmpPart { + pub part_type: PartType, + pub size: usize, + pub data: Bytes, +} + +pub struct SabrParser { + header_id_to_format_key: HashMap, + formats_by_key: HashMap, + initialized_formats: Vec, + playback_cookie: Option, +} + +impl SabrParser { + pub fn new() -> Self { + Self { + header_id_to_format_key: HashMap::new(), + formats_by_key: HashMap::new(), + initialized_formats: Vec::new(), + playback_cookie: None, + } + } + + pub fn parse_response(&mut self, data: &[u8]) -> io::Result { + self.header_id_to_format_key.clear(); + + // Clear sequence lists and media chunks for existing formats + for format in &mut self.initialized_formats { + format.sequence_list.clear(); + format.media_chunks.clear(); + } + + let mut sabr_error: Option = None; + let mut sabr_redirect: Option = None; + let mut stream_protection_status: Option = None; + let mut next_request_policy: Option = None; + + let mut offset = 0; + while offset < data.len() { + match self.parse_ump_part(&data[offset..]) { + Ok((part, consumed)) => { + offset += consumed; + + match part.part_type { + PartType::MediaHeader => { + self.process_media_header(&part.data)?; + } + PartType::Media => { + self.process_media_data(&part.data)?; + } + PartType::MediaEnd => { + self.process_media_end(&part.data)?; + } + PartType::NextRequestPolicy => { + let policy = NextRequestPolicy::decode(&part.data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // Store playback cookie for use in subsequent requests + if let Some(cookie) = &policy.playback_cookie { + self.playback_cookie = Some(cookie.clone()); + } + + next_request_policy = Some(policy); + } + PartType::FormatInitializationMetadata => { + self.process_format_initialization(&part.data)?; + } + PartType::SabrError => { + sabr_error = Some( + SabrError::decode(&part.data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ); + } + PartType::SabrRedirect => { + sabr_redirect = Some( + SabrRedirect::decode(&part.data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ); + } + PartType::StreamProtectionStatus => { + stream_protection_status = Some( + StreamProtectionStatus::decode(&part.data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ); + } + _ => { + // Ignore other part types for now + } + } + } + Err(_) => break, // Not enough data or invalid part + } + } + + // Update initialized_formats with the processed data from formats_by_key + self.initialized_formats.clear(); + for format in self.formats_by_key.values() { + self.initialized_formats.push(format.clone()); + } + + // Sort by format_key to ensure deterministic ordering + self.initialized_formats + .sort_by(|a, b| a.format_key.cmp(&b.format_key)); + + Ok(SabrResponse { + initialized_formats: self.initialized_formats.clone(), + stream_protection_status, + sabr_redirect, + sabr_error, + next_request_policy, + }) + } + + fn parse_ump_part(&self, data: &[u8]) -> io::Result<(UmpPart, usize)> { + let mut offset = 0; + + let (part_type, consumed) = self.read_varint(&data[offset..])?; + offset += consumed; + + let (part_size, consumed) = self.read_varint(&data[offset..])?; + offset += consumed; + + if data.len() < offset + part_size as usize { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Not enough data for part", + )); + } + + let part_data = Bytes::copy_from_slice(&data[offset..offset + part_size as usize]); + offset += part_size as usize; + + Ok(( + UmpPart { + part_type: PartType::from(part_type), + size: part_size as usize, + data: part_data, + }, + offset, + )) + } + + fn read_varint(&self, data: &[u8]) -> io::Result<(i32, usize)> { + if data.is_empty() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "No data to read varint", + )); + } + + let first_byte = data[0]; + let byte_length = if first_byte < 128 { + 1 + } else if first_byte < 192 { + 2 + } else if first_byte < 224 { + 3 + } else if first_byte < 240 { + 4 + } else { + 5 + }; + + if data.len() < byte_length { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Not enough data for varint", + )); + } + + let value = match byte_length { + 1 => data[0] as i32, + 2 => { + let byte1 = data[0]; + let byte2 = data[1]; + (byte1 as i32 & 0x3f) + 64 * (byte2 as i32) + } + 3 => { + let byte1 = data[0]; + let byte2 = data[1]; + let byte3 = data[2]; + (byte1 as i32 & 0x1f) + 32 * (byte2 as i32 + 256 * byte3 as i32) + } + 4 => { + let byte1 = data[0]; + let byte2 = data[1]; + let byte3 = data[2]; + let byte4 = data[3]; + (byte1 as i32 & 0x0f) + + 16 * (byte2 as i32 + 256 * (byte3 as i32 + 256 * byte4 as i32)) + } + _ => { + let value = u32::from_le_bytes([data[1], data[2], data[3], data[4]]) as i32; + value + } + }; + + Ok((value, byte_length)) + } + + fn process_media_header(&mut self, data: &Bytes) -> io::Result<()> { + let media_header = MediaHeader::decode(&data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + if let Some(format_id) = &media_header.format_id { + let format_key = self.get_format_key(format_id); + + // Register format if not exists + if !self.formats_by_key.contains_key(&format_key) { + self.register_format_from_header(&media_header); + } + + // Save header ID mapping + if let Some(header_id) = media_header.header_id { + self.header_id_to_format_key + .insert(header_id, format_key.clone()); + } + + // Add sequence to format + if let Some(format) = self.formats_by_key.get_mut(&format_key) { + let sequence = Sequence { + itag: media_header.itag, + format_id: media_header.format_id.clone(), + is_init_segment: media_header.is_init_seg, + duration_ms: media_header.duration_ms, + start_ms: media_header.start_ms, + start_data_range: media_header.start_range, + sequence_number: media_header.sequence_number, + content_length: media_header.content_length, + time_range: media_header.time_range.clone(), + }; + format.sequence_list.push(sequence); + } + } + + Ok(()) + } + + fn process_media_data(&mut self, data: &Bytes) -> io::Result<()> { + if data.is_empty() { + return Ok(()); + } + + let header_id = data[0] as u32; + let stream_data = data.slice(1..); + + if let Some(format_key) = self.header_id_to_format_key.get(&header_id) { + if let Some(format) = self.formats_by_key.get_mut(format_key) { + format.media_chunks.push(stream_data); + } + } + + Ok(()) + } + + fn process_media_end(&mut self, data: &Bytes) -> io::Result<()> { + if !data.is_empty() { + let header_id = data[0] as u32; + self.header_id_to_format_key.remove(&header_id); + } + Ok(()) + } + + fn process_format_initialization(&mut self, data: &Bytes) -> io::Result<()> { + let format_init = FormatInitializationMetadata::decode(&data[..]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + self.register_format_from_init(&format_init); + Ok(()) + } + + fn register_format_from_header(&mut self, header: &MediaHeader) { + if let Some(format_id) = &header.format_id { + let format_key = self.get_format_key(format_id); + + let format = InitializedFormat { + format_id: format_id.clone(), + format_key: format_key.clone(), + duration_ms: header.duration_ms, + mime_type: None, + sequence_count: None, + sequence_list: Vec::new(), + media_chunks: Vec::new(), + }; + + self.initialized_formats.push(format.clone()); + self.formats_by_key.insert(format_key.clone(), format); + } + } + + fn register_format_from_init(&mut self, init: &FormatInitializationMetadata) { + if let Some(format_id) = &init.format_id { + let format_key = self.get_format_key(format_id); + + if !self.formats_by_key.contains_key(&format_key) { + let format = InitializedFormat { + format_id: format_id.clone(), + format_key: format_key.clone(), + duration_ms: init.duration_ms, + mime_type: init.mime_type.clone(), + sequence_count: init.end_segment_number, + sequence_list: Vec::new(), + media_chunks: Vec::new(), + }; + + self.initialized_formats.push(format.clone()); + self.formats_by_key.insert(format_key.clone(), format); + } + } + } + + pub fn get_playback_cookie(&self) -> Option<&PlaybackCookie> { + self.playback_cookie.as_ref() + } + + fn get_format_key(&self, format_id: &FormatId) -> String { + format!( + "{};{};", + format_id.itag.unwrap_or(0), + format_id.last_modified.unwrap_or(0) + ) + } +} + +impl Default for SabrParser { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_sabr_parser_with_bin_file() { + // Read the sabr_response.bin file + let data = fs::read("test/sabr_response.bin").expect("Failed to read sabr_response.bin"); + + let mut parser = SabrParser::new(); + match parser.parse_response(&data) { + Ok(response) => { + // Verify the next_request_policy fields + assert!(response.next_request_policy.is_some()); + if let Some(policy) = &response.next_request_policy { + assert_eq!(policy.target_audio_readahead_ms, Some(15016)); + assert_eq!(policy.target_video_readahead_ms, Some(15016)); + assert_eq!(policy.backoff_time_ms, None); + assert_eq!(policy.video_id, None); + + // Verify playback cookie + assert!(policy.playback_cookie.is_some()); + if let Some(cookie) = &policy.playback_cookie { + // Verify video format + assert!(cookie.video_fmt.is_some()); + if let Some(video_fmt) = &cookie.video_fmt { + assert_eq!(video_fmt.itag, Some(136)); + assert_eq!(video_fmt.last_modified, Some(1747754870573293)); + assert_eq!(video_fmt.xtags, None); + } + + // Verify audio format + assert!(cookie.audio_fmt.is_some()); + if let Some(audio_fmt) = &cookie.audio_fmt { + assert_eq!(audio_fmt.itag, Some(251)); + assert_eq!(audio_fmt.last_modified, Some(1747754876286051)); + assert_eq!(audio_fmt.xtags, None); + } + } + } + + // Assert the expected number of initialized formats + assert_eq!(response.initialized_formats.len(), 2); + + // Assert that we have a stream protection status + assert!(response.stream_protection_status.is_some()); + + if let Some(status) = &response.stream_protection_status { + // Assert the expected stream protection status + assert_eq!(status.status, Some(1)); + } + + // Assert specific format details + assert_eq!( + response.initialized_formats[0].format_key, + "136;1747754870573293;" + ); + assert_eq!(response.initialized_formats[0].sequence_list.len(), 4); + assert_eq!(response.initialized_formats[0].media_chunks.len(), 16); + + // Verify specific chunk sizes for format 136 (video) + let video_chunk_sizes: Vec = response.initialized_formats[0] + .media_chunks + .iter() + .map(|c| c.len()) + .collect(); + assert_eq!( + video_chunk_sizes, + vec![ + 32768, 32768, 12433, 32768, 32768, 16936, 32768, 32768, 29449, 6024, 16384, + 16384, 16384, 16384, 16384, 1942 + ] + ); + + // Verify sequence details for format 136 (video) + assert_eq!( + response.initialized_formats[0].sequence_list[0].sequence_number, + Some(26) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[0].start_ms, + Some(123888) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[0].duration_ms, + Some(5008) + ); + + assert_eq!( + response.initialized_formats[0].sequence_list[1].sequence_number, + Some(27) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[1].start_ms, + Some(128896) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[1].duration_ms, + Some(5008) + ); + + assert_eq!( + response.initialized_formats[0].sequence_list[2].sequence_number, + Some(28) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[2].start_ms, + Some(133903) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[2].duration_ms, + Some(5008) + ); + + assert_eq!( + response.initialized_formats[0].sequence_list[3].sequence_number, + Some(29) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[3].start_ms, + Some(138910) + ); + assert_eq!( + response.initialized_formats[0].sequence_list[3].duration_ms, + Some(4974) + ); + + assert_eq!( + response.initialized_formats[1].format_key, + "251;1747754876286051;" + ); + assert_eq!(response.initialized_formats[1].sequence_list.len(), 3); + assert_eq!(response.initialized_formats[1].media_chunks.len(), 15); + + // Verify specific chunk sizes for format 251 (audio) + let audio_chunk_sizes: Vec = response.initialized_formats[1] + .media_chunks + .iter() + .map(|c| c.len()) + .collect(); + assert_eq!( + audio_chunk_sizes, + vec![ + 32768, 32768, 32768, 32768, 2862, 32768, 32768, 32768, 32768, 1553, 32768, + 32768, 32768, 32768, 2552 + ] + ); + + // Verify sequence details for format 251 (audio) + assert_eq!( + response.initialized_formats[1].sequence_list[0].sequence_number, + Some(13) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[0].start_ms, + Some(120001) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[0].duration_ms, + Some(10000) + ); + + assert_eq!( + response.initialized_formats[1].sequence_list[1].sequence_number, + Some(14) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[1].start_ms, + Some(130001) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[1].duration_ms, + Some(10000) + ); + + assert_eq!( + response.initialized_formats[1].sequence_list[2].sequence_number, + Some(15) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[2].start_ms, + Some(140001) + ); + assert_eq!( + response.initialized_formats[1].sequence_list[2].duration_ms, + Some(10000) + ); + + // Verify chunk distribution per sequence (based on our analysis) + // Format 136 should have chunks distributed as: 3 + 3 + 3 + 7 = 16 total + // Format 251 should have chunks distributed as: 5 + 5 + 5 = 15 total + + // Verify that all sequences have the expected itag values + for seq in &response.initialized_formats[0].sequence_list { + assert_eq!(seq.itag, Some(136)); + } + for seq in &response.initialized_formats[1].sequence_list { + assert_eq!(seq.itag, Some(251)); + } + } + Err(e) => { + panic!("Failed to parse SABR response: {}", e); + } + } + } + + #[test] + fn test_varint_parsing() { + let parser = SabrParser::new(); + + // Test single byte varint + let data = [0x08]; // 8 in varint encoding + let (value, consumed) = parser.read_varint(&data).unwrap(); + assert_eq!(value, 8); + assert_eq!(consumed, 1); + + // Test two byte varint - let's use a correct encoding + // For 150: first byte should be >= 128 to indicate 2-byte encoding + // 150 = (first_byte & 0x3f) + 64 * second_byte + // Let's use [0x96, 0x01] which should give us: (0x96 & 0x3f) + 64 * 0x01 = 22 + 64 = 86 + let data = [0x96, 0x01]; + let (value, consumed) = parser.read_varint(&data).unwrap(); + assert_eq!(value, 86); // Corrected expected value + assert_eq!(consumed, 2); + + // Test another two-byte varint for 150 + // 150 = (first_byte & 0x3f) + 64 * second_byte + // We need: 150 = x + 64 * y where x <= 63 + // 150 = 22 + 64 * 2, so first_byte = 128 + 22 = 150, second_byte = 2 + let data = [0x96, 0x02]; // This should give us (0x96 & 0x3f) + 64 * 0x02 = 22 + 128 = 150 + let (value, consumed) = parser.read_varint(&data).unwrap(); + assert_eq!(value, 150); + assert_eq!(consumed, 2); + } + + #[test] + fn test_part_type_conversion() { + assert_eq!(PartType::from(20), PartType::MediaHeader); + assert_eq!(PartType::from(21), PartType::Media); + assert_eq!(PartType::from(35), PartType::NextRequestPolicy); + assert_eq!(PartType::from(43), PartType::SabrRedirect); + assert_eq!(PartType::from(44), PartType::SabrError); + } +} diff --git a/src/sabr_request.rs b/src/sabr_request.rs new file mode 100644 index 0000000..f7c5dce --- /dev/null +++ b/src/sabr_request.rs @@ -0,0 +1,473 @@ +use prost::Message; + +// Re-export the FormatId from sabr_parser for consistency +pub use crate::sabr_parser::FormatId; + +#[derive(Clone, PartialEq, Message)] +pub struct ClientInfo { + #[prost(string, optional, tag = "12")] + pub device_make: Option, + #[prost(string, optional, tag = "13")] + pub device_model: Option, + #[prost(int32, optional, tag = "16")] + pub client_name: Option, + #[prost(string, optional, tag = "17")] + pub client_version: Option, + #[prost(string, optional, tag = "18")] + pub os_name: Option, + #[prost(string, optional, tag = "19")] + pub os_version: Option, + #[prost(string, optional, tag = "21")] + pub accept_language: Option, + #[prost(string, optional, tag = "22")] + pub accept_region: Option, + #[prost(int32, optional, tag = "37")] + pub screen_width_points: Option, + #[prost(int32, optional, tag = "38")] + pub screen_height_points: Option, + #[prost(float, optional, tag = "39")] + pub screen_width_inches: Option, + #[prost(float, optional, tag = "40")] + pub screen_height_inches: Option, + #[prost(int32, optional, tag = "41")] + pub screen_pixel_density: Option, + #[prost(int32, optional, tag = "46")] + pub client_form_factor: Option, + #[prost(int32, optional, tag = "50")] + pub gmscore_version_code: Option, + #[prost(int32, optional, tag = "55")] + pub window_width_points: Option, + #[prost(int32, optional, tag = "56")] + pub window_height_points: Option, + #[prost(int32, optional, tag = "64")] + pub android_sdk_version: Option, + #[prost(float, optional, tag = "65")] + pub screen_density_float: Option, + #[prost(int64, optional, tag = "67")] + pub utc_offset_minutes: Option, + #[prost(string, optional, tag = "80")] + pub time_zone: Option, + #[prost(string, optional, tag = "92")] + pub chipset: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct StreamerContext { + #[prost(message, optional, tag = "1")] + pub client_info: Option, + #[prost(bytes = "vec", optional, tag = "2")] + pub po_token: Option>, + #[prost(bytes = "vec", optional, tag = "3")] + pub playback_cookie: Option>, + #[prost(bytes = "vec", optional, tag = "4")] + pub gp: Option>, + #[prost(bytes = "vec", repeated, tag = "5")] + pub field5: Vec>, + #[prost(int32, repeated, tag = "6")] + pub field6: Vec, + #[prost(string, optional, tag = "7")] + pub field7: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct TimeRange { + #[prost(int64, optional, tag = "1")] + pub start: Option, + #[prost(int64, optional, tag = "2")] + pub end: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct BufferedRange { + #[prost(message, optional, tag = "1")] + pub format_id: Option, + #[prost(int64, optional, tag = "2")] + pub start_time_ms: Option, + #[prost(int64, optional, tag = "3")] + pub duration_ms: Option, + #[prost(int32, optional, tag = "4")] + pub start_segment_index: Option, + #[prost(int32, optional, tag = "5")] + pub end_segment_index: Option, + #[prost(message, optional, tag = "6")] + pub time_range: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct ClientAbrState { + #[prost(int64, optional, tag = "13")] + pub time_since_last_manual_format_selection_ms: Option, + #[prost(sint32, optional, tag = "14")] + pub last_manual_direction: Option, + #[prost(int32, optional, tag = "16")] + pub last_manual_selected_resolution: Option, + #[prost(int32, optional, tag = "17")] + pub detailed_network_type: Option, + #[prost(int32, optional, tag = "18")] + pub client_viewport_width: Option, + #[prost(int32, optional, tag = "19")] + pub client_viewport_height: Option, + #[prost(int64, optional, tag = "20")] + pub client_bitrate_cap_bytes_per_sec: Option, + #[prost(int32, optional, tag = "21")] + pub sticky_resolution: Option, + #[prost(bool, optional, tag = "22")] + pub client_viewport_is_flexible: Option, + #[prost(int64, optional, tag = "23")] + pub bandwidth_estimate: Option, + #[prost(int32, optional, tag = "24")] + pub min_audio_quality: Option, + #[prost(int32, optional, tag = "25")] + pub max_audio_quality: Option, + #[prost(int32, optional, tag = "26")] + pub video_quality_setting: Option, + #[prost(int32, optional, tag = "27")] + pub audio_route: Option, + #[prost(int64, optional, tag = "28")] + pub player_time_ms: Option, + #[prost(int64, optional, tag = "29")] + pub time_since_last_seek: Option, + #[prost(bool, optional, tag = "30")] + pub data_saver_mode: Option, + #[prost(int32, optional, tag = "32")] + pub network_metered_state: Option, + #[prost(int32, optional, tag = "34")] + pub visibility: Option, + #[prost(float, optional, tag = "35")] + pub playback_rate: Option, + #[prost(int64, optional, tag = "36")] + pub elapsed_wall_time_ms: Option, + #[prost(bytes = "vec", optional, tag = "38")] + pub media_capabilities: Option>, + #[prost(int64, optional, tag = "39")] + pub time_since_last_action_ms: Option, + #[prost(int32, optional, tag = "40")] + pub enabled_track_types_bitfield: Option, + #[prost(int32, optional, tag = "43")] + pub max_pacing_rate: Option, + #[prost(int64, optional, tag = "44")] + pub player_state: Option, + #[prost(bool, optional, tag = "46")] + pub drc_enabled: Option, + #[prost(int32, optional, tag = "48")] + pub jda: Option, + #[prost(int32, optional, tag = "50")] + pub qw: Option, + #[prost(int32, optional, tag = "51")] + pub ky: Option, + #[prost(int32, optional, tag = "54")] + pub sabr_report_request_cancellation_info: Option, + #[prost(bool, optional, tag = "56")] + pub l: Option, + #[prost(int64, optional, tag = "57")] + pub g7: Option, + #[prost(bool, optional, tag = "58")] + pub prefer_vp9: Option, + #[prost(int32, optional, tag = "59")] + pub qj: Option, + #[prost(int32, optional, tag = "60")] + pub hx: Option, + #[prost(bool, optional, tag = "61")] + pub is_prefetch: Option, + #[prost(int32, optional, tag = "62")] + pub sabr_support_quality_constraints: Option, + #[prost(bytes = "vec", optional, tag = "63")] + pub sabr_license_constraint: Option>, + #[prost(int32, optional, tag = "64")] + pub allow_proxima_live_latency: Option, + #[prost(int32, optional, tag = "66")] + pub sabr_force_proxima: Option, + #[prost(int32, optional, tag = "67")] + pub tqb: Option, + #[prost(int64, optional, tag = "68")] + pub sabr_force_max_network_interruption_duration_ms: Option, + #[prost(string, optional, tag = "69")] + pub audio_track_id: Option, +} + +#[derive(Clone, PartialEq, Message)] +pub struct VideoPlaybackAbrRequest { + #[prost(message, optional, tag = "1")] + pub client_abr_state: Option, + #[prost(message, repeated, tag = "2")] + pub selected_format_ids: Vec, + #[prost(message, repeated, tag = "3")] + pub buffered_ranges: Vec, + #[prost(int64, optional, tag = "4")] + pub player_time_ms: Option, + #[prost(bytes = "vec", optional, tag = "5")] + pub video_playback_ustreamer_config: Option>, + #[prost(message, repeated, tag = "16")] + pub selected_audio_format_ids: Vec, + #[prost(message, repeated, tag = "17")] + pub selected_video_format_ids: Vec, + #[prost(message, optional, tag = "19")] + pub streamer_context: Option, + #[prost(int32, optional, tag = "22")] + pub field22: Option, + #[prost(int32, optional, tag = "23")] + pub field23: Option, + #[prost(message, repeated, tag = "1000")] + pub field1000: Vec, +} + +#[derive(Clone, PartialEq, Message)] +pub struct Pqa { + #[prost(message, repeated, tag = "1")] + pub formats: Vec, + #[prost(message, repeated, tag = "2")] + pub ud: Vec, + #[prost(string, optional, tag = "3")] + pub clip_id: Option, +} + +// Default quality constant +const DEFAULT_QUALITY: i32 = 720; // HD720 + +#[derive(Debug, Clone)] +pub struct SabrRequestBuilder { + pub client_abr_state: ClientAbrState, + pub streamer_context: StreamerContext, + pub video_playback_ustreamer_config: Option>, + pub selected_audio_format_ids: Vec, + pub selected_video_format_ids: Vec, + pub buffered_ranges: Vec, +} + +impl SabrRequestBuilder { + pub fn new() -> Self { + Self { + // Minimal defaults matching googlevideo pattern + client_abr_state: ClientAbrState { + last_manual_direction: Some(0), + time_since_last_manual_format_selection_ms: Some(0), + last_manual_selected_resolution: Some(DEFAULT_QUALITY), + sticky_resolution: Some(DEFAULT_QUALITY), + player_time_ms: Some(0), + visibility: Some(0), + enabled_track_types_bitfield: Some(0), + ..Default::default() + }, + streamer_context: StreamerContext { + field5: Vec::new(), + field6: Vec::new(), + // Basic client info matching googlevideo + client_info: Some(ClientInfo { + client_name: Some(1), + client_version: Some("2.2040620.05.00".to_string()), + os_name: Some("Windows".to_string()), + os_version: Some("10.0".to_string()), + ..Default::default() + }), + ..Default::default() + }, + video_playback_ustreamer_config: None, + selected_audio_format_ids: Vec::new(), + selected_video_format_ids: Vec::new(), + buffered_ranges: Vec::new(), + } + } + + pub fn with_client_info(mut self, client_info: ClientInfo) -> Self { + self.streamer_context.client_info = Some(client_info); + self + } + + pub fn with_po_token(mut self, po_token: Vec) -> Self { + self.streamer_context.po_token = Some(po_token); + self + } + + pub fn with_playback_cookie(mut self, playback_cookie: Vec) -> Self { + self.streamer_context.playback_cookie = Some(playback_cookie); + self + } + + pub fn with_video_playback_ustreamer_config(mut self, config: Vec) -> Self { + self.video_playback_ustreamer_config = Some(config); + self + } + + pub fn with_player_time_ms(mut self, time_ms: i64) -> Self { + self.client_abr_state.player_time_ms = Some(time_ms); + self + } + + pub fn with_resolution(mut self, resolution: i32) -> Self { + self.client_abr_state.last_manual_selected_resolution = Some(resolution); + self.client_abr_state.sticky_resolution = Some(resolution); + self + } + + pub fn with_viewport_size(mut self, width: i32, height: i32) -> Self { + self.client_abr_state.client_viewport_width = Some(width); + self.client_abr_state.client_viewport_height = Some(height); + self + } + + pub fn with_bandwidth_estimate(mut self, bandwidth: i64) -> Self { + self.client_abr_state.bandwidth_estimate = Some(bandwidth); + self + } + + pub fn with_audio_formats(mut self, formats: Vec) -> Self { + self.selected_audio_format_ids = formats; + self + } + + pub fn with_video_formats(mut self, formats: Vec) -> Self { + self.selected_video_format_ids = formats; + self + } + + pub fn with_buffered_ranges(mut self, ranges: Vec) -> Self { + self.buffered_ranges = ranges; + self + } + + pub fn with_enabled_track_types(mut self, bitfield: i32) -> Self { + self.client_abr_state.enabled_track_types_bitfield = Some(bitfield); + self + } + + pub fn with_visibility(mut self, visibility: i32) -> Self { + self.client_abr_state.visibility = Some(visibility); + self + } + + pub fn with_playback_rate(mut self, rate: f32) -> Self { + self.client_abr_state.playback_rate = Some(rate); + self + } + + pub fn build(self) -> VideoPlaybackAbrRequest { + // selectedFormatIds should contain all format IDs (both audio and video) + let mut selected_format_ids = Vec::new(); + selected_format_ids.extend(self.selected_audio_format_ids.clone()); + selected_format_ids.extend(self.selected_video_format_ids.clone()); + + VideoPlaybackAbrRequest { + client_abr_state: Some(self.client_abr_state), + selected_format_ids, + buffered_ranges: self.buffered_ranges, + player_time_ms: Some(0), + video_playback_ustreamer_config: self.video_playback_ustreamer_config, + selected_audio_format_ids: self.selected_audio_format_ids, + selected_video_format_ids: self.selected_video_format_ids, + streamer_context: Some(self.streamer_context), + field22: Some(0), + field23: Some(0), + field1000: Vec::new(), + } + } + + pub fn encode(self) -> Vec { + let request = self.build(); + request.encode_to_vec() + } +} + +impl Default for SabrRequestBuilder { + fn default() -> Self { + Self::new() + } +} + +// Utility functions for creating common format IDs and buffered ranges +pub fn create_format_id(itag: i32, last_modified: u64, xtags: Option) -> FormatId { + FormatId { + itag: Some(itag), + last_modified: Some(last_modified as i64), + xtags, + } +} + +pub fn create_buffered_range( + format_id: FormatId, + start_time_ms: i64, + duration_ms: i64, + start_segment_index: i32, + end_segment_index: i32, +) -> BufferedRange { + BufferedRange { + format_id: Some(format_id), + start_time_ms: Some(start_time_ms), + duration_ms: Some(duration_ms), + start_segment_index: Some(start_segment_index), + end_segment_index: Some(end_segment_index), + time_range: None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sabr_request_builder() { + let audio_format = create_format_id(251, 1747754876286051, None); + let video_format = create_format_id(136, 1747754870573293, None); + + let buffered_range = create_buffered_range(audio_format.clone(), 0, 5000, 1, 5); + + let request = SabrRequestBuilder::new() + .with_player_time_ms(1000) + .with_resolution(720) + .with_viewport_size(1280, 720) + .with_bandwidth_estimate(1000000) + .with_audio_formats(vec![audio_format]) + .with_video_formats(vec![video_format]) + .with_buffered_ranges(vec![buffered_range]) + .with_enabled_track_types(0) + .with_visibility(0) + .with_playback_rate(1.0) + .build(); + + assert!(request.client_abr_state.is_some()); + assert!(request.streamer_context.is_some()); + assert_eq!(request.selected_audio_format_ids.len(), 1); + assert_eq!(request.selected_video_format_ids.len(), 1); + assert_eq!(request.buffered_ranges.len(), 1); + + // Test encoding + let encoded = SabrRequestBuilder::new().with_player_time_ms(1000).encode(); + + assert!(!encoded.is_empty()); + } + + #[test] + fn test_format_id_creation() { + let format_id = create_format_id(251, 1747754876286051, Some("test".to_string())); + + assert_eq!(format_id.itag, Some(251)); + assert_eq!(format_id.last_modified, Some(1747754876286051)); + assert_eq!(format_id.xtags, Some("test".to_string())); + } + + #[test] + fn test_default_values() { + let builder = SabrRequestBuilder::new(); + assert_eq!( + builder.client_abr_state.last_manual_selected_resolution, + Some(DEFAULT_QUALITY) + ); + assert_eq!( + builder.client_abr_state.sticky_resolution, + Some(DEFAULT_QUALITY) + ); + assert_eq!(builder.client_abr_state.player_time_ms, Some(0)); + assert_eq!(builder.client_abr_state.visibility, Some(0)); + assert_eq!( + builder.client_abr_state.enabled_track_types_bitfield, + Some(0) + ); + + let client_info = builder.streamer_context.client_info.unwrap(); + assert_eq!(client_info.client_name, Some(1)); + assert_eq!( + client_info.client_version, + Some("2.2040620.05.00".to_string()) + ); + assert_eq!(client_info.os_name, Some("Windows".to_string())); + } +} diff --git a/test/sabr_response.bin b/test/sabr_response.bin new file mode 100644 index 0000000000000000000000000000000000000000..36d624b5637263a7e7b88ede30c6f1acaa493ec1 GIT binary patch literal 746341 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 z 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Âr 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