diff --git a/package-lock.json b/package-lock.json index b24a0b376..e109c5167 100644 --- a/package-lock.json +++ b/package-lock.json @@ -701,6 +701,54 @@ "node": ">=16" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "cpu": [ @@ -716,6 +764,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -4713,30 +5049,101 @@ "version": "1.2.0", "license": "ISC" }, - "node_modules/@vitejs/plugin-react": { - "version": "3.1.0", - "dev": true, - "license": "MIT", + "node_modules/@videojs/http-streaming": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.3.tgz", + "integrity": "sha512-L7H+iTeqHeZ5PylzOx+pT3CVyzn4TALWYTJKkIc1pDaV/cTVfNGtG+9/vXPAydD+wR/xH1M9/t2JH8tn/DCT4w==", "dependencies": { - "@babel/core": "^7.20.12", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", - "react-refresh": "^0.14.0" + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", + "global": "^4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.3.0", + "mux.js": "7.0.3", + "video.js": "^7 || ^8" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": ">=8", + "npm": ">=5" }, "peerDependencies": { - "vite": "^4.1.0-beta.0" + "video.js": "^8.14.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dev": true, - "peer": true, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", + "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -4892,6 +5299,14 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -4935,6 +5350,30 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/agent-base": { "version": "7.1.0", "dev": true, @@ -6102,6 +6541,11 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dotenv": { "version": "16.4.4", "dev": true, @@ -6368,86 +6812,422 @@ "es-errors": "^1.3.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.18.20", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/es6-error": { - "version": "4.1.1", + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/esbuild": { + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" } }, "node_modules/escalade": { @@ -7651,6 +8431,15 @@ "dev": true, "peer": true }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/global-modules": { "version": "0.2.3", "dev": true, @@ -8242,6 +9031,11 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "node_modules/is-generator-function": { "version": "1.0.10", "license": "MIT", @@ -9233,6 +10027,29 @@ "version": "0.2.0", "license": "ISC" }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, + "node_modules/m3u8-parser/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/magic-string": { "version": "0.27.0", "dev": true, @@ -9338,6 +10155,14 @@ "node": ">=6" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/min-indent": { "version": "1.0.1", "license": "MIT", @@ -9593,10 +10418,40 @@ "node": "*" } }, + "node_modules/mpd-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", + "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.2", "license": "MIT" }, + "node_modules/mux.js": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", + "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/nanoid": { "version": "3.3.7", "dev": true, @@ -10381,6 +11236,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "dev": true, @@ -10580,6 +11446,14 @@ "node": ">=0.8" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-on-spawn": { "version": "1.0.0", "dev": true, @@ -10919,6 +11793,10 @@ "resolved": "samples/javascript/02.react-video", "link": true }, + "node_modules/react-video-js": { + "resolved": "samples/typescript/11.react-video-js", + "link": true + }, "node_modules/read-pkg-up": { "version": "7.0.1", "dev": true, @@ -12595,6 +13473,11 @@ "node": ">= 0.4" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "license": "MIT" @@ -12652,6 +13535,53 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/video.js": { + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.4.tgz", + "integrity": "sha512-AECieAxKMKB/QgYK36ci50phfpWys6bFT6+pGMpSafeFYSoZaQ2Vpl83T9Qqcesv4TO7oNtiycnVeaBnrva2oA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "3.13.3", + "@videojs/vhs-utils": "^4.0.0", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.2.2", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/vite": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", @@ -13680,6 +14610,105 @@ "vite": "^4.0.4" } }, + "samples/typescript/11.react-video-js": { + "name": "react-video-js", + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "@fluentui/react-components": "^9.18.6", + "@fluentui/react-icons": "^2.0.202", + "@fluentui/react-theme": "^9.1.5", + "@microsoft/live-share": "2.0.0-internal.8", + "@microsoft/live-share-canvas": "2.0.0-internal.8", + "@microsoft/live-share-media": "2.0.0-internal.8", + "@microsoft/teams-js": "^2.16.0", + "fluid-framework": "^2.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.4.2", + "react-router-dom": "^6.4.2", + "use-resize-observer": "^9.1.0", + "uuid": "^9.0.0", + "video.js": "^8.17.4", + "web-vitals": "^3.1.1" + }, + "devDependencies": { + "@fluidframework/test-runtime-utils": "^2.0.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^14.4.3", + "@types/lodash": "^4.14.191", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", + "@vitejs/plugin-react": "^3.0.1", + "dotenv-cli": "^7.2.1", + "esbuild": "^0.17.18", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.4", + "eslint-plugin-react-hooks": "^4.6.2", + "start-server-and-test": "^2.0.0", + "vite": "^4.0.4" + } + }, + "samples/typescript/11.react-video-js/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "samples/typescript/11.react-video-js/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, "samples/typescript/21.react-media-template": { "name": "react-media-template-ts", "version": "0.4.0", diff --git a/packages/live-share-react/src/live-hooks/useLiveCanvas.ts b/packages/live-share-react/src/live-hooks/useLiveCanvas.ts index c08f64288..4629addb3 100644 --- a/packages/live-share-react/src/live-hooks/useLiveCanvas.ts +++ b/packages/live-share-react/src/live-hooks/useLiveCanvas.ts @@ -89,7 +89,7 @@ export function ExampleLiveCanvas() { export function useLiveCanvas( uniqueKey: string, canvasElementRef: React.RefObject | string, - props: IUseLiveCanvasOptionalProps + props?: IUseLiveCanvasOptionalProps ): IUseLiveCanvasResults { /** * User facing: inking manager instance diff --git a/samples/javascript/01.dice-roller/README.md b/samples/javascript/01.dice-roller/README.md index 9aac863fc..0ead2f39f 100644 --- a/samples/javascript/01.dice-roller/README.md +++ b/samples/javascript/01.dice-roller/README.md @@ -13,15 +13,11 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/01* -npm start ``` Running `npm start`, it will do two things: start the `tinylicious` server and start the application using `vite`. If you have never used `tinylicious` before, you should see instead is a prompt saying `Ok to proceed? (y)`, after which you should type `y` and press the "enter" key. In rare cases you might not see the `Ok to proceed? (y)` prompt, in which case try running `npx tinylicious@latest` in your command line directly, and then try again. -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing the app in Teams ### Create a ngrok tunnel to allow Teams to reach your tab app diff --git a/samples/javascript/02.react-video/README.md b/samples/javascript/02.react-video/README.md index 2bf380647..0643f84c5 100644 --- a/samples/javascript/02.react-video/README.md +++ b/samples/javascript/02.react-video/README.md @@ -8,12 +8,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/02* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/03.live-canvas-demo/README.md b/samples/javascript/03.live-canvas-demo/README.md index 51583e1cd..cc8c96a5d 100644 --- a/samples/javascript/03.live-canvas-demo/README.md +++ b/samples/javascript/03.live-canvas-demo/README.md @@ -8,12 +8,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/03* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/04.live-share-react/README.md b/samples/javascript/04.live-share-react/README.md index 8d32c18a0..c9dd2949e 100644 --- a/samples/javascript/04.live-share-react/README.md +++ b/samples/javascript/04.live-share-react/README.md @@ -10,12 +10,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/04* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/05.dice-roller-turbo/README.md b/samples/javascript/05.dice-roller-turbo/README.md index 3ff045688..8defde15b 100644 --- a/samples/javascript/05.dice-roller-turbo/README.md +++ b/samples/javascript/05.dice-roller-turbo/README.md @@ -12,12 +12,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/05* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/21.react-media-template/README.md b/samples/javascript/21.react-media-template/README.md index 36316ff8a..78a30a26d 100644 --- a/samples/javascript/21.react-media-template/README.md +++ b/samples/javascript/21.react-media-template/README.md @@ -13,12 +13,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/21* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/22.react-agile-poker/README.md b/samples/javascript/22.react-agile-poker/README.md index 62888b701..1fb599fb1 100644 --- a/samples/javascript/22.react-agile-poker/README.md +++ b/samples/javascript/22.react-agile-poker/README.md @@ -12,12 +12,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/22* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/javascript/23.react-live-canvas/README.md b/samples/javascript/23.react-live-canvas/README.md index ba3804def..6a6cc12f5 100644 --- a/samples/javascript/23.react-live-canvas/README.md +++ b/samples/javascript/23.react-live-canvas/README.md @@ -17,12 +17,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/j*/22* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/01.dice-roller/README.md b/samples/typescript/01.dice-roller/README.md index 924dd7e41..2327502b5 100644 --- a/samples/typescript/01.dice-roller/README.md +++ b/samples/typescript/01.dice-roller/README.md @@ -13,15 +13,11 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/01* -npm start ``` Running `npm start`, it will do two things: start the `tinylicious` server and start the application using `vite`. If you have never used `tinylicious` before, you should see instead is a prompt saying `Ok to proceed? (y)`, after which you should type `y` and press the "enter" key. In rare cases you might not see the `Ok to proceed? (y)` prompt, in which case try running `npx tinylicious@latest` in your command line directly, and then try again. -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing the app in Teams ### Create a ngrok tunnel to allow Teams to reach your tab app diff --git a/samples/typescript/03.live-canvas-demo/README.md b/samples/typescript/03.live-canvas-demo/README.md index 5ffc39409..5eda28b5a 100644 --- a/samples/typescript/03.live-canvas-demo/README.md +++ b/samples/typescript/03.live-canvas-demo/README.md @@ -8,12 +8,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/03* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/04.live-share-react/README.md b/samples/typescript/04.live-share-react/README.md index 962d8ef1a..844a2727c 100644 --- a/samples/typescript/04.live-share-react/README.md +++ b/samples/typescript/04.live-share-react/README.md @@ -10,12 +10,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/04* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/05.dice-roller-turbo/README.md b/samples/typescript/05.dice-roller-turbo/README.md index 30b3c37b9..9e29fdbe3 100644 --- a/samples/typescript/05.dice-roller-turbo/README.md +++ b/samples/typescript/05.dice-roller-turbo/README.md @@ -12,13 +12,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/05* -npm start ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/06.presence-avatars/README.md b/samples/typescript/06.presence-avatars/README.md index 8095edd2a..469c8feee 100644 --- a/samples/typescript/06.presence-avatars/README.md +++ b/samples/typescript/06.presence-avatars/README.md @@ -10,12 +10,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/06* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/07.countdown-timer/README.md b/samples/typescript/07.countdown-timer/README.md index 9fe086188..3fec790e6 100644 --- a/samples/typescript/07.countdown-timer/README.md +++ b/samples/typescript/07.countdown-timer/README.md @@ -10,12 +10,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/06* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/08.3d-model/README.md b/samples/typescript/08.3d-model/README.md index 80d457c0e..54a04b4d1 100644 --- a/samples/typescript/08.3d-model/README.md +++ b/samples/typescript/08.3d-model/README.md @@ -10,12 +10,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages -cd samples/t*/06* +cd samples/t*/08* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start` diff --git a/samples/typescript/11.react-video-js/.env b/samples/typescript/11.react-video-js/.env new file mode 100644 index 000000000..6f809cc25 --- /dev/null +++ b/samples/typescript/11.react-video-js/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true diff --git a/samples/typescript/11.react-video-js/.eslintignore b/samples/typescript/11.react-video-js/.eslintignore new file mode 100644 index 000000000..ffc6dc6cf --- /dev/null +++ b/samples/typescript/11.react-video-js/.eslintignore @@ -0,0 +1,8 @@ +bin +build +demo-packages +dist +manifest +node_modules +package-lock.json +docs/assets/main.js diff --git a/samples/typescript/11.react-video-js/.eslintrc b/samples/typescript/11.react-video-js/.eslintrc new file mode 100644 index 000000000..c15376521 --- /dev/null +++ b/samples/typescript/11.react-video-js/.eslintrc @@ -0,0 +1,54 @@ +{ + "parser": "@typescript-eslint/parser", + "root": true, + "env": { + "browser": true, + "node": true, + "es2015": true, + "mocha": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime" + ], + "plugins": ["react"], + "parserOptions": { + "ecmaVersion": 2015, + // Allows for the parsing of modern ECMAScript features + "sourceType": "module", // Allows for the use of imports + "ecmaFeatures": { + "jsx": true + } + }, + "rules": { + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "no-async-promise-executor": "off", + "no-constant-condition": "off", + "no-undef": "off", // Disabled due to conflicts with @typescript/eslint + "no-unused-vars": "off", // Disabled due to conflicts with @typescript/eslint + "react/react-in-jsx-scope": "off", + "react/prop-types": "off" + }, + "overrides": [ + { + "files": ["bin/*.js", "lib/*.js"] + } + ], + "ignorePatterns": ["node_modules/*"], + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/samples/typescript/11.react-video-js/.gitignore b/samples/typescript/11.react-video-js/.gitignore new file mode 100644 index 000000000..92ba1c959 --- /dev/null +++ b/samples/typescript/11.react-video-js/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +azcopy.exe diff --git a/samples/typescript/11.react-video-js/README.md b/samples/typescript/11.react-video-js/README.md new file mode 100644 index 000000000..09a00c566 --- /dev/null +++ b/samples/typescript/11.react-video-js/README.md @@ -0,0 +1,89 @@ +# TypeScript: React Video.js Sample + +This repository contains a simple app that enables all connected clients to watch videos together using the `live-share-react` package and Video.js. + +We have found this structure to be very useful in composing advanced applications with Live Share using Functional React components, but you can compose this differently for your app. + +## Getting started + +After cloning the repository, you must first set up the npm workspace from the root of the project. Then, run the following commands from the command line: + +```bash +npm install +cd samples/t*/11* +``` + +## Testing locally in browser + +### `npm run start` + +Running `npm run start`, it will do two things: start the `tinylicious` server and start the application using `vite`. If you have never used `tinylicious` before, you should see instead is a prompt saying `Ok to proceed? (y)`, after which you should type `y` and press the "enter" key. In rare cases you might not see the `Ok to proceed? (y)` prompt, in which case try running `npx tinylicious@latest` in your command line directly, and then try again. + +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes. + +Upon loading, if there is no `/#{id}` in the URL, it will create one and insert it into the URL. + +You can copy this URL and paste it into new browser tabs to test Live Share using a local server. + +_Note:_ if testing with HTTPS, such as when using a tunneling service like Ngrok, instead use the command `npm run start-https`. + +### Known issues when testing in browser + +When not in Teams, we don't have a way to know the user's userId, so we generate a random one. + +That means you might not always start out in control of playback, and need to press "Take control". + +Tab configuration page doesn't do anything in browser. + +## Testing the app in Teams + +There are two options for testing this sample in Teams. The first is to use ngrok to serve and tunnel the app locally, before zipping the app package. + +### Create a ngrok tunnel to allow Teams to reach your tab app + +1. [Download ngrok](https://ngrok.com/download). +2. Launch ngrok with port 3000. + `ngrok http 3000 --host-header=localhost` (You will need an ngrok account to use host-header) +3. In a second terminal, run `npm run start-https` (rather than the traditional `npm run start`) + +### Create the app package to sideload into Teams + +1. Open `.\manifest\manifest.json` and update values in it, including your [Application ID](https://learn.microsoft.com/microsoftteams/platform/resources/schema/manifest-schema#id. +2. You must replace `https://<>` with the https path to your ngrok tunnel. +3. It is recommended that you also update the following fields. + - Set `developer.name` to your name. + - Update `developer.websiteUrl` with your website. + - Update `developer.privacyUrl` with your privacy policy. + - Update `developer.termsOfUseUrl` with your terms of use. +4. Create a zip file with the contents of `.\manifest` directory so that manifest.json, color.png, and outline.png are in the root directory of the zip file. + - On Windows, select all files in `.\manifest` directory and compress them to zip. + - Give your zip file a descriptive name, e.g. `ContosoMediaTemplate`. + +### Test it out + +1. Schedule a meeting for testing from calendar in Teams. +2. Join the meeting. +3. In the meeting window, tap on **+ Apps** and tap on **Manage apps** in the flyout that opens. +4. In the **Manage apps** pane, tap on **Upload a custom app**. + - _Don't see the option to **Upload a custom app?!** Follow [instructions here](https://docs.microsoft.com/en-us/microsoftteams/teams-custom-app-policies-and-settings) to enable custom-apps in your tenant._ +5. Select the zip file you created earlier and upload it. +6. In the dialog that shows up, tap **Add** to add your sample app into the meeting. +7. Now, back in the meeting window, tap **+ Apps** again and type the name of your app in the _Find an app_ textbox. +8. Select the app to activate it in the meeting. +9. In the configuration dialog, just tap **Save** to add your app into the meeting. +10. In the side panel, tap the share icon to put your app on the main stage in the meeting. +11. That's it! You should now see react-media-template on the meeting stage. +12. Your friends/colleagues invited to the meeting should be able to see your app on stage when they join the meeting. + +### Make your own manifest + +To make a new app manifest, you can visit the [Teams Developer Portal](https://dev.teams.microsoft.com/). + +## `npm run build` + +Builds the app for production to the `dist` folder. + +The build is minified and the filenames include the hashes. +Your app is ready to be deployed! diff --git a/samples/typescript/11.react-video-js/index.html b/samples/typescript/11.react-video-js/index.html new file mode 100644 index 000000000..365b24993 --- /dev/null +++ b/samples/typescript/11.react-video-js/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + React Media Template + + + +
+ + + + diff --git a/samples/typescript/11.react-video-js/manifest/color.png b/samples/typescript/11.react-video-js/manifest/color.png new file mode 100644 index 000000000..b8cf81afb Binary files /dev/null and b/samples/typescript/11.react-video-js/manifest/color.png differ diff --git a/samples/typescript/11.react-video-js/manifest/manifest.json b/samples/typescript/11.react-video-js/manifest/manifest.json new file mode 100644 index 000000000..390b1913a --- /dev/null +++ b/samples/typescript/11.react-video-js/manifest/manifest.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", + "version": "1.0.1", + "manifestVersion": "1.16", + "id": "<>", + "packageName": "com.microsoft.teams.livesharereactmediatemplate", + "name": { + "short": "Contoso Media", + "full": "Contoso Media" + }, + "developer": { + "name": "Your name here", + "mpnId": "", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/PrivacyStatement", + "termsOfUseUrl": "https://www.example.com/TermsOfUse" + }, + "description": { + "short": "Watch amazing stock videos with colleagues", + "full": "Have you ever loved stock videos so much that you just had to publish an app to Teams just to watch them with friends? Us to!" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#38a2ff", + "configurableTabs": [ + { + "configurationUrl": "https://<>/config?inTeams=true", + "canUpdateConfiguration": false, + "scopes": [ + "groupchat" + ], + "context": [ + "meetingSidePanel", + "meetingStage" + ] + } + ], + "validDomains": [ + "*.ngrok.io" + ], + "showLoadingIndicator": true, + "authorization": { + "permissions": { + "orgWide": [], + "resourceSpecific": [ + { + "name": "MeetingStage.Write.Chat", + "type": "Delegated" + }, + { + "name": "ChannelMeetingStage.Write.Group", + "type": "Delegated" + }, + { + "name": "LiveShareSession.ReadWrite.Chat", + "type": "Delegated" + }, + { + "name": "LiveShareSession.ReadWrite.Group", + "type": "Delegated" + }, + { + "name": "OnlineMeetingIncomingAudio.Detect.Chat", + "type": "Delegated" + }, + { + "name": "OnlineMeetingIncomingAudio.Detect.Group", + "type": "Delegated" + } + ] + } + } +} \ No newline at end of file diff --git a/samples/typescript/11.react-video-js/manifest/outline.png b/samples/typescript/11.react-video-js/manifest/outline.png new file mode 100644 index 000000000..77f195a52 Binary files /dev/null and b/samples/typescript/11.react-video-js/manifest/outline.png differ diff --git a/samples/typescript/11.react-video-js/package.json b/samples/typescript/11.react-video-js/package.json new file mode 100644 index 000000000..26c239071 --- /dev/null +++ b/samples/typescript/11.react-video-js/package.json @@ -0,0 +1,71 @@ +{ + "name": "react-video-js", + "version": "0.4.0", + "private": true, + "author": "Microsoft", + "license": "MIT", + "dependencies": { + "@fluentui/react-components": "^9.18.6", + "@fluentui/react-icons": "^2.0.202", + "@fluentui/react-theme": "^9.1.5", + "@microsoft/live-share": "2.0.0-internal.8", + "@microsoft/live-share-canvas": "2.0.0-internal.8", + "@microsoft/live-share-media": "2.0.0-internal.8", + "@microsoft/teams-js": "^2.16.0", + "fluid-framework": "^2.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.4.2", + "react-router-dom": "^6.4.2", + "use-resize-observer": "^9.1.0", + "uuid": "^9.0.0", + "web-vitals": "^3.1.1", + "video.js": "^8.17.4" + }, + "devDependencies": { + "@fluidframework/test-runtime-utils": "^2.0.0", + "@types/lodash": "^4.14.191", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^14.4.3", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", + "@vitejs/plugin-react": "^3.0.1", + "dotenv-cli": "^7.2.1", + "esbuild": "^0.17.18", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.4", + "eslint-plugin-react-hooks": "^4.6.2", + "start-server-and-test": "^2.0.0", + "vite": "^4.0.4" + }, + "scripts": { + "build": "tsc && vite build", + "clean": "npx shx rm -rf build", + "doctor": "eslint src/**/*.{j,t}s{,x} --fix --no-error-on-unmatched-pattern", + "preview": "vite preview", + "start-https": "start-server-and-test start:server 7070 start:https", + "start:client": "vite", + "start:https": "vite --config vite.https-config.ts", + "start:server": "npx tinylicious@latest", + "start": "start-server-and-test start:server 7070 start:client" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/samples/typescript/11.react-video-js/public/favicon.ico b/samples/typescript/11.react-video-js/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/samples/typescript/11.react-video-js/public/favicon.ico differ diff --git a/samples/typescript/11.react-video-js/public/logo192.png b/samples/typescript/11.react-video-js/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/samples/typescript/11.react-video-js/public/logo192.png differ diff --git a/samples/typescript/11.react-video-js/public/logo512.png b/samples/typescript/11.react-video-js/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/samples/typescript/11.react-video-js/public/logo512.png differ diff --git a/samples/typescript/11.react-video-js/public/manifest.json b/samples/typescript/11.react-video-js/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/samples/typescript/11.react-video-js/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/samples/typescript/11.react-video-js/public/robots.txt b/samples/typescript/11.react-video-js/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/samples/typescript/11.react-video-js/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/samples/typescript/11.react-video-js/src/App.tsx b/samples/typescript/11.react-video-js/src/App.tsx new file mode 100644 index 000000000..303a76392 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/App.tsx @@ -0,0 +1,102 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + FluentProvider, + teamsDarkTheme, + teamsHighContrastTheme, + teamsLightTheme, +} from "@fluentui/react-components"; +import * as microsoftTeams from "@microsoft/teams-js"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { useEffect, useRef, useState } from "react"; +import MeetingStage from "./pages/MeetingStage"; +import TabConfig from "./pages/TabConfig"; +import { inTeams } from "./utils/inTeams"; + +export const App = () => { + const startedInitializingRef = useRef(false); + const [initialized, setInitialized] = useState(false); + const [teamsTheme, setTeamsTheme] = useState(teamsDarkTheme); + + useEffect(() => { + // This hook should only be called once, so we use a ref to track if it has been called. + // This is a workaround for the fact that useEffect is called twice on initial render in React V18. + // In production, you might consider using React Suspense if you are using React V18. + // We are not doing this here because many customers are still using React V17. + // We are monitoring the React Suspense situation closely and may revisit in the future. + if (startedInitializingRef.current) return; + startedInitializingRef.current = true; + const initialize = async () => { + try { + console.log("App.tsx: initializing client SDK initialized"); + await microsoftTeams.app.initialize(); + microsoftTeams.app.notifyAppLoaded(); + microsoftTeams.app.notifySuccess(); + const context = await microsoftTeams.app.getContext(); + const curTheme = context.app.theme; + switch (curTheme) { + case "dark": + setTeamsTheme(teamsDarkTheme); + break; + case "contrast": + setTeamsTheme(teamsHighContrastTheme); + break; + case "default": + default: + setTeamsTheme(teamsLightTheme); + break; + } + microsoftTeams.app.registerOnThemeChangeHandler( + (theme: string | undefined) => { + if (theme == "dark") { + setTeamsTheme(teamsDarkTheme); + } else if (theme == "contrast") { + setTeamsTheme(teamsHighContrastTheme); + } else { + setTeamsTheme(teamsLightTheme); + } + } + ); + setInitialized(true); + } catch (error) { + console.error(error); + } + }; + + if (inTeams()) { + console.log("App.tsx: initializing client SDK"); + initialize(); + } + }); + + const appReady = (inTeams() && initialized) || !inTeams(); + + if (appReady) { + return ( + + + + } /> + } /> + + + + ); + } + return null; +}; diff --git a/samples/typescript/11.react-video-js/src/assets/eraser.svg b/samples/typescript/11.react-video-js/src/assets/eraser.svg new file mode 100644 index 000000000..6e689f11e --- /dev/null +++ b/samples/typescript/11.react-video-js/src/assets/eraser.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/typescript/11.react-video-js/src/assets/highlighter.svg b/samples/typescript/11.react-video-js/src/assets/highlighter.svg new file mode 100644 index 000000000..bbe5d93fe --- /dev/null +++ b/samples/typescript/11.react-video-js/src/assets/highlighter.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/typescript/11.react-video-js/src/assets/laser-pointer.svg b/samples/typescript/11.react-video-js/src/assets/laser-pointer.svg new file mode 100644 index 000000000..7d3d858b6 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/assets/laser-pointer.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/typescript/11.react-video-js/src/assets/pen.svg b/samples/typescript/11.react-video-js/src/assets/pen.svg new file mode 100644 index 000000000..8e3827251 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/assets/pen.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/typescript/11.react-video-js/src/components/InkCanvas.tsx b/samples/typescript/11.react-video-js/src/components/InkCanvas.tsx new file mode 100644 index 000000000..b892afa4a --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/InkCanvas.tsx @@ -0,0 +1,62 @@ +import { FC, MutableRefObject, useEffect } from "react"; +import { VideoSize } from "../utils/useVisibleVideoSize"; +import { useEventListener } from "../utils/useEventListener"; +import { InkingManager } from "@microsoft/live-share-canvas"; + +const REFERENCE_HEIGHT = 1080; + +interface IInkCanvasProps { + isEnabled: boolean; + inkingManager?: InkingManager; + canvasRef: MutableRefObject; + videoSize: VideoSize | undefined; +} + +export const InkCanvas: FC = ({ + isEnabled, + inkingManager, + canvasRef, + videoSize, +}) => { + const onMouseEvent = (event: Event) => { + if (isEnabled) { + event.preventDefault(); + } + }; + + useEffect(() => { + if (videoSize && canvasRef.current) { + canvasRef.current.style.width = `${videoSize.width}px`; + canvasRef.current.style.height = `${videoSize.height}px`; + if (inkingManager) { + // Update the scale of inkingManager so that the annotations appear + // in the same positions for all users + const scale = videoSize.height / REFERENCE_HEIGHT; + inkingManager.scale = scale; + } + } + }, [videoSize, inkingManager, canvasRef]); + + useEventListener("mousedown", onMouseEvent, canvasRef.current ?? undefined); + useEventListener("mouseup", onMouseEvent, canvasRef.current ?? undefined); + useEventListener("mousemove", onMouseEvent, canvasRef.current ?? undefined); + + return ( + <> +
+ + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/InkingControlButton.tsx b/samples/typescript/11.react-video-js/src/components/InkingControlButton.tsx new file mode 100644 index 000000000..0fed2bc3c --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/InkingControlButton.tsx @@ -0,0 +1,30 @@ +import { Image, Button } from "@fluentui/react-components"; +import { InkingTool } from "@microsoft/live-share-canvas"; +import React, { ReactNode } from "react"; +import { FC } from "react"; + +export const InkingControlButton: FC<{ + tool: InkingTool; + selectedTool: InkingTool; + isEnabled: boolean; + onSelectTool: (tool: InkingTool) => void; + children: ReactNode; +}> = ({ tool, selectedTool, isEnabled, onSelectTool, children }) => ( + // TODO: change back to button +
{ + onSelectTool(tool); + }} + > + {children} +
+); diff --git a/samples/typescript/11.react-video-js/src/components/InkingControls.tsx b/samples/typescript/11.react-video-js/src/components/InkingControls.tsx new file mode 100644 index 000000000..322f174bb --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/InkingControls.tsx @@ -0,0 +1,107 @@ +import { + useState, + useCallback, + useEffect, + FC, + SetStateAction, + Dispatch, +} from "react"; +import { + InkingTool, + fromCssColor, + InkingManager, + LiveCanvas, +} from "@microsoft/live-share-canvas"; +import { Image } from "@fluentui/react-components"; +import { FlexRow } from "./flex"; +import { InkingControlButton } from "./InkingControlButton"; +// @ts-ignore +import LaserPointerIcon from "../assets/laser-pointer.svg"; +// @ts-ignore +import PenIcon from "../assets/pen.svg"; +// @ts-ignore +import HighlighterIcon from "../assets/highlighter.svg"; +// @ts-ignore +import EraserIcon from "../assets/eraser.svg"; + +interface InkingControlsProps { + liveCanvas: LiveCanvas; + inkingManager: InkingManager; + setIsEnabled: Dispatch>; + isEnabled: boolean; +} + +export const InkingControls: FC = ({ + liveCanvas, + inkingManager, + setIsEnabled, + isEnabled, +}) => { + const [selectedTool, setSelectedTool] = useState(inkingManager.tool); + const onSelectTool = useCallback( + (tool: InkingTool) => { + if (tool !== selectedTool) { + inkingManager.tool = tool; + setSelectedTool(tool); + } + if (isEnabled && tool === selectedTool) { + setIsEnabled(false); + } else { + setIsEnabled(true); + } + }, + [inkingManager, isEnabled, selectedTool, setIsEnabled] + ); + + useEffect(() => { + if (!inkingManager) return; + // Change default color of pen brush + inkingManager.penBrush.color = fromCssColor("#E3182D"); + }, [inkingManager]); + + useEffect(() => { + if (!liveCanvas) return; + liveCanvas.isCursorShared = true; + }, [liveCanvas]); + + return ( + + {/* TODO: (Corina) fix marginSpacer usage to gap="small" */} + + + + + + + + + + + + + + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/ListWrapper.tsx b/samples/typescript/11.react-video-js/src/components/ListWrapper.tsx new file mode 100644 index 000000000..0339a384b --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/ListWrapper.tsx @@ -0,0 +1,19 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { FC, ReactNode } from "react"; +import { FlexColumn, FlexItem } from "./flex"; + +export const ListWrapper: FC<{ children: ReactNode }> = ({ children }) => { + return ( + + + + {children} + + + + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/LiveNotifications.tsx b/samples/typescript/11.react-video-js/src/components/LiveNotifications.tsx new file mode 100644 index 000000000..63019b9a5 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/LiveNotifications.tsx @@ -0,0 +1,97 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { useEffect, useState, useRef, FC } from "react"; +import { mergeClasses } from "@fluentui/react-components"; +import { useLiveEvent } from "@microsoft/live-share-react"; +import { getLiveNotificationStyles, getPillStyles } from "../styles/styles"; +import { FlexColumn } from "./flex"; +import { UNIQUE_KEYS } from "../constants"; +import { LivePresence } from "@microsoft/live-share"; + +interface Notification { + id: string; + text: string; +} + +interface ILiveNotificationsProps { + notificationToDisplay: string | undefined; +} + +export const LiveNotifications: FC = ({ + notificationToDisplay, +}) => { + const notificationsRef = useRef([]); + const [notifications, setNotifications] = useState([]); + useEffect(() => { + if (notificationToDisplay) { + // Display the notification + const updatedNotifications: Notification[] = [ + ...notificationsRef.current, + ]; + const notificationId = `notification${Math.abs( + Math.random() * 999999999 + )}`; + updatedNotifications.push({ + id: notificationId, + text: notificationToDisplay, + }); + notificationsRef.current = updatedNotifications; + setNotifications(notificationsRef.current); + + // Remove the notification after a 1s delay + setTimeout(() => { + const resetNotifications = [...notificationsRef.current]; + const matchIndex = resetNotifications.findIndex( + (notification) => notification.id === notificationId + ); + if (matchIndex >= 0) { + resetNotifications.splice(matchIndex, 1); + notificationsRef.current = resetNotifications; + setNotifications(notificationsRef.current); + } + }, 1500); + } + }, [notificationToDisplay, setNotifications]); + + const pillStyles = getPillStyles(); + const liveNotificationStyles = getLiveNotificationStyles(); + + return ( + + {notifications.map((notification) => { + return ( +
+ {notification.text} +
+ ); + })} +
+ ); +}; + +/** + * Hook for sending notifications to display across clients + */ +const useNotifications = (livePresence: LivePresence) => { + const { latestEvent, sendEvent } = useLiveEvent( + UNIQUE_KEYS.notifications + ); + + return { + notificationToDisplay: latestEvent + ? `${livePresence.getUserForClient(latestEvent.clientId)} ${ + latestEvent.value + }` + : undefined, + sendNotification: sendEvent, + }; +}; diff --git a/samples/typescript/11.react-video-js/src/components/LiveSharePage.tsx b/samples/typescript/11.react-video-js/src/components/LiveSharePage.tsx new file mode 100644 index 000000000..4522cc257 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/LiveSharePage.tsx @@ -0,0 +1,57 @@ +import { Text } from "@fluentui/react-components"; +import { Spinner } from "@fluentui/react-components"; +import { app } from "@microsoft/teams-js"; +import { FC, ReactNode } from "react"; +import { FlexColumn } from "./flex"; +import { inTeams } from "../utils/inTeams"; +import { useLiveShareContext } from "@microsoft/live-share-react"; +import { PageError } from "./PageError"; + +export const LiveSharePage: FC<{ + children: ReactNode; + context: app.Context | undefined; +}> = ({ children, context }) => { + const { joined, joinError } = useLiveShareContext(); + let loadText: string | undefined; + if (!context) { + loadText = "Loading Teams Client SDK..."; + } else if (!joined) { + loadText = "Joining Live Share session..."; + } + + return ( + <> + {!!joinError && } + {!joinError && !!loadText && ( + + + + {loadText} + + + )} + {!!context && ( +
+ {children} +
+ )} + + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/MediaCard.tsx b/samples/typescript/11.react-video-js/src/components/MediaCard.tsx new file mode 100644 index 000000000..9c86d316b --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/MediaCard.tsx @@ -0,0 +1,117 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Card, CardPreview, CardFooter } from "@fluentui/react-components"; +import { Image, Text, Button } from "@fluentui/react-components"; +import { Delete20Regular } from "@fluentui/react-icons"; +import { FC } from "react"; +import { MediaItem } from "../utils/media-list"; +import { FlexItem, FlexRow } from "./flex"; + +export const MediaCard: FC<{ + mediaItem: MediaItem; + nowPlayingId?: string; + sharingActive: boolean; + buttonText: string; + selectMedia: (mediaItem: MediaItem) => void; + removeMediaItem: (id: string) => void; +}> = ({ + mediaItem, + nowPlayingId, + sharingActive, + buttonText, + selectMedia, + removeMediaItem, +}) => { + return ( + + + + + +
+ + {mediaItem.title} + +
+ + + + + + {!!removeMediaItem && ( + + )} + + {/* Take Control */} + {!suspended && ( + + )} + + {/* Ink Toggle */} + {localUserIsPresenting && inkingManager && liveCanvas && ( + <> + {/* Divider */} +
+ + + )} + + + + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/PlayerProgressBar.tsx b/samples/typescript/11.react-video-js/src/components/PlayerProgressBar.tsx new file mode 100644 index 000000000..958994f99 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/PlayerProgressBar.tsx @@ -0,0 +1,208 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + useEffect, + useState, + useCallback, + useRef, + FC, + SyntheticEvent, + ReactEventHandler, + MutableRefObject, +} from "react"; +import { PositioningImperativeRef, Slider } from "@fluentui/react-components"; +import { Tooltip } from "@fluentui/react-components"; +import { getProgressBarStyles } from "../styles/styles"; +import { debounce } from "lodash"; +import useResizeObserver from "use-resize-observer"; +import { getFlexItemStyles } from "../styles/layouts"; +import { formatTimeValue } from "../utils/format"; + +const PlayerProgressBar: FC<{ + currentTime: number; + duration: number; + isPlaybackDisabled: boolean; + onSeek: (time: number) => void; +}> = ({ currentTime, duration, isPlaybackDisabled, onSeek }) => { + const toolTipPositioningRef = useRef(); + const sliderRef = useRef(); + const [dimension, setDimensions] = useState(); + const [toolTipContent, setToolTipContent] = useState("0:00"); + + const { ref: resizeRef, width = 1, height = 1 } = useResizeObserver(); + + const [localCurrentTime, setLocalCurrentTime] = useState(0.0); + const [isSeeking, setIsSeeking] = useState(false); + + const styles = getProgressBarStyles(); + const flexItemStyles = getFlexItemStyles(); + + const onDidSeek = useCallback(() => { + onSeek(localCurrentTime); + setTimeout(() => { + setIsSeeking(false); + }, 500); + }, [onSeek, setIsSeeking, localCurrentTime]); + + // eslint-disable-next-line + const debouncedSeek = useCallback(debounce(onDidSeek, 200), [ + onDidSeek, + localCurrentTime, + ]); + + useEffect(() => { + if (!isSeeking && currentTime !== localCurrentTime) { + setLocalCurrentTime(currentTime); + } + }, [currentTime, localCurrentTime, isSeeking]); + + useEffect(() => { + if (sliderRef.current && width > 1 && height > 1) { + setDimensions(sliderRef.current.getBoundingClientRect()); + } + }, [sliderRef, duration, width, height]); + + const onMouseMove: ReactEventHandler = ( + e: SyntheticEvent + ) => { + if (dimension) { + const xPosition = Math.min( + Math.max(e.nativeEvent.clientX, dimension.left), + dimension.right - 4 + ); + const distanceFromOrigin = xPosition - dimension.left; + const mousePos = (distanceFromOrigin / dimension.width) * 100; + const hoverTime = Math.max( + 0, + Math.min(Math.round(duration * (mousePos / 100)), duration) + ); + const scrollOffSet = 0; + + setToolTipContent(formatTimeValue(hoverTime)); + + toolTipPositioningRef.current?.setTarget({ + getBoundingClientRect: getRect( + xPosition, + dimension.top - scrollOffSet + ), + }); + } + }; + + const onTouchMove: ReactEventHandler = ( + e: SyntheticEvent + ) => { + if (dimension) { + const xPosition = Math.min( + Math.max(e.nativeEvent.touches[0].clientX, dimension.left), + dimension.right - 4 + ); + const distanceFromOrigin = xPosition - dimension.left; + const mousePos = (distanceFromOrigin / dimension.width) * 100; + const hoverTime = Math.max( + 0, + Math.min(Math.round(duration * (mousePos / 100)), duration) + ); + const scrollOffSet = 0; + + setToolTipContent(formatTimeValue(hoverTime)); + + toolTipPositioningRef.current?.setTarget({ + getBoundingClientRect: getRect( + xPosition, + dimension.top - scrollOffSet + ), + }); + } + }; + + const durationToDivideBy = duration === 0 ? 100 : duration; + const bufferLoadedPercent = 0; + + return ( +
+
+ , + }} + content={toolTipContent} + relationship="label" + > + } + min={0} + max={durationToDivideBy} + value={localCurrentTime} + disabled={isPlaybackDisabled} + style={ + { + "--oneplayer-play-progress-percent": `${ + (localCurrentTime / durationToDivideBy) * + 100 + }%`, + "--oneplayer-buff-progress-percent": `${ + ((duration * bufferLoadedPercent) / + durationToDivideBy) * + 100 + }%`, + "--fui-slider-thumb-size": "1rem", + } as any + } + input={{ + className: styles.input, + "aria-valuemin": 0, + "aria-valuemax": duration, + "aria-label": "progress bar", + "aria-live": "polite", + role: "slider", + }} + rail={{ + className: styles.rail, + }} + className={styles.root} + thumb={{ className: styles.thumb }} + onChange={(ev, data) => { + setIsSeeking(true); + setLocalCurrentTime(data.value); + }} + onMouseMove={onMouseMove} + onMouseDown={() => { + setIsSeeking(true); + }} + onTouchStart={(e) => { + setIsSeeking(true); + }} + onTouchMove={onTouchMove} + onTouchEnd={(e) => { + debouncedSeek(); + }} + onMouseUp={() => { + debouncedSeek(); + }} + /> + +
+
+ ); +}; + +const getRect = (x = 0, y = 0) => { + return () => ({ + x: x, + y: y, + width: 0, + height: 0, + top: y, + right: x, + bottom: y, + left: x, + }); +}; + +export default PlayerProgressBar; diff --git a/samples/typescript/11.react-video-js/src/components/TabbedList.tsx b/samples/typescript/11.react-video-js/src/components/TabbedList.tsx new file mode 100644 index 000000000..3f3d7da48 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/TabbedList.tsx @@ -0,0 +1,105 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + mergeClasses, + TabList, + Tab, + SelectTabEventHandler, + SelectTabEvent, + SelectTabData, +} from "@fluentui/react-components"; +import { getFlexItemStyles, getFlexRowStyles } from "../styles/layouts"; +import { MediaCard } from "./MediaCard"; +import { FC, useMemo, useState } from "react"; +import { MediaItem } from "../utils/media-list"; +import { FlexItem, FlexRow } from "./flex"; + +export const TabbedList: FC<{ + mediaItems: MediaItem[]; + browseItems: MediaItem[]; + sharingActive: boolean; + nowPlayingId?: string; + addMediaItem: (id: string) => void; + removeMediaItem: (id: string) => void; + selectMedia: (mediaItem: MediaItem) => void; +}> = ({ + mediaItems, + browseItems, + sharingActive, + nowPlayingId, + addMediaItem, + removeMediaItem, + selectMedia, +}) => { + const [selectedValue, setSelectedValue] = useState("tab1"); + + const onTabSelect: SelectTabEventHandler = ( + event: SelectTabEvent, + data: SelectTabData + ) => { + setSelectedValue(data.value as string); + }; + + const filteredBrowseItems = useMemo(() => { + return browseItems.filter( + (browseItem) => + !mediaItems.find((mediaItem) => browseItem.id === mediaItem.id) + ); + }, [browseItems, mediaItems]); + + const flexRowStyles = getFlexRowStyles(); + const flexItemStyles = getFlexItemStyles(); + return ( + <> + + + + Playlist + Browse + + + + {selectedValue === "tab1" && + mediaItems.map((mediaItem) => ( + + ))} + {selectedValue === "tab2" && + filteredBrowseItems.map((mediaItem) => ( + { + addMediaItem(item.id); + setSelectedValue("tab1"); + }} + removeMediaItem={removeMediaItem} + /> + ))} + + ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/flex/FlexColumn.tsx b/samples/typescript/11.react-video-js/src/components/flex/FlexColumn.tsx new file mode 100644 index 000000000..7e1f69d74 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/flex/FlexColumn.tsx @@ -0,0 +1,73 @@ +import { forwardRef } from "react"; +import { mergeClasses } from "@fluentui/react-components"; +import { FlexOptions, getFlexColumnStyles } from "./flex-styles"; + +export interface IFlexColumnOptions extends FlexOptions { + /** Unique property for styles for Side Panel or Tab content */ + isSidePanel?: boolean; +} + +export const FlexColumn = forwardRef( + (props, ref) => { + const { + children, + className, + fill, + gap, + hAlign, + name, + role, + scroll, + spaceBetween, + style, + transparent, + vAlign, + onClick, + } = props; + const flexColumnStyles = getFlexColumnStyles(); + + const isHidden = role === "presentation"; + + const mergedClasses = mergeClasses( + flexColumnStyles.root, + fill === "both" && flexColumnStyles.fill, + fill === "height" && flexColumnStyles.fillH, + fill === "view" && flexColumnStyles.fillV, + fill === "view-height" && flexColumnStyles.fillVH, + fill === "width" && flexColumnStyles.fillW, + gap && flexColumnStyles.gapReset, + gap === "smaller" && flexColumnStyles.gapSmaller, + gap === "small" && flexColumnStyles.gapSmall, + gap === "medium" && flexColumnStyles.gapMedium, + gap === "large" && flexColumnStyles.gapLarge, + hAlign === "center" && flexColumnStyles.hAlignCenter, + hAlign === "end" && flexColumnStyles.hAlignEnd, + hAlign === "start" && flexColumnStyles.hAlignStart, + isHidden && flexColumnStyles.defaultCursor, + isHidden && flexColumnStyles.pointerEvents, + scroll && flexColumnStyles.scroll, + spaceBetween && flexColumnStyles.spaceBetween, + transparent && flexColumnStyles.transparent, + vAlign === "center" && flexColumnStyles.vAlignCenter, + vAlign === "end" && flexColumnStyles.vAlignEnd, + vAlign === "start" && flexColumnStyles.vAlignStart, + className && className + ); + + return ( +
+ {children} +
+ ); + } +); +FlexColumn.displayName = "FlexColumn"; diff --git a/samples/typescript/11.react-video-js/src/components/flex/FlexItem.tsx b/samples/typescript/11.react-video-js/src/components/flex/FlexItem.tsx new file mode 100644 index 000000000..e1c5be301 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/flex/FlexItem.tsx @@ -0,0 +1,26 @@ +import { FC, ReactNode } from "react"; +import { mergeClasses } from "@fluentui/react-components"; +import { getFlexItemStyles } from "./flex-styles"; + +export type FlexItemOptions = { + children: ReactNode; + className?: string; + grow?: boolean | number; + noShrink?: boolean; + style?: any; +}; +export const FlexItem: FC = (props) => { + const { className, children, grow, noShrink, style } = props; + const flexItemStyles = getFlexItemStyles(); + const mergedClasses = mergeClasses( + grow ? flexItemStyles.grow : "", + noShrink ? flexItemStyles.noShrink : "", + className ?? "" + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/samples/typescript/11.react-video-js/src/components/flex/FlexRow.tsx b/samples/typescript/11.react-video-js/src/components/flex/FlexRow.tsx new file mode 100644 index 000000000..d683d0998 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/flex/FlexRow.tsx @@ -0,0 +1,75 @@ +import { FC, forwardRef } from "react"; +import { mergeClasses } from "@fluentui/react-components"; +import { FlexOptions, getFlexRowStyles } from "./flex-styles"; + +export interface IFlexRowOptions extends FlexOptions { + wrap?: boolean; + columnOnSmallScreen?: boolean; +} + +export const FlexRow = forwardRef( + (props, ref) => { + const { + children, + // Merged classes from parent + className, + columnOnSmallScreen, + fill, + gap, + hAlign, + name, + role, + scroll, + spaceBetween, + style, + transparent, + vAlign, + wrap, + } = props; + + const flexRowStyles = getFlexRowStyles(); + + const isHidden = role === "presentation"; + + const mergedClasses = mergeClasses( + flexRowStyles.root, + fill === "both" && flexRowStyles.fill, + fill === "height" && flexRowStyles.fillH, + fill === "view" && flexRowStyles.fillV, + fill === "view-height" && flexRowStyles.fillVH, + fill === "width" && flexRowStyles.fillW, + gap === "smaller" && flexRowStyles.gapSmaller, + gap === "small" && flexRowStyles.gapSmall, + gap === "medium" && flexRowStyles.gapMedium, + gap === "large" && flexRowStyles.gapLarge, + hAlign === "center" && flexRowStyles.hAlignCenter, + hAlign === "end" && flexRowStyles.hAlignEnd, + hAlign === "start" && flexRowStyles.hAlignStart, + isHidden && flexRowStyles.defaultCursor, + isHidden && flexRowStyles.pointerEvents, + scroll && flexRowStyles.scroll, + spaceBetween && flexRowStyles.spaceBetween, + transparent && flexRowStyles.transparent, + vAlign === "center" && flexRowStyles.vAlignCenter, + vAlign === "end" && flexRowStyles.vAlignEnd, + vAlign === "start" && flexRowStyles.vAlignStart, + wrap && flexRowStyles.wrap, + columnOnSmallScreen && flexRowStyles.columnOnSmallScreen, + className && className + ); + return ( +
+ {children} +
+ ); + } +); +FlexRow.displayName = "FlexRow"; diff --git a/samples/typescript/11.react-video-js/src/components/flex/flex-styles.ts b/samples/typescript/11.react-video-js/src/components/flex/flex-styles.ts new file mode 100644 index 000000000..273f9cfab --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/flex/flex-styles.ts @@ -0,0 +1,340 @@ +import { MouseEventHandler, ReactNode } from "react"; +import { makeStyles, tokens } from "@fluentui/react-components"; + +/** Send Flex style options via FlexColumn or FlexRow props */ +export type FlexOptions = { + children?: ReactNode; + className?: string; + fill?: "both" | "height" | "width" | "view" | "view-height"; + gap?: "smaller" | "small" | "medium" | "large"; + hAlign?: "start" | "center" | "end"; + inline?: boolean; + name?: string; + role?: string; + spaceBetween?: boolean; + style?: any; + transparent?: boolean; // refactor for other background colors + vAlign?: "start" | "center" | "end"; + scroll?: boolean; + onClick?: MouseEventHandler; +}; + +export const getFlexRowStyles = makeStyles({ + root: { + display: "flex", + height: "auto", + /** Fix for flex containers: + * minHeight/Width ensures padding is respected when + * computing height wrt child components */ + minHeight: 0, + minWidth: 0, + }, + defaultCursor: { + cursor: "default", + }, + fill: { + width: "100%", + height: "100%", + }, + fillH: { + height: "100%", + }, + // fill view + fillV: { + height: "100vh", + width: "100vw", + overflowY: "hidden", + "@supports(height: 100svh)": { + height: "100svh", + }, + }, + // fill view (height only) + fillVH: { + height: "100vh", + overflowY: "hidden", + "@supports(height: 100svh)": { + height: "100svh", + }, + }, + fillW: { + width: "100%", + }, + gapSmaller: { + "> :not(:last-child)": { + marginRight: "0.5rem", + }, + }, + gapSmall: { + "> :not(:last-child)": { + marginRight: "1rem", + }, + }, + gapMedium: { + "> :not(:last-child)": { + marginRight: "1.5rem", + }, + }, + gapLarge: { + "> :not(:last-child)": { + marginRight: "3rem", + }, + }, + scroll: { + overflowX: "auto", + overflowY: "hidden", + msOverflowStyle: "auto", + "::-webkit-scrollbar": { + height: "16px", + }, + "::-webkit-scrollbar-track": { + borderRadiusStartStart: "8px", + borderRadiusStartEnd: "8px", + borderRadiusEndStart: "8px", + borderRadiusEndEnd: "8px", + backgroundColor: tokens.colorNeutralBackground3, + borderLeftWidth: "1px", + borderLeftStyle: "solid", + borderLeftColor: tokens.colorNeutralBackground3, + borderRightWidth: "1px", + borderRightStyle: "solid", + borderRightColor: tokens.colorNeutralBackground3, + borderTopWidth: "1px", + borderTopStyle: "solid", + borderTopColor: tokens.colorNeutralBackground3, + borderBottomWidth: "1px", + borderBottomStyle: "solid", + borderBottomColor: tokens.colorNeutralBackground3, + }, + "::-webkit-scrollbar-thumb": { + borderBottomLeftRadius: "8px", + borderBottomRightRadius: "8px", + borderTopLeftRadius: "8px", + borderTopRightRadius: "8px", + backgroundColor: tokens.colorNeutralForeground3, + borderLeftWidth: "4px", + borderLeftStyle: "solid", + borderLeftColor: tokens.colorNeutralBackground3, + borderRightWidth: "4px", + borderRightStyle: "solid", + borderRightColor: tokens.colorNeutralBackground3, + borderTopWidth: "4px", + borderTopStyle: "solid", + borderTopColor: tokens.colorNeutralBackground3, + borderBottomWidth: "4px", + borderBottomStyle: "solid", + borderBottomColor: tokens.colorNeutralBackground3, + ":hover": { + backgroundColor: tokens.colorNeutralForeground3Hover, + }, + ":focus": { + backgroundColor: tokens.colorNeutralForeground3Pressed, + }, + }, + }, + spaceBetween: { + justifyContent: "space-between", + }, + hAlignStart: { + justifyContent: "start", + }, + hAlignCenter: { + justifyContent: "center", + }, + hAlignEnd: { + justifyContent: "end", + }, + inline: { + display: "inline-flex", + }, + pointerEvents: { + pointerEvents: "none", + }, + transparent: { + backgroundColor: "transparent", + }, + vAlignStart: { + alignItems: "start", + }, + vAlignCenter: { + alignItems: "center", + }, + vAlignEnd: { + alignItems: "end", + }, + wrap: { + flexWrap: "wrap", + }, + columnOnSmallScreen: { + "@media only screen and (max-width: 720px)": { + flexDirection: "column", + height: "auto", + /** Fix for flex containers: + * minHeight/Width ensures padding is respected when + * computing height wrt child components */ + minHeight: 0, + minWidth: 0, + }, + }, +}); + +export const getFlexColumnStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + height: "auto", + /** Fix for flex containers: + * minHeight/Width ensures padding is respected when + * computing height wrt child components */ + minHeight: 0, + minWidth: 0, + }, + defaultCursor: { + cursor: "default", + }, + fill: { + width: "100%", + height: "100%", + }, + fillH: { + height: "100%", + }, + // fill view + fillV: { + width: "100vw", + height: "100vh", + overflowY: "hidden", + "@supports(height: 100svh)": { + height: "100svh", + }, + }, + // fill view (height only) + fillVH: { + height: "100vh", + overflowY: "hidden", + "@supports(height: 100svh)": { + height: "100svh", + }, + }, + fillW: { + width: "100%", + }, + // Needed to reset top margin when using

within a + gapReset: { + "> :not(:last-child)": { + marginTop: "0rem", + }, + }, + gapSmaller: { + "> :not(:last-child)": { + marginBottom: "0.5rem", + }, + }, + gapSmall: { + "> :not(:last-child)": { + marginBottom: "1rem", + }, + }, + gapMedium: { + "> :not(:last-child)": { + marginBottom: "1.5rem", + }, + }, + gapLarge: { + "> :not(:last-child)": { + marginBottom: "3rem", + }, + }, + spaceBetween: { + justifyContent: "space-between", + }, + hAlignStart: { + alignItems: "start", + }, + hAlignCenter: { + alignItems: "center", + }, + hAlignEnd: { + alignItems: "end", + }, + inline: { + display: "inline-flex", + }, + isSidePanel: { + maxWidth: "24rem", + }, + pointerEvents: { + pointerEvents: "none", + }, + scroll: { + overflowY: "auto", + msOverflowStyle: "auto", + maxHeight: "100vh", + "::-webkit-scrollbar": { + width: "16px", + }, + "::-webkit-scrollbar-track": { + borderRadiusStartStart: "8px", + borderRadiusStartEnd: "8px", + borderRadiusEndStart: "8px", + borderRadiusEndEnd: "8px", + backgroundColor: tokens.colorNeutralBackground3, + borderLeftWidth: "1px", + borderLeftStyle: "solid", + borderLeftColor: tokens.colorNeutralBackground3, + borderRightWidth: "1px", + borderRightStyle: "solid", + borderRightColor: tokens.colorNeutralBackground3, + borderTopWidth: "1px", + borderTopStyle: "solid", + borderTopColor: tokens.colorNeutralBackground3, + borderBottomWidth: "1px", + borderBottomStyle: "solid", + borderBottomColor: tokens.colorNeutralBackground3, + }, + "::-webkit-scrollbar-thumb": { + borderBottomLeftRadius: "8px", + borderBottomRightRadius: "8px", + borderTopLeftRadius: "8px", + borderTopRightRadius: "8px", + backgroundColor: tokens.colorNeutralForeground3, + borderLeftWidth: "4px", + borderLeftStyle: "solid", + borderLeftColor: tokens.colorNeutralBackground3, + borderRightWidth: "4px", + borderRightStyle: "solid", + borderRightColor: tokens.colorNeutralBackground3, + borderTopWidth: "4px", + borderTopStyle: "solid", + borderTopColor: tokens.colorNeutralBackground3, + borderBottomWidth: "4px", + borderBottomStyle: "solid", + borderBottomColor: tokens.colorNeutralBackground3, + ":hover": { + backgroundColor: tokens.colorNeutralForeground3Hover, + }, + ":focus": { + backgroundColor: tokens.colorNeutralForeground3Pressed, + }, + }, + }, + transparent: { + backgroundColor: "transparent", + }, + vAlignStart: { + justifyContent: "start", + }, + vAlignCenter: { + justifyContent: "center", + }, + vAlignEnd: { + justifyContent: "end", + }, +}); +export const getFlexItemStyles = makeStyles({ + grow: { + flexGrow: 1, + }, + noShrink: { + flexShrink: 0, + }, +}); diff --git a/samples/typescript/11.react-video-js/src/components/flex/index.tsx b/samples/typescript/11.react-video-js/src/components/flex/index.tsx new file mode 100644 index 000000000..c5c4da802 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/flex/index.tsx @@ -0,0 +1,4 @@ +export * from "./FlexColumn"; +export * from "./FlexItem"; +export * from "./FlexRow"; +export * from "./flex-styles"; diff --git a/samples/typescript/11.react-video-js/src/components/index.ts b/samples/typescript/11.react-video-js/src/components/index.ts new file mode 100644 index 000000000..6e65d32a9 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/components/index.ts @@ -0,0 +1,19 @@ +import { MediaPlayerContainer } from "./MediaPlayerContainer"; +import { MediaCard } from "./MediaCard"; +import { LiveNotifications } from "./LiveNotifications"; +import { PageError } from "./PageError"; +import { ListWrapper } from "./ListWrapper"; +import { FlexRow, FlexColumn, FlexItem } from "./flex"; +import { LiveSharePage } from "./LiveSharePage"; + +export { + MediaPlayerContainer, + MediaCard, + LiveNotifications, + PageError, + ListWrapper, + FlexRow, + FlexColumn, + FlexItem, + LiveSharePage, +}; diff --git a/samples/typescript/11.react-video-js/src/constants/AppConfiguration.ts b/samples/typescript/11.react-video-js/src/constants/AppConfiguration.ts new file mode 100644 index 000000000..78aa2d79c --- /dev/null +++ b/samples/typescript/11.react-video-js/src/constants/AppConfiguration.ts @@ -0,0 +1,10 @@ +export interface IAppConfiguration { + /** + * Flag to fully optimize for large meetings, including disabling background updates for non-leaders. + */ + isFullyLargeMeetingOptimized: boolean; +} +export const AppConfiguration: IAppConfiguration = { + // Set to false to disable large meeting optimizations + isFullyLargeMeetingOptimized: true, +}; diff --git a/samples/typescript/11.react-video-js/src/constants/allowed-roles.ts b/samples/typescript/11.react-video-js/src/constants/allowed-roles.ts new file mode 100644 index 000000000..70a55e8ed --- /dev/null +++ b/samples/typescript/11.react-video-js/src/constants/allowed-roles.ts @@ -0,0 +1,8 @@ +import { UserMeetingRole } from "@microsoft/live-share"; + +// Choose roles that can control playback (e.g., pause/play) +// If empty or undefined, all users can control playback +export const ACCEPT_PLAYBACK_CHANGES_FROM = [ + UserMeetingRole.presenter, + UserMeetingRole.organizer, +]; diff --git a/samples/typescript/11.react-video-js/src/constants/index.ts b/samples/typescript/11.react-video-js/src/constants/index.ts new file mode 100644 index 000000000..108e45588 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/constants/index.ts @@ -0,0 +1,7 @@ +import { inTeams } from "../utils/inTeams"; + +export * from "./allowed-roles"; +export * from "./unique-keys"; +export * from "./AppConfiguration"; + +export const IN_TEAMS = inTeams(); diff --git a/samples/typescript/11.react-video-js/src/constants/unique-keys.ts b/samples/typescript/11.react-video-js/src/constants/unique-keys.ts new file mode 100644 index 000000000..dd492797a --- /dev/null +++ b/samples/typescript/11.react-video-js/src/constants/unique-keys.ts @@ -0,0 +1,14 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const UNIQUE_KEYS = { + notifications: "NOTIFICATIONS", + media: "MEDIA", + playlist: "PLAYLIST", + selectedVideoId: "SELECTED-VIDEO-ID", + presence: "PRESENCE", + takeControl: "TAKE-CONTROL", + inking: "INKING", +}; diff --git a/samples/typescript/11.react-video-js/src/index.css b/samples/typescript/11.react-video-js/src/index.css new file mode 100644 index 000000000..98556d760 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/samples/typescript/11.react-video-js/src/index.tsx b/samples/typescript/11.react-video-js/src/index.tsx new file mode 100644 index 000000000..308994982 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./index.css"; + +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render( + + + +); diff --git a/samples/typescript/11.react-video-js/src/live-share-hooks/index.ts b/samples/typescript/11.react-video-js/src/live-share-hooks/index.ts new file mode 100644 index 000000000..9f0200220 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/live-share-hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./useMediaSession"; +export * from "./useNotifications"; +export * from "./useTakeControl"; +export * from "./useInkingManager"; diff --git a/samples/typescript/11.react-video-js/src/live-share-hooks/useInkingManager.ts b/samples/typescript/11.react-video-js/src/live-share-hooks/useInkingManager.ts new file mode 100644 index 000000000..bd40f4b1c --- /dev/null +++ b/samples/typescript/11.react-video-js/src/live-share-hooks/useInkingManager.ts @@ -0,0 +1,30 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { RefObject, useEffect } from "react"; +import { UNIQUE_KEYS } from "../constants"; +import { useLiveCanvas } from "@microsoft/live-share-react"; + +/** + * Sets up LiveCanvas instance + */ +export const useInkingManager = ( + threadId: string, + hostingElement: RefObject +) => { + const { inkingManager, liveCanvas } = useLiveCanvas( + `${threadId}/${UNIQUE_KEYS.inking}`, + hostingElement + ); + + useEffect(() => { + inkingManager?.activate(); + }, [inkingManager]); + + return { + inkingManager, + liveCanvas, + }; +}; diff --git a/samples/typescript/11.react-video-js/src/live-share-hooks/useMediaSession.ts b/samples/typescript/11.react-video-js/src/live-share-hooks/useMediaSession.ts new file mode 100644 index 000000000..036b108ea --- /dev/null +++ b/samples/typescript/11.react-video-js/src/live-share-hooks/useMediaSession.ts @@ -0,0 +1,237 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ExtendedMediaMetadata, + IMediaPlayer, + IMediaPlayerSynchronizerEvent, + MediaPlayerSynchronizerEvents, +} from "@microsoft/live-share-media"; +import { useEffect, useCallback } from "react"; +import { MediaItem } from "../utils/media-list"; +import { useMediaSynchronizer } from "@microsoft/live-share-react"; +import { + ACCEPT_PLAYBACK_CHANGES_FROM, + AppConfiguration, + IN_TEAMS, + UNIQUE_KEYS, +} from "../constants"; +import { meeting } from "@microsoft/teams-js"; +import { DisplayNotificationCallback } from "./useNotifications"; + +/** + * Hook that synchronizes a media element using MediaSynchronizer and LiveMediaSession + * + * @remarks + * Works with any HTML5

+ {/* Live Share wrapper to show loading indicator before setup */} + + + +
+ + ); +}; + +interface IMeetingStateContentProps { + context: app.Context; + shareStatus: ISharingStatus; +} + +const SELECTED_MEDIA_ITEM = + "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov"; + +const MeetingStageContent: FC = ({ + context, + shareStatus, +}) => { + // Element ref for inking canvas + const canvasRef = useRef(null); + // Media player + const [player, setPlayer] = useState(null); + // Flag tracking whether player setup has started + const playerSetupStarted = useRef(false); + + const { notificationToDisplay, displayNotification } = + liveShareHooks.useNotifications(); + + const threadId = + context.meeting?.id ?? + context.chat?.id ?? + context.channel?.id ?? + "unknown"; + + // Take control map + const { + localUserIsPresenting, // boolean that is true if local user is currently presenting + localUserIsEligiblePresenter, // boolean that is true if the local user has the required roles to present + takeControl, // callback method to take control of playback + } = liveShareHooks.useTakeControl( + threadId, + shareStatus.isShareInitiator, + displayNotification + ); + + // Media session hook + const { + suspended, // boolean that is true if synchronizer is suspended + play, // callback method to synchronize a play action + pause, // callback method to synchronize a pause action + seekTo, // callback method to synchronize a seekTo action + endSuspension, // callback method to end the synchronizer suspension + } = liveShareHooks.useMediaSession( + threadId, + localUserIsPresenting, + shareStatus.isShareInitiator, + player, + SELECTED_MEDIA_ITEM, + displayNotification + ); + + // Set up the media player + useEffect(() => { + if (player || playerSetupStarted.current) return; + playerSetupStarted.current = true; + const options: any = { + src: "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov", + preload: "auto", + poster: "https://images4.alphacoders.com/247/247356.jpg", + sources: [ + { + src: "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov", + type: "video/mp4", + }, + ], + controls: true, + controlBar: { + lockShowing: true, + }, + }; + videojs("video", options, function () { + const videojsDelegate = new VideoJSDelegate(this); + setPlayer(videojsDelegate); + }); + }, [player, setPlayer]); + + useEffect(() => { + if (!player) return; + const controlBar = player.getChild("controlBar"); + const children = player.children(); + if (children.length === 0) return; + const videoEl = children[0]; + const unsubscribes: Function[] = []; + const togglePlayPause = () => { + // inverse because we assume the state already changed by the time this emits + if (!player.paused) { + play().catch((err) => console.error(err)); + } else { + pause().catch((err) => console.error(err)); + } + }; + videoEl.onclick = togglePlayPause; + unsubscribes.push(() => { + videoEl.onclick = undefined; + }); + + const poster = player.getChild("PosterImage"); + if (poster) { + poster.on("click", togglePlayPause); + unsubscribes.push(() => { + poster.off("click", togglePlayPause); + }); + } + + if (controlBar) { + const playToggle = controlBar.getChild("playToggle"); + if (playToggle) { + playToggle.on("click", togglePlayPause); + unsubscribes.push(() => { + playToggle.off("click", togglePlayPause); + }); + } + const progressControl = controlBar.getChild("ProgressControl"); + if (progressControl) { + const handler = (evt: any) => { + seekTo(player.currentTime).catch((err) => + console.error(err) + ); + }; + progressControl.on("mouseup", handler); + progressControl.on("touchend", handler); + unsubscribes.push(() => { + progressControl.off("mouseup", handler); + progressControl.off("touchend", handler); + }); + } + } + const bigPlayButton = player.getChild("BigPlayButton"); + if (bigPlayButton) { + const handler = (evt: any) => { + play(); + evt.stopPropagation(); + }; + bigPlayButton.on("click", handler); + unsubscribes.push(() => { + bigPlayButton.off("click", handler); + }); + } + + return () => { + unsubscribes.forEach((unsub) => unsub()); + }; + }, [player, play, pause, seekTo]); + + return ( + <> + {/* Display Notifications */} + + {/* Media Player */} + + {/* // Render video */} + + + ); +}; + +export default MeetingStage; diff --git a/samples/typescript/11.react-video-js/src/pages/TabConfig.tsx b/samples/typescript/11.react-video-js/src/pages/TabConfig.tsx new file mode 100644 index 000000000..e4e06d015 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/pages/TabConfig.tsx @@ -0,0 +1,36 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { pages } from "@microsoft/teams-js"; +import { FC, useEffect } from "react"; +import { Title2, Subtitle2 } from "@fluentui/react-components"; +import { FlexColumn } from "../components"; + +const TabConfig: FC = () => { + useEffect(() => { + pages.config.registerOnSaveHandler(function (saveEvent) { + pages.config.setConfig({ + suggestedDisplayName: "Contoso", + contentUrl: `${window.location.origin}/?inTeams=true`, + }); + saveEvent.notifySuccess(); + }); + + pages.config.setValidityState(true); + }, []); + + return ( + + + Welcome to Contoso Media! + + + Press the save button to continue. + + + ); +}; + +export default TabConfig; diff --git a/samples/typescript/11.react-video-js/src/styles/layouts.ts b/samples/typescript/11.react-video-js/src/styles/layouts.ts new file mode 100644 index 000000000..b2e8d4e18 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/styles/layouts.ts @@ -0,0 +1,116 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { makeStyles } from "@fluentui/react-components"; +import { tokens } from "@fluentui/react-theme"; + +export const getFlexRowStyles = makeStyles({ + root: { + display: "flex", + minHeight: "0px", + }, + fill: { + width: "100%", + height: "100%", + }, + smallGap: { + "> :not(:last-child)": { + marginRight: "0.5rem", + }, + }, + spaceBetween: { + justifyContent: "space-between", + }, + hAlignStart: { + justifyContent: "start", + }, + hAlignCenter: { + justifyContent: "center", + }, + hAlignEnd: { + justifyContent: "end", + }, + vAlignStart: { + alignItems: "start", + }, + vAlignCenter: { + alignItems: "center", + }, + vAlignEnd: { + alignItems: "end", + }, +}); + +export const getFlexColumnStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + minHeight: "0px", + }, + fill: { + width: "100%", + height: "100%", + }, + smallGap: { + "> :not(:last-child)": { + marginBottom: "0.5rem", + }, + }, + spaceBetween: { + justifyContent: "space-between", + }, + hAlignStart: { + alignItems: "start", + }, + hAlignCenter: { + alignItems: "center", + }, + hAlignEnd: { + alignItems: "end", + }, + vAlignStart: { + justifyContent: "start", + }, + vAlignCenter: { + justifyContent: "center", + }, + vAlignEnd: { + justifyContent: "end", + }, + scroll: { + overflowY: "auto", + msOverflowStyle: "auto", + maxHeight: "100vh", + "::-webkit-scrollbar": { + width: "12px", + }, + "::-webkit-scrollbar-track": { + backgroundColor: "transparent", + }, + "::-webkit-scrollbar-thumb": { + // backgroundColor: tokens.colorPaletteCharcoalBackground3, + // borderLeftColor: tokens.colorPaletteCharcoalBackground2, + borderLeftWidth: "6px", + borderLeftStyle: "solid", + backgroundClip: "padding-box", + borderTopLeftRadius: "4px", + borderTopRightRadius: "14px", + borderBottomLeftRadius: "5px", + borderBottomRightRadius: "14px", + }, + "::-webkit-scrollbar-thumb:hover": { + // backgroundColor: tokens.colorPaletteCharcoalForeground3, + }, + }, +}); + +export const getFlexItemStyles = makeStyles({ + grow: { + flexGrow: 1, + }, + noShrink: { + flexShrink: 0, + }, +}); diff --git a/samples/typescript/11.react-video-js/src/styles/styles.ts b/samples/typescript/11.react-video-js/src/styles/styles.ts new file mode 100644 index 000000000..a550cb1d3 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/styles/styles.ts @@ -0,0 +1,118 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { makeStyles, shorthands } from "@fluentui/react-components"; +import { tokens } from "@fluentui/react-theme"; + +export const getVideoStyle = makeStyles({ + root: { + cursor: "pointer", + zIndex: 0, + position: "fixed", + backgroundColor: "black", + display: "flex", + flexDirection: "column", + minHeight: "0px", + justifyContent: "center", + alignItems: "center", + outlineWidth: "0px", + }, +}); + +export const getPlayerControlStyles = makeStyles({ + root: { + backgroundColor: "black", + position: "absolute", + zIndex: 0, + top: "0", + right: "0", + left: "0", + bottom: "0", + }, +}); + +export const getProgressBarStyles = makeStyles({ + root: { + width: "100%", + cursor: "pointer", + minHeight: "0px", + }, + input: { + cursor: "pointer", + }, + rail: { + backgroundImage: `linear-gradient( + to right, + rgba(255,255,255, 1) 0%, + rgba(255,255,255,1) var(--oneplayer-play-progress-percent), + rgba(255,255,255,0.5) var(--oneplayer-play-progress-percent), + rgba(255,255,255,0.5) var(--oneplayer-buff-progress-percent), + rgba(255,255,255,0.3) var(--oneplayer-buff-progress-percent), + rgba(255,255,255,0.3) 100%) + `, + ":before": { + backgroundImage: "none", + }, + }, + thumb: { + width: "1rem", + height: "1rem", + backgroundColor: "white", + boxShadow: "none", + ":before": { + ...shorthands.borderColor("white"), + }, + }, + pageEl: { + backgroundColor: "transparent", + color: "white", + ...shorthands.padding(".75rem"), + }, +}); + +export const getPillStyles = makeStyles({ + root: { + backgroundColor: tokens.colorNeutralBackground3, + color: tokens.colorNeutralForeground1, + pointerEvents: "none", + fontSize: "1rem", + lineHeight: "100%", + paddingTop: "0.6rem", + paddingBottom: "0.6rem", + paddingLeft: "0.8rem", + paddingRight: "0.8rem", + borderTopLeftRadius: "1.6rem", + borderTopRightRadius: "1.6rem", + borderBottomLeftRadius: "1.6rem", + borderBottomRightRadius: "1.6rem", + marginBottom: "0.8rem", + maxWidth: "80%", + }, +}); + +export const getResizeReferenceStyles = makeStyles({ + root: { + position: "absolute", + zIndex: 0, + top: "0", + bottom: "0", + left: "0", + right: "0", + textAlign: "center", + pointerEvents: "none", + }, +}); + +export const getLiveNotificationStyles = makeStyles({ + root: { + pointerEvents: "none", + position: "fixed", + zIndex: 3, + top: "5px", + left: "4px", + right: "4px", + textAlign: "center", + }, +}); diff --git a/samples/typescript/11.react-video-js/src/teams-js-hooks/useSharingStatus.ts b/samples/typescript/11.react-video-js/src/teams-js-hooks/useSharingStatus.ts new file mode 100644 index 000000000..3de28db5e --- /dev/null +++ b/samples/typescript/11.react-video-js/src/teams-js-hooks/useSharingStatus.ts @@ -0,0 +1,100 @@ +import * as teamsJs from "@microsoft/teams-js"; +import { useEffect, useState, useRef } from "react"; +import { inTeams } from "../utils/inTeams"; + +export interface ISharingStatus { + isAppSharing: boolean; + isShareInitiator: boolean; +} + +export const useSharingStatus = (): ISharingStatus | undefined => { + const [status, setSharingStatus] = useState( + () => { + if (inTeams()) return undefined; + return { + isAppSharing: false, + isShareInitiator: getSimulatedIsShareInitiator(), + }; + } + ); + const intervalIdRef = useRef(); + + useEffect(() => { + if (!inTeams()) return; + if (!intervalIdRef.current) { + if (teamsJs.meeting) { + const setAppSharingStatus = () => { + teamsJs.meeting.getAppContentStageSharingCapabilities( + (capabilitiesError, result) => { + if ( + !capabilitiesError && + result?.doesAppHaveSharePermission + ) { + teamsJs.meeting.getAppContentStageSharingState( + (_, state) => { + if (state) { + setSharingStatus( + polyfillSharingStatus(state) + ); + } else { + setSharingStatus({ + isAppSharing: false, + isShareInitiator: false, + }); + } + } + ); + } else { + setSharingStatus({ + isAppSharing: false, + isShareInitiator: false, + }); + } + } + ); + }; + setAppSharingStatus(); + intervalIdRef.current = setInterval(() => { + setAppSharingStatus(); + }, 2000); + } + } + return () => { + if (intervalIdRef.current) { + clearInterval(intervalIdRef.current); + } + }; + }, []); + + return status; +}; + +/** + * Teams JS has not yet added `isShareInitiator` to the `IAppContentStageSharingState` interface. + * However, it does return it in the response, so we polyfill it. + */ +function polyfillSharingStatus( + state: teamsJs.meeting.IAppContentStageSharingState +): ISharingStatus { + if (isShareStatus(state)) return state; + return { + isAppSharing: state.isAppSharing, + isShareInitiator: false, + }; +} + +function isShareStatus(value: any): value is ISharingStatus { + if (typeof value !== "object") return false; + if (typeof value.isAppSharing !== "boolean") return false; + return ( + typeof value.isShareInitiator === "boolean" || + value.isShareInitiator === undefined + ); +} + +// In localhost, we will default to isShareInitiator == true, but can override with isShareInitiator search param. +function getSimulatedIsShareInitiator(): boolean { + const url = new URL(window.location.href); + const param = url.searchParams.get("isShareInitiator"); + return param !== null ? param === "true" : true; +} diff --git a/samples/typescript/11.react-video-js/src/teams-js-hooks/useTeamsContext.ts b/samples/typescript/11.react-video-js/src/teams-js-hooks/useTeamsContext.ts new file mode 100644 index 000000000..6a8449cc4 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/teams-js-hooks/useTeamsContext.ts @@ -0,0 +1,79 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as microsoftTeams from "@microsoft/teams-js"; +import { app } from "@microsoft/teams-js"; +import { useEffect, useState, useRef } from "react"; +import { inTeams } from "../utils/inTeams"; + +export const useTeamsContext = () => { + const startedRef = useRef(false); + const [ctx, setCtx] = useState(); + + useEffect(() => { + if (startedRef.current) return; + startedRef.current = true; + if (!ctx?.user?.id) { + // Add inTeams=true to URL params to get real Teams context + if (inTeams()) { + console.log("useTeamsContext: Attempting to get Teams context"); + // Get Context from the Microsoft Teams SDK + microsoftTeams.app + .getContext() + .then((context) => { + console.log( + `useTeamsContext: received context: ${JSON.stringify( + context + )}` + ); + setCtx(context); + }) + .catch((error) => console.error(error)); + } else { + console.log("useTeamsContext: Creating fake Teams context"); + setCtx(createFakeContext()); + } + } + }, [ctx?.user?.id]); + + return ctx; +}; + +const createFakeContext = () => { + const fakeHost: app.AppHostInfo = { + name: microsoftTeams.HostName.teams, + clientType: microsoftTeams.HostClientType.web, + sessionId: "", + }; + + const fakeAppInfo: app.AppInfo = { + locale: "", + theme: "", + sessionId: "", + host: fakeHost, + }; + + const fakePageInfo: app.PageInfo = { + id: "", + frameContext: microsoftTeams.FrameContexts.meetingStage, + }; + + const fakeUserInfo: app.UserInfo = { + id: `user${Math.abs(Math.random() * 999999999)}`, + }; + + const fakeMeetingInfo: app.MeetingInfo = { + id: "foo", + }; + + const fakeContext: app.Context = { + app: fakeAppInfo, + page: fakePageInfo, + user: fakeUserInfo, + meeting: fakeMeetingInfo, + }; + + return fakeContext; +}; diff --git a/samples/typescript/11.react-video-js/src/utils/VideoJSDelegate.ts b/samples/typescript/11.react-video-js/src/utils/VideoJSDelegate.ts new file mode 100644 index 000000000..4d30485b9 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/VideoJSDelegate.ts @@ -0,0 +1,357 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { isErrorLike } from "@microsoft/live-share/internal"; +import Component from "video.js/dist/types/component"; +import Player from "video.js/dist/types/player"; + +const BlockDetectionState = { + unknown: "unknown", + detecting: "detecting", + unblocked: "unblocked", +}; + +const PlayerEvent = { + playing: "playing", + play: "play", + pause: "pause", + rateChange: "ratechange", + timeUpdate: "timeupdate", + ended: "ended", + loadedMetadata: "loadedmetadata", + loadedData: "loadeddata", + stalled: "stalled", + blocked: "blocked", + volumeChange: "volumechange", + durationChange: "durationchange", + ready: "ready", + seeked: "seeked", + seeking: "seeking", + waiting: "waiting", + canPlayThrough: "canplaythrough", + canPlay: "canplay", + emptied: "emptied", + error: "error", + abort: "abort", + suspend: "suspend", + progress: "progress", + loadStart: "loadstart", +}; + +declare global { + interface Window { + amp: any; // turn off type checking + } +} + +interface TrackState { + started: boolean; + ended: boolean; + error?: any; + loaded: boolean; + paused: boolean; + playing: boolean; + src: string; + lastPosition: number; + lastPositionCheck: number; + skipTo?: { + timeAdded: number; + position: number; + }; + autoPause: boolean; +} + +/** + * Class for VideoJSDelegate HTML shiv for compatibility with `MediaSynchronizer`. + * If your media player's interface doesn't follow the HTML interface, this example + * will help show how to create a thin wrapper around your player to make it function + * exactly like an HTML5 media element so the `MediaSynchronizer` can properly wire + * up `LiveMediaSession` action handlers. + */ +export class VideoJSDelegate extends EventTarget { + _player: Player; + + // Position tracking + _positionTimer: NodeJS.Timeout | undefined; + + // Track state + _track = emptyTrackState(""); + _blockedState = BlockDetectionState.unknown; + + // If browser blocks initial play event due to autoplay policy + // we mute and try again + _autoplayPolicyChecked = false; + + constructor(player: Player) { + super(); + this._player = player; + this._startPositionTracker(); + Object.entries(PlayerEvent).forEach(([_, value]) => { + this._player.on(value, this._onStateChangeEvent.bind(this)); + }); + } + + //--------------------------------------------------------------------------------------------- + // Player Source + //--------------------------------------------------------------------------------------------- + get currentSrc(): string { + return this._track.src; + } + get src(): string { + return this._track.src; + } + /** + * @param {src} value + */ + set src(value: string) { + if (this._track.src !== value) { + this._track = emptyTrackState(value); + } + } + + //--------------------------------------------------------------------------------------------- + // Ready state + //--------------------------------------------------------------------------------------------- + + get readyState(): number { + return this._player.readyState(); + } + + get seeking(): boolean { + return false; + } + + //--------------------------------------------------------------------------------------------- + // Playback state + //--------------------------------------------------------------------------------------------- + + get currentTime(): number { + return this._player.currentTime() || 0; + } + + /** + * @param {number} value timestamp in seconds + */ + set currentTime(value: number) { + this._player.currentTime(value); + } + + get duration(): number { + return this._player.duration() || 0; + } + + get paused(): boolean { + return this._player.paused(); + } + + get playbackRate(): number { + return this._player.playbackRate() || 1; + } + + /** + * @param {number} value (e.g., 1.0) + */ + set playbackRate(value: number) { + this._player.playbackRate(value); + } + + get ended(): boolean { + return this._player.ended(); + } + + get autoplay(): boolean { + return this._player.autoplay() !== false; + } + + /** + * @param {boolean} value + */ + set autoplay(value: boolean) { + this._player.autoplay(value); + } + + //--------------------------------------------------------------------------------------------- + // Player Controls + //--------------------------------------------------------------------------------------------- + + get muted(): boolean { + return this._player.muted() ?? false; + } + + set muted(value: boolean) { + this._player.muted(value); + } + + get volume(): number { + return this._player.volume() ?? 0; + } + + /** + * @param {number} value volume between 0 and 100 + */ + set volume(value: number) { + this._player.volume(value); + } + + //--------------------------------------------------------------------------------------------- + // Transport Controls + //--------------------------------------------------------------------------------------------- + + load(): void { + this._player.src({ src: this._track.src }); + } + + async play(): Promise { + try { + await this._player.play(); + } catch (err) { + if ( + isErrorLike(err) && + err.message.includes("didn't interact with the document first") + ) { + this.muted = true; + return await this._player.play(); + } + throw err; + } + } + + pause(): void { + this._player.pause(); + } + + //--------------------------------------------------------------------------------------------- + // Position Tracking + //--------------------------------------------------------------------------------------------- + + _startPositionTracker(): void { + if (this._positionTimer === undefined) { + this._positionTimer = setInterval(() => { + // Is the player playing? + if (this._track.playing) { + // Dispatch timeupdate event + this.dispatchEvent(new Event(PlayerEvent.timeUpdate)); + } + + // Check for seek + // - If the playback head jumps forwards/backwards by more than 1 second + // we'll fire a seeked event. + const now = new Date().getTime(); + const position = this.currentTime; + if (this._track.lastPositionCheck > 0) { + const movement = + Math.abs(position - this._track.lastPosition) * 1000; + const range = now - this._track.lastPositionCheck + 2000; + if (movement > range) { + this.dispatchEvent(new Event(PlayerEvent.seeked)); + } + } + + // Remember last position + this._track.lastPositionCheck = now; + this._track.lastPosition = position; + }, 250); + } + } + + _stopPositionTracker(): void { + if (this._positionTimer !== undefined) { + clearInterval(this._positionTimer); + this._positionTimer = undefined; + this._track.lastPositionCheck = -1; + this._track.lastPosition = -1; + } + } + + _applySkipTo(adjustPosition: boolean): void { + if (this._track.skipTo && this._player) { + const skipTo = this._track.skipTo; + this._track.skipTo = undefined; + + // Seek to adjusted position + const adjustment = adjustPosition + ? (new Date().getTime() - skipTo.timeAdded) / 1000 + : 0; + this.currentTime = skipTo.position + adjustment; + } + } + + //--------------------------------------------------------------------------------------------- + // Player Events + //--------------------------------------------------------------------------------------------- + + _onStateChangeEvent(event: any): void { + // Dispatch state + switch (event.type) { + case PlayerEvent.playing: + this._track.started = true; + // Only fire play event when state first entered. + // - We could be continuing after buffering + if (!this._track.playing) { + this._startPositionTracker(); + this._track.ended = false; + this._track.playing = true; + this._track.paused = false; + } + + this._blockedState = BlockDetectionState.unblocked; + // this._applySkipTo(true); + break; + case PlayerEvent.pause: + this._track.ended = false; + this._track.playing = false; + this._track.paused = true; + this._blockedState = BlockDetectionState.unblocked; + // this._applySkipTo(false); + break; + case PlayerEvent.ended: + this.load(); + this.play(); + this.pause(); + this._stopPositionTracker(); + this._track.ended = true; + this._track.playing = false; + this._track.paused = true; + break; + default: + break; + } + if (event.type !== PlayerEvent.timeUpdate) { + this.dispatchEvent(new Event(event.type)); + } + } + + //--------------------------------------------------------------------------------------------- + // Video JS Specific APIs + //--------------------------------------------------------------------------------------------- + + height(value?: number | string): number | undefined { + return this._player.height(value); + } + + getChild(name: string): Component | undefined { + return this._player.getChild(name); + } + + children(): any[] { + return this._player.children(); + } +} + +function emptyTrackState(src: string): TrackState { + return { + started: false, + ended: false, + error: null, + loaded: false, + paused: true, + playing: false, + src: src, + lastPosition: -1, + lastPositionCheck: -1, + skipTo: undefined, + autoPause: false, + }; +} diff --git a/samples/typescript/11.react-video-js/src/utils/format.ts b/samples/typescript/11.react-video-js/src/utils/format.ts new file mode 100644 index 000000000..3adb56c18 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/format.ts @@ -0,0 +1,21 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const formatTimeValue = (timeValue: number) => { + if (!timeValue) { + return "0:00"; + } + const minutes = Math.floor(timeValue / 60); + const seconds = Math.floor(timeValue % 60); + const secondsFormatted = seconds >= 10 ? seconds : `0${seconds}`; + if (minutes >= 60) { + const hours = Math.floor(minutes / 60); + const minutesRemainder = Math.floor(minutes % 60); + const minutesFormatted = + minutesRemainder >= 10 ? minutesRemainder : `0${minutesRemainder}`; + return `${hours}:${minutesFormatted}:${secondsFormatted}`; + } + return `${minutes}:${secondsFormatted}`; +}; diff --git a/samples/typescript/11.react-video-js/src/utils/inTeams.ts b/samples/typescript/11.react-video-js/src/utils/inTeams.ts new file mode 100644 index 000000000..45ce31255 --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/inTeams.ts @@ -0,0 +1,14 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export function inTeams() { + const currentUrl = window.location.href; + // Check if using HistoryRouter + const url = currentUrl.includes("/#/") + ? new URL(`${window.location.href.split("/#/").join("/")}`) + : new URL(window.location.href); + const params = url.searchParams; + return !!params.get("inTeams"); +} diff --git a/samples/typescript/11.react-video-js/src/utils/media-list.ts b/samples/typescript/11.react-video-js/src/utils/media-list.ts new file mode 100644 index 000000000..c34396dfe --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/media-list.ts @@ -0,0 +1,89 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export type MediaItem = { + id: string; + thumbnailImage: string; + title: string; + src: string; + type: string; +}; + +export const mediaList: MediaItem[] = [ + { + id: "0", + thumbnailImage: + "https://by3302files.storage.live.com/y4mohyKkLvuC6HfaeJbCtX1gnRdCHlLpJe1n6L4OLkd4z9ljbC_w-vcL0RJjs0k5IJvVY3AcJ1O0wmfG_Bwzs7CUCwqTTwuPNej7MiIbXqQDySexIq_qRUVK7v68bA4glUS6UBn-lmZBfiWI51h63-tZvhwtso6_55F1xjvo6z02v-K5yp56mTi0LsWPLblOPxL?width=660&height=371&cropmode=none", + title: "Teams Channel Trailer", + src: "https://livesharemedia-usw22.streaming.media.azure.net/d636cbe2-5e4a-419b-958e-6cc81dd3ca72/15 - Channel Trailer V8.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, +]; + +export const searchList: MediaItem[] = [ + ...mediaList, + { + id: "1", + thumbnailImage: + "https://by3302files.storage.live.com/y4mJHPKYRDVa0Tq0tEL0MLkNp8Od4hPhcYudyjGdnU7DgErpEgEAflpu3f4QLCa5BjIwE11D2R3iKGauB3I0pbJDKSKrc6hMCB46pMB7fdsmbiAao0C5RmXOfzEZKcX6dFwQiIod2G0zeyfRL2akN9A8jw0Pf-sGWnKkXE-nMWv8-42_jM88n-F3PooGSSKaAyH?width=660&height=372&cropmode=none", + title: "Onboarding to Teams Apps", + src: "https://livesharemedia-usw22.streaming.media.azure.net/8bf16f5f-d4b0-437b-8121-d780abe744c9/Onboarding_Video11_Final_H264.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, + { + id: "2", + thumbnailImage: + "https://by3302files.storage.live.com/y4mUBBvkvbh4U8GCw-auQJ4c0e9AoU9jk7OmqL1HoI5uv0ah2IVSngvGqhg4l-1P2w2rA1CqunF16uhBvYU_FXRpQq_UtsDg6cXovkskMxi-3tO-_r41HJg6Y2a_u0qkUl1amExyG7yl3Q8fmAvMRrXEg5D1ouyYX82YP9BqQDRFJlgVx9tCFk-jfLGVO1nMIru?width=660&height=369&cropmode=none", + title: "3 app tips in Teams", + src: "https://livesharemedia-usw22.streaming.media.azure.net/113f04f1-5e06-45f5-be52-b848aa3f7afe/71372_teams_tip_three_apps_chat_.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, + { + id: "3", + thumbnailImage: + "https://by3302files.storage.live.com/y4mvA_fpovY-RkRbEAK3JFfyFJIJaikw2iFDNYeAPncjPRJ7jRfiJyVFiSk9Sq5qNXS7NpzBgBP9NHFr_I2KMs8qUgxkGTPebP86JQOHuezGclUtVPsmtzfA-7yYWJxFsRowzB6v6okwDQdC9il2Xq6183WBGgKWCwiCTX0HZ9ELVQFdUwfgxTK5BwEb79r_mPh?width=660&height=370&cropmode=none", + title: "Reply to a message demo", + src: "https://livesharemedia-usw22.streaming.media.azure.net/8961cf76-8e34-440f-b52d-e50601d8cf61/Reply_To_A_Message_Aisha_MASTER.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, + { + id: "4", + thumbnailImage: + "https://by3302files.storage.live.com/y4mL6V3p0Sqx7xCXX96BYtVxG-GSNNQ573Cc_SP769NWcpdjhhfOG9ZjzlndGQBqeoEkVARmtoW0rwyRe3Up7ldUin3YUhte0MSh5aYpxi68IDDJ19drmIDC6VF_h3w_2tPANAl27g15f4w42aANYhPM4VyeREVKp3ItAUNoULO6NXbQPooDXpfQMN4fYuNdqL6?width=660&height=374&cropmode=none", + title: "Standout presenter demo", + src: "https://livesharemedia-usw22.streaming.media.azure.net/a0e332ba-4b9c-4172-a7e4-f74e32eff698/Standout_Presenter_Aisha_MASTER.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, + { + id: "5", + thumbnailImage: + "https://by3302files.storage.live.com/y4m9tgZXn-22LyDf-IU2Wg_GMJE-JPMHbmO2fXO2DzI9ZGKYKguArpqcLTgmIOaIeaMDKoEDOOvYnIkHsys_Q7WvM8hHOGGJVwvHs1ksd12jiG8ZecJvevz_K7wE2VqqqG5Z3bZnDpuWvlolYmgzd_StHgYZodbmtHbNFrf5AUekdx8uNfDIptlP7Rqnt-mpO7P?width=1443&height=811&cropmode=none", + title: "Hang Gliding", + src: "https://livesharemedia-usw22.streaming.media.azure.net/2b941807-5fd5-45b6-bdc5-362c3c20ae8f/pexels-cédric-estienne-6749146.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, + { + id: "6", + thumbnailImage: + "https://by3302files.storage.live.com/y4mzi99em6H2SBgawxkOet8pPzlPhQC2L4RWj8-qK3wZBhL_Ybol_8p04L_in28Phl7_D9hOMjl97aEjD93ZB7X18COjmHysfs0bK93goF_FMDBBEJUXAHbmkgvUHFj45VWtEN3n6BSoXZlvZWQXYoDRoGtuBLVMK7wbFUoYxE6ZJR0i8xvycB7bEQ2eJ2iSuVR?width=256&height=136&cropmode=none", + title: "Dog Days", + src: "https://livesharemedia-usw22.streaming.media.azure.net/a5a174b6-3ca1-4b33-96fb-ce20b4b3eda1/pexels-zen-chung-5740700.ism/manifest(format=mpd-time-cmaf,encryption=cbc)", + type: "video", + }, +]; + +function getInitialMediaId() { + const url = new URL(window.location.href); + const params = url.searchParams; + return params.get("mediaId"); +} + +export function getInitialMediaItem(): MediaItem { + const mediaId = getInitialMediaId(); + const mediaItem = mediaList.find( + (mediaItem) => `${mediaItem.id}` === mediaId + ); + return mediaItem ? mediaItem : mediaList[0]; +} diff --git a/samples/typescript/11.react-video-js/src/utils/useEventListener.ts b/samples/typescript/11.react-video-js/src/utils/useEventListener.ts new file mode 100644 index 000000000..6f59151be --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/useEventListener.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef } from "react"; + +// Hook +export const useEventListener = ( + eventName: string, + handler: (event: Event) => void, + element: HTMLElement | Window = window +) => { + // Create a ref that stores handler + const savedHandler = useRef<(event: Event) => void>(); + // Update ref.current value if handler changes. + // This allows our effect to be low to always get the latest handler ... + // ... without us needing to pass it in effect deps array ... + // ... and potentially cause effect to re-run every render. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + useEffect( + () => { + // Make sure element supports addEventListener + // On + const isSupported = element && element.addEventListener; + if (!isSupported) return; + // Create event listener that calls handler function stored in ref + const eventListener = (event: Event) => + savedHandler?.current?.(event); + // Add event listener + element.addEventListener(eventName, eventListener); + // Remove event listener on cleanup + return () => { + element.removeEventListener(eventName, eventListener); + }; + }, + [eventName, element] // Re-run if eventName or element changes + ); +}; diff --git a/samples/typescript/11.react-video-js/src/utils/useVisibleVideoSize.ts b/samples/typescript/11.react-video-js/src/utils/useVisibleVideoSize.ts new file mode 100644 index 000000000..f912be2ad --- /dev/null +++ b/samples/typescript/11.react-video-js/src/utils/useVisibleVideoSize.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef, useState } from "react"; + +export interface VideoSize { + width: number; + height: number; + xOffset: number; + yOffset: number; + fScaleToTargetWidth: boolean; +} + +export const useVisibleVideoSize = ( + viewportWidth: number, + viewportHeight: number +) => { + const videoSizeRef = useRef(); + const [videoSize, setVideoSize] = useState(videoSizeRef.current); + // Effect for calculating the rectangle that matches the visible video size + useEffect(() => { + const result = { + width: 0, + height: 0, + xOffset: 0, + yOffset: 0, + fScaleToTargetWidth: true, + }; + const videoWidth = 1920; + const videoHeight = 1080; + + const scaleX1 = viewportWidth; + const scaleY1 = (videoHeight * viewportWidth) / videoWidth; + + // scale to the target height + const scaleX2 = (videoWidth * viewportHeight) / videoHeight; + const scaleY2 = viewportHeight; + + // now figure out which one we should use + let fScaleOnWidth = scaleX2 > viewportWidth; + if (fScaleOnWidth) { + fScaleOnWidth = true; + } else { + fScaleOnWidth = false; + } + + if (fScaleOnWidth) { + result.width = Math.floor(scaleX1); + result.height = Math.floor(scaleY1); + result.fScaleToTargetWidth = true; + } else { + result.width = Math.floor(scaleX2); + result.height = Math.floor(scaleY2); + result.fScaleToTargetWidth = false; + } + result.xOffset = Math.floor((viewportWidth - result.width) / 2); + result.yOffset = Math.floor((viewportHeight - result.height) / 2); + if ( + result.xOffset !== videoSizeRef.current?.xOffset || + result.yOffset !== videoSizeRef.current?.yOffset + ) { + videoSizeRef.current = result; + setVideoSize(videoSizeRef.current); + } + }, [viewportWidth, viewportHeight, videoSizeRef]); + + return videoSize; +}; diff --git a/samples/typescript/11.react-video-js/tsconfig.json b/samples/typescript/11.react-video-js/tsconfig.json new file mode 100644 index 000000000..0987ddfa4 --- /dev/null +++ b/samples/typescript/11.react-video-js/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "moduleResolution": "bundler", + "lib": [ + "es2020", + "DOM" + ] /* Specify library files to be included in the compilation. */, + "allowJs": false /* Allow javascript files to be compiled. */, + "jsx": "react-jsx", + "declaration": true /* Generates corresponding '.d.ts' file. */, + "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + "outDir": "./dist" /* Redirect output structure to the directory. */, + "strict": true /* Enable all strict type-checking options. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "include": ["./src"], + "exclude": ["build", "dist", "node_modules"], + "files": ["src/index.tsx"] +} diff --git a/samples/typescript/11.react-video-js/vite.config.ts b/samples/typescript/11.react-video-js/vite.config.ts new file mode 100644 index 000000000..9d50e3f82 --- /dev/null +++ b/samples/typescript/11.react-video-js/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + preserveSymlinks: true, + }, + server: { + port: 3000, + open: true, + }, + optimizeDeps: { + force: true, + }, +}); diff --git a/samples/typescript/11.react-video-js/vite.https-config.ts b/samples/typescript/11.react-video-js/vite.https-config.ts new file mode 100644 index 000000000..1da5f0689 --- /dev/null +++ b/samples/typescript/11.react-video-js/vite.https-config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + preserveSymlinks: true, + }, + server: { + hmr: { + // Needed to make ngrok work with Vite + clientPort: 443, + }, + port: 3000, + open: true, + }, + optimizeDeps: { + force: true, + }, +}); diff --git a/samples/typescript/21.react-media-template/README.md b/samples/typescript/21.react-media-template/README.md index c6d0e92d6..35808c02b 100644 --- a/samples/typescript/21.react-media-template/README.md +++ b/samples/typescript/21.react-media-template/README.md @@ -13,12 +13,9 @@ After cloning the repository, you must first set up the npm workspace from the r ```bash npm install -npm run build:packages # Build Live Share packages cd samples/t*/21* ``` -_Note:_ Do not run `npm start` before running `npm run build:packages` from the root of the project, unless you first move the sample out of this npm workspace. When using our samples, you are testing the packages using symlinks, and not the Live Share SDK versions published to npm. - ## Testing locally in browser ### `npm run start`