Compare commits

...
Author SHA1 Message Date
shimon e28cbce5ee small updates 2023-05-14 10:30:29 +03:00
shimon 767b5c8541 added internalIds to collections 2023-05-08 10:02:23 +03:00
shimon af80accc15 added internalIds to collections 2023-05-07 17:15:59 +03:00
shimon f3508e1701 Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.gitmodules
	Dockerfile
	app/config/services.php
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/console
	app/controllers/api/databases.php
	app/controllers/api/projects.php
	app/controllers/api/teams.php
	composer.json
	composer.lock
	src/Appwrite/Utopia/Response.php
2023-05-07 11:43:29 +03:00
shimon 3c69c211e9 added internalIds to collections 2023-05-07 11:31:27 +03:00
shimon 927bead147 bug fixes 2023-04-30 18:58:10 +03:00
shimon 6af8fd6660 bug fixes 2023-04-27 12:12:14 +03:00
shimon a187aa4bc5 bug fixes 2023-04-23 19:57:12 +03:00
shimon 60098d71ee adjustments 2023-04-23 10:43:18 +03:00
Shimon NewmanandGitHub 38c64996ac Update videos.php 2023-04-22 22:45:59 +03:00
Shimon NewmanandGitHub 89dfbfba25 Update hls-master.phtml 2023-04-22 22:39:06 +03:00
shimon 578c8d9277 tests 2023-04-17 10:09:48 +03:00
shimon dbd58966db updates 2023-04-16 17:21:47 +03:00
shimon d6e1d0f6f6 updates 2023-04-16 11:43:35 +03:00
shimon 741a87f464 updates 2023-04-12 18:18:56 +03:00
shimon db4506b0fe updates 2023-04-10 16:41:23 +03:00
shimon 14575a7e40 updates 2023-04-10 10:59:51 +03:00
shimon 1830138455 updates 2023-04-10 09:45:15 +03:00
shimon 4cd273e976 updates 2023-04-09 12:58:21 +03:00
shimon fc991e89f1 transcoding -> videos 2023-04-04 20:20:40 +03:00
shimon 9f6589d21b Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	docker-compose.yml
2023-04-04 17:10:31 +03:00
shimon c09f600e73 transcoding -> videos 2023-04-04 17:08:45 +03:00
shimon 2b60b18a3c timeline 2023-04-04 14:24:18 +03:00
shimon 053d688754 Merge remote-tracking branch 'origin/__stream' into __stream
# Conflicts:
#	app/config/locale/languages.php
2023-04-02 13:50:26 +03:00
shimon bccd1a96c0 timeline 2023-04-02 13:48:59 +03:00
shimon 4a8ef126bd Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	app/config/errors.php
	composer.json
	composer.lock
2023-03-23 18:06:11 +02:00
Shimon NewmanandGitHub 61e3faa964 Update languages.php 2023-03-18 10:25:12 +02:00
shimon 22b4555cc0 subtitle lan code 2023-03-13 20:35:39 +02:00
shimon aa8334e7d8 subtitle lan code 2023-03-13 10:16:48 +02:00
shimon 8a8a2d37e4 subtitle lan code 2023-03-12 17:55:41 +02:00
shimon 883d48c2e5 subtitle lan code 2023-03-12 14:53:56 +02:00
shimon cb217b43b4 Updates 2023-03-09 09:57:10 +02:00
shimon 7340968d1f Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/console
2023-03-08 16:31:56 +02:00
shimon 5785145e58 Updates 2023-03-08 16:30:19 +02:00
shimon 9f94560a2c Updates 2023-03-08 16:29:40 +02:00
shimon 92f75bd3d9 Updates 2023-03-07 09:47:50 +02:00
shimon 658cfee38e Updates 2023-03-06 18:28:40 +02:00
shimon 26251e94f6 Updates 2023-03-06 17:58:26 +02:00
shimon dbad8c2a85 Updates 2023-03-05 19:30:53 +02:00
shimon d6d6a7753a Updates 2023-03-05 19:21:51 +02:00
shimon 7a0255d011 Updates 2023-03-05 11:24:24 +02:00
shimon 81ca8a5fc9 metadata extraction ffmpeg -> mediainfo 2023-03-02 18:44:20 +02:00
shimon 8d496d66d1 metadata extraction ffmpeg -> mediainfo 2023-03-02 15:05:15 +02:00
shimon cfb53be8ac metadata extraction ffmpeg -> mediainfo 2023-03-02 10:56:04 +02:00
shimon 4b227c7428 video tests 2023-02-23 09:36:34 +02:00
shimon b9fa699c03 video tests 2023-02-22 13:16:33 +02:00
shimon a1918d1512 video tests 2023-02-21 11:21:47 +02:00
shimon d26aabb083 realtime events 2023-02-20 10:14:20 +02:00
shimon ca90711c30 realtime events 2023-02-20 10:02:51 +02:00
shimon d1c0e0d9a2 realtime events 2023-02-19 16:36:11 +02:00
shimon 4ac062edfe Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.gitmodules
	Dockerfile
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/console
	composer.lock
2023-02-19 10:51:50 +02:00
shimon f71f18095f realtime events 2023-02-19 10:46:49 +02:00
shimon e2df63b78a realtime events 2023-02-14 09:53:37 +02:00
shimon 1b6adce14f realtime events 2023-02-13 20:18:33 +02:00
shimon 3d1228ba13 sync with master
and renaming attr
2023-02-12 10:56:11 +02:00
shimon edaa93936f Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	composer.json
	composer.lock
2023-02-09 19:28:26 +02:00
shimon 65a1921d48 bla 2023-02-01 10:39:54 +02:00
shimon f7a1cb14f2 bla 2023-02-01 10:38:44 +02:00
shimon f1fa22c4fc usage test 2023-01-16 10:48:26 +02:00
shimon 331c9070b3 compose.lock 2023-01-12 15:50:33 +02:00
shimon 7c81a1ea88 compose.lock 2023-01-12 15:50:03 +02:00
shimon e4972cc126 compose.lock 2023-01-11 17:14:28 +02:00
shimon 1af48bd895 Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.gitmodules
	app/config/errors.php
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	composer.json
	composer.lock
	docker-compose.yml
	src/Appwrite/Extend/Exception.php
2023-01-11 16:31:26 +02:00
shimon 403e1c2d64 compose.lock 2023-01-11 16:24:26 +02:00
shimon 2bb45c36d8 compose.lock 2023-01-11 16:23:39 +02:00
shimon 7ea695937b Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.gitmodules
	app/console
2022-11-27 20:05:28 +02:00
shimon 19388cc1a6 updates 2022-11-27 20:04:19 +02:00
shimon 7daa198f99 updates 2022-11-27 20:03:30 +02:00
shimon 89f325fe87 updates 2022-11-22 11:43:56 +02:00
shimon 19ad9914a3 updates 2022-11-22 11:15:48 +02:00
shimon 5546de13e3 aligning with master 2022-11-21 19:03:56 +02:00
shimon c55599516c Merge branch 'master' of github.com:appwrite/appwrite into __stream 2022-11-21 11:07:57 +02:00
shimon 0172a6c393 bla 2022-11-21 11:07:36 +02:00
shimon 19a38cd06a Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/workers/deletes.php
	composer.json
	composer.lock
	docker-compose.yml
2022-11-20 13:37:03 +02:00
shimon de6359cf9e audit 2022-09-29 14:46:08 +03:00
shimon 59c5c037c4 audit 2022-09-29 14:44:32 +03:00
shimon 104bd58c45 audit 2022-09-18 11:48:48 +03:00
shimon 110c38d03d Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/controllers/api/projects.php
	composer.json
	composer.lock
	phpunit.xml
2022-09-18 10:27:51 +03:00
shimon c9b0c6f906 sync with 0.16 2022-09-08 15:01:33 +03:00
shimon baea836574 sync with 0.16 2022-09-07 11:16:38 +03:00
shimon aef8c1b207 sync with 0.16 2022-09-06 18:20:51 +03:00
shimon 1aed3a1db5 Merge branch '0.16.x' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.env
	app/config/collections.php
	app/config/roles.php
	app/config/specs/open-api3-latest-client.json
	app/config/specs/open-api3-latest-console.json
	app/config/specs/open-api3-latest-server.json
	app/config/specs/swagger2-latest-client.json
	app/config/specs/swagger2-latest-console.json
	app/config/specs/swagger2-latest-server.json
	app/controllers/api/account.php
	app/controllers/api/storage.php
	app/init.php
	composer.json
	composer.lock
	docker-compose.yml
	phpunit.xml
	src/Appwrite/Extend/Exception.php
	src/Appwrite/Utopia/Response.php
2022-09-06 11:14:11 +03:00
shimon 53b020fac0 test adjustments 2022-09-06 10:59:25 +03:00
shimon 9060d86145 test adjustments 2022-09-06 09:52:10 +03:00
shimon ca0017ca40 streamId 2022-09-05 17:29:03 +03:00
shimon 115b032f3a hls-playlist 2022-09-04 09:29:56 +03:00
shimon b13c9f58cc audio tracks 2022-08-31 18:52:42 +03:00
shimon fcd29baacc m3u8 tidy 2022-08-31 10:42:20 +03:00
shimon 735072f0a4 m3u8 tidy 2022-08-31 10:42:05 +03:00
shimon b26a28d4fb Multi audio tracks 2022-08-30 12:12:29 +03:00
shimon 33e608e625 Multi audio tracks 2022-08-30 12:12:03 +03:00
shimon 6d2d115fd3 addressing comments 2022-08-25 09:56:37 +03:00
shimon ed93e54480 addressing comments 2022-08-23 17:23:29 +03:00
shimon ebe794926c addressing comments 2022-08-23 17:17:40 +03:00
59b188755c Update src/Appwrite/Resque/Worker.php
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
2022-08-23 16:10:46 +03:00
5db53f7db8 Update src/Appwrite/Resque/Worker.php
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
2022-08-23 16:05:57 +03:00
e843c0059d Update src/Appwrite/Extend/Exception.php
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
2022-08-23 16:05:47 +03:00
93d777a412 Update src/Appwrite/Event/Transcoding.php
Co-authored-by: Eldad A. Fux <eldad.fux@gmail.com>
2022-08-23 16:05:32 +03:00
shimon 9ffd52d030 tests 2022-08-22 09:58:42 +03:00
shimon d6dc025331 sync with 0.16 2022-08-17 12:20:07 +03:00
shimon b9bca60e09 Merge branch 'master' of github.com:appwrite/appwrite into __stream
 Conflicts:
	.gitignore
	app/controllers/api/teams.php
	composer.json
	composer.lock
2022-08-17 12:11:59 +03:00
shimon f2fbdf8a7c transcoding gop 2022-08-07 09:59:42 +03:00
shimon e8c96e8452 dash manifest 2022-08-05 11:38:32 +03:00
shimon 6ca361d0b0 dash subtitles 2022-08-04 14:58:25 +03:00
shimon 2f1a116e22 video scopes 2022-08-03 18:50:21 +03:00
shimon fd3a4fd485 e2e tests 2022-08-03 16:08:14 +03:00
shimon 49f8eae3f6 mpeg-dash-stream 2022-07-28 17:34:54 +03:00
shimon fe6a47252e mpeg-dash-stream 2022-07-28 17:32:14 +03:00
shimon b403a829de dash 2022-07-26 13:32:59 +03:00
shimon e232f2bbd2 dash 2022-07-26 13:31:34 +03:00
shimon 6896ff3999 restore 2022-07-25 22:31:16 +03:00
shimon 1bcd2d6bd5 draft 2022-07-25 14:37:45 +03:00
shimon 8340198077 Merge branch 'master' of github.com:appwrite/appwrite into __stream 2022-07-25 13:41:30 +03:00
shimon 5b01232ecc update 2022-07-25 11:38:53 +03:00
shimon 076f333f77 update 2022-07-25 11:37:23 +03:00
shimon fbe63a884e hls stream 2022-07-19 10:51:25 +03:00
shimon cb0ab3c823 subtitles crud 2022-07-14 18:26:56 +03:00
shimon 817793a587 subtitles crud 2022-07-14 17:54:43 +03:00
shimon 2a02784b0c profiles crud 2022-07-14 16:09:29 +03:00
shimon a7d7879d37 metadata collecting
routes renaming
permissions
2022-07-13 18:48:38 +03:00
shimon 86b7215375 metadata collecting
routes renaming
permissions
2022-07-13 18:40:30 +03:00
shimon aeebbbe91d metadata collecting
routes renaming
permissions
2022-07-13 18:38:47 +03:00
shimon 4efbc92cf2 metadata collecting
routes renaming
permissions
2022-07-13 17:28:05 +03:00
shimon 7f44f45bf6 hls master name change 2022-07-12 12:13:37 +03:00
shimon 33d942c356 hls master name change 2022-07-11 13:51:47 +03:00
Matej Baco 4089b81b3b Added labels, rebuilt specs 2022-07-11 12:37:20 +02:00
shimon 9f4ebcf208 logger moved to App::shutdown 2022-07-11 11:22:43 +03:00
shimon ec8eaa1ddd gitignore large file 2022-07-07 09:40:11 +03:00
shimon 6c7c5c1891 small fix 2022-07-06 19:12:58 +03:00
shimon d8f4d097d1 updated dependencies 2022-07-05 14:35:39 +03:00
shimon bc43989aa4 Merge branch 'master' of github.com:appwrite/appwrite into __stream 2022-07-05 14:33:28 +03:00
shimon dc38b63160 small fix 2022-07-05 14:02:23 +03:00
shimon 01eaa65d6b small fix 2022-07-05 13:57:03 +03:00
shimon 5e903daf6f small fix 2022-07-05 13:51:28 +03:00
shimon e48e1dae2c small fix 2022-07-05 11:55:49 +03:00
shimon c2e599222e big file 2022-07-03 16:59:40 +03:00
shimon bb172f8a3e refactor worker flow 2022-06-27 16:13:01 +03:00
shimon 48320d40ab Merge remote-tracking branch 'origin/__stream' into __stream
# Conflicts:
#	app/config/collections.php
#	app/controllers/api/video.php
#	app/init.php
#	app/workers/transcoding.php
#	composer.lock
#	src/Appwrite/Event/Transcoding.php
#	src/Appwrite/Utopia/Response.php
#	tests/e2e/Services/Video/VideoCustomServerTest.php
2022-06-27 16:08:17 +03:00
shimon c89b028797 refactor worker flow 2022-06-27 16:06:46 +03:00
shimon c7534760a0 refactor worker flow 2022-06-22 17:44:03 +03:00
shimon d6007b24ec refactor worker flow 2022-06-22 08:56:57 +03:00
shimon c8c282afb2 restore 2022-06-21 11:45:49 +03:00
shimon 6491cea581 restore 2022-06-21 11:45:49 +03:00
shimon 100fb73323 view template 2022-06-21 11:45:49 +03:00
shimon aa6ff513a6 restore 2022-06-20 14:44:04 +03:00
shimon 56545a7344 restore 2022-06-19 11:52:56 +03:00
shimon e97957d46e Merge branch 'master' of github.com:appwrite/appwrite into __stream 2022-06-19 10:52:30 +03:00
shimon 329c43f860 view template 2022-06-16 23:01:42 +03:00
61 changed files with 8019 additions and 353 deletions
+7 -5
View File
@@ -23,15 +23,15 @@ _APP_DB_SCHEMA=appwrite
_APP_DB_USER=user
_APP_DB_PASS=password
_APP_DB_ROOT_PASS=rootsecretpassword
_APP_STORAGE_DEVICE=Local
_APP_STORAGE_DEVICE=local
_APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
_APP_STORAGE_S3_REGION=us-east-1
_APP_STORAGE_S3_REGION=
_APP_STORAGE_S3_BUCKET=
_APP_STORAGE_DO_SPACES_ACCESS_KEY=
_APP_STORAGE_DO_SPACES_SECRET=
_APP_STORAGE_DO_SPACES_REGION=us-east-1
_APP_STORAGE_DO_SPACES_BUCKET=
_APP_STORAGE_DO_SPACES_REGION=fra1
_APP_STORAGE_DO_SPACES_BUCKET=videos-test
_APP_STORAGE_BACKBLAZE_ACCESS_KEY=
_APP_STORAGE_BACKBLAZE_SECRET=
_APP_STORAGE_BACKBLAZE_REGION=us-west-004
@@ -58,8 +58,10 @@ _APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_SMS_PROVIDER=sms://username:password@mock
_APP_SMS_FROM=+123456789
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_LIMIT=30000000000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_PHONE_PROVIDER=phone://mock
_APP_PHONE_FROM=+123456789
_APP_FUNCTIONS_SIZE_LIMIT=30000000
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_BUILD_TIMEOUT=900
+4
View File
@@ -13,3 +13,7 @@ debug/
app/sdks
dev/yasd_init.php
.phpunit.result.cache
/tests/tmp/
/tests/resources/disk-a/very-large-file-1.mov
/tests/resources/disk-a/very-big-file-2.mov
/tests/resources/disk-a/video-srt.*
+1 -1
View File
@@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = 2.3.4
branch = video-scopes-support
+10
View File
@@ -124,6 +124,13 @@ ENV _APP_SERVER=swoole \
_APP_LOGGING_PROVIDER= \
_APP_LOGGING_CONFIG=
RUN \
apk update \
&& apk add --no-cache\
mediainfo \
ffmpeg \
&& rm -rf /var/cache/apk/*
RUN \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
@@ -142,12 +149,14 @@ COPY ./src /usr/src/code/src
# Set Volumes
RUN mkdir -p /storage/uploads && \
mkdir -p /storage/videos && \
mkdir -p /storage/cache && \
mkdir -p /storage/config && \
mkdir -p /storage/certificates && \
mkdir -p /storage/functions && \
mkdir -p /storage/debug && \
chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \
chown -Rf www-data.www-data /storage/videos && chmod -Rf 0755 /storage/videos && \
chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \
chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \
chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \
@@ -176,6 +185,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-videos && \
chmod +x /usr/local/bin/worker-webhooks
# Letsencrypt Permissions
+917
View File
@@ -3251,6 +3251,17 @@ $collections = [
'default' => null,
'filters' => [],
],
[
'$id' => ID::custom('bucketInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
@@ -3580,6 +3591,912 @@ $collections = [
],
],
],
'videos' => [
'$collection' => Database::METADATA,
'$id' => 'videos',
'$name' => 'Videos',
'attributes' => [
[
'$id' => 'bucketId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'bucketInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'previewId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'previewInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'size',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'format',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'duration',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'width',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'height',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'aspectRatio',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoFormat',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoFormatProfile',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoFrameRateMode',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoFrameRate',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'audioFormat',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'audioBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'audioSampleRate',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_bucket_file',
'type' => Database::INDEX_KEY,
'attributes' => ['bucketId', 'fileId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
]
],
'videos_previews' => [
'$collection' => Database::METADATA,
'$id' => 'videos_previews',
'$name' => 'Videos_previews',
'attributes' => [
[
'$id' => 'videoId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'type',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'path',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'second',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_video_type',
'type' => Database::INDEX_UNIQUE,
'attributes' => ['videoId', 'type', 'name'],
'lengths' => [],
'orders' => [],
],
]
],
'videos_renditions' => [
'$collection' => Database::METADATA,
'$id' => 'videos_renditions',
'$name' => 'Videos_renditions',
'attributes' => [
[
'$id' => 'videoId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'profileId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'profileInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'startedAt',
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => 'endedAt',
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => 'path',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'targetDuration',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'width',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'height',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'audioBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'metadata',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 4096,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => 'status',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'progress',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'output',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_video_output_profile',
'type' => Database::INDEX_KEY,
'attributes' => ['videoId', 'output', 'profileId'],
'lengths' => [],
'orders' => [],
],
]
],
'videos_renditions_segments' => [
'$collection' => Database::METADATA,
'$id' => 'videos_rendition_segments',
'$name' => 'Videos_rendition_segments',
'attributes' => [
[
'$id' => 'renditionId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'renditionInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileName',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'path',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'duration',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'streamId',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'isInit',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_renditionId',
'type' => Database::INDEX_KEY,
'attributes' => ['renditionId'],
'lengths' => [],
'orders' => [],
],
]
],
'videos_profiles' => [
'$collection' => Database::METADATA,
'$id' => 'videos_profiles',
'$name' => 'Videos_profiles',
'attributes' => [
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'audioBitRate',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'width',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'height',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_profile',
'type' => Database::INDEX_KEY,
'attributes' => ['width', 'height', 'videoBitRate', 'audioBitRate'],
'lengths' => [],
'orders' => [],
],
]
],
'videos_subtitles' => [
'$collection' => Database::METADATA,
'$id' => 'videos_subtitles',
'$name' => 'Videos_subtitles',
'attributes' => [
[
'$id' => 'videoId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'videoInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'bucketId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'bucketInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'path',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'code',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'default',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => false,
'array' => false,
'filters' => [],
],
[
'$id' => 'targetDuration',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'status',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_bucket_video_subtitle',
'type' => Database::INDEX_KEY,
'attributes' => ['videoId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
]
],
'videos_subtitles_segments' => [
'$collection' => Database::METADATA,
'$id' => 'videos_subtitles_segments',
'$name' => 'Videos_subtitles_segments',
'attributes' => [
[
'$id' => 'subtitleId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'subtitleInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'fileName',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'path',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'duration',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_subtitleId',
'type' => Database::INDEX_KEY,
'attributes' => ['subtitleId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
]
],
];
return $collections;
+60
View File
@@ -569,6 +569,66 @@ return [
'description' => 'Domain verification for the requested domain has failed.',
'code' => 401,
],
Exception::VIDEO_PROFILE_NOT_FOUND => [
'name' => Exception::VIDEO_PROFILE_NOT_FOUND,
'description' => 'Video profile not found.',
'code' => 404,
],
Exception::VIDEO_NOT_VALID => [
'name' => Exception::VIDEO_NOT_VALID,
'description' => 'Video not valid.',
'code' => 400,
],
Exception::VIDEO_RENDITION_NOT_FOUND => [
'name' => Exception::VIDEO_RENDITION_NOT_FOUND,
'description' => 'Video rendition not found.',
'code' => 404,
],
Exception::VIDEO_SUBTITLE_NOT_FOUND => [
'name' => Exception::VIDEO_SUBTITLE_NOT_FOUND,
'description' => 'Video subtitle not found.',
'code' => 404,
],
Exception::VIDEO_LANGUAGE_CODE_NOT_VALID => [
'name' => Exception::VIDEO_LANGUAGE_CODE_NOT_VALID,
'description' => 'Video language code not valid.',
'code' => 400,
],
Exception::VIDEO_SUBTITLE_NOT_VALID => [
'name' => Exception::VIDEO_SUBTITLE_NOT_VALID,
'description' => 'Video subtitle not valid.',
'code' => 400,
],
Exception::VIDEO_SECOND_OUT_OF_RANGE => [
'name' => Exception::VIDEO_SECOND_OUT_OF_RANGE,
'description' => 'Video second out of duration range.',
'code' => 400,
],
Exception::VIDEO_NOT_FOUND => [
'name' => Exception::VIDEO_NOT_FOUND,
'description' => 'Video not found.',
'code' => 404,
],
Exception::VIDEO_PREVIEW_NOT_FOUND => [
'name' => Exception::VIDEO_PREVIEW_NOT_FOUND,
'description' => 'Video preview not found.',
'code' => 404,
],
Exception::VIDEO_TIMELINE_NOT_FOUND => [
'name' => Exception::VIDEO_TIMELINE_NOT_FOUND,
'description' => 'Video timeline not found.',
'code' => 404,
],
Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND => [
'name' => Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND,
'description' => 'Video rendition segment not found',
'code' => 404,
],
Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND => [
'name' => Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND,
'description' => 'Video subtitle segment not found',
'code' => 404,
],
Exception::DOMAIN_TARGET_INVALID => [
'name' => Exception::DOMAIN_TARGET_INVALID,
'description' => 'Your Appwrite instance is not publicly accessible. Please check the _APP_DOMAIN_TARGET environment variable of your Appwrite server.',
+29 -1
View File
@@ -236,5 +236,33 @@ return [
'update' => [
'$description' => 'This event triggers when a function is updated.',
]
]
],
'videos' => [
'$model' => Response::MODEL_VIDEO,
'$resource' => true,
'$description' => 'This event triggers on any videos event.',
'renditions' => [
'$model' => Response::MODEL_RENDITION,
'$resource' => true,
'$description' => 'This event triggers on any renditions event.',
'create' => [
'$description' => 'This event triggers when a rendition is created.',
],
'delete' => [
'$description' => 'This event triggers when a rendition is deleted.'
],
'update' => [
'$description' => 'This event triggers when a rendition is updated.'
],
],
'create' => [
'$description' => 'This event triggers when a video is created.'
],
'delete' => [
'$description' => 'This event triggers when a video is deleted.',
],
'update' => [
'$description' => 'This event triggers when a video is updated.',
]
],
];
File diff suppressed because it is too large Load Diff
+5
View File
@@ -20,6 +20,8 @@ $member = [
'avatars.read',
'execution.read',
'execution.write',
'videos.read',
'videos.write',
];
$admins = [
@@ -51,6 +53,8 @@ $admins = [
'functions.write',
'execution.read',
'execution.write',
'videos.read',
'videos.write',
];
return [
@@ -68,6 +72,7 @@ return [
'locale.read',
'avatars.read',
'execution.write',
'videos.read',
],
],
Auth::USER_ROLE_USERS => [
+6
View File
@@ -76,4 +76,10 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
'videos.read' => [
'description' => 'Access to read your project\'s video content',
],
'videos.write' => [
'description' => 'Access to create, update, and delete your project\'s video content',
]
];
+13
View File
@@ -199,4 +199,17 @@ return [
'optional' => false,
'icon' => '',
],
'videos' => [
'key' => 'videos',
'name' => 'Videos',
'subtitle' => 'Appwrite\'s Videos Endpoint',
'description' => 'Videos Endpoint',
'controller' => 'api/videos.php',
'sdk' => false,
'docs' => false,
'docsUrl' => '',
'tests' => true,
'optional' => false,
'icon' => '',
],
];
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+48
View File
@@ -0,0 +1,48 @@
<?php
return [
[
'name' => '360p',
'videoBitRate' => 890, //video BitRate in Kbps
'audioBitRate' => 64, //audio BitRate in Kbps
'width' => 640, //width resolution in px
'height' => 360, //height resolution in px
],
[
'name' => '480p',
'videoBitRate' => 2100,
'audioBitRate' => 64,
'width' => 854,
'height' => 480,
],
[
'name' => '576p',
'videoBitRate' => 2538,
'audioBitRate' => 128,
'width' => 1024,
'height' => 576,
],
[
'name' => '720p',
'videoBitRate' => 3551,
'audioBitRate' => 128,
'width' => 1280,
'height' => 720,
],
[
'name' => '1080p',
'videoBitRate' => 4800,
'audioBitRate' => 128,
'width' => 1920,
'height' => 1080,
],
[
'name' => '2160p',
'videoBitRate' => 16000,
'audioBitRate' => 356,
'width' => 4096,
'height' => 2160,
],
];
+19 -85
View File
@@ -10,8 +10,8 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
use Utopia\Validator\URL;
use Appwrite\Network\Validator\Host;
use Appwrite\Network\Validator\URL;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
@@ -64,7 +64,7 @@ App::post('/v1/account')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -144,7 +144,7 @@ App::post('/v1/account')
App::post('/v1/account/sessions/email')
->alias('/v1/account/sessions')
->desc('Create Email Session')
->groups(['api', 'account', 'auth', 'session'])
->groups(['api', 'account', 'auth'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('auth.type', 'emailPassword')
@@ -375,7 +375,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
App::get('/v1/account/sessions/oauth2/:provider/redirect')
->desc('OAuth2 Redirect')
->groups(['api', 'account', 'session'])
->groups(['api', 'account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
@@ -641,7 +641,7 @@ App::post('/v1/account/sessions/magic-url')
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->inject('request')
@@ -733,33 +733,11 @@ App::post('/v1/account/sessions/magic-url')
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire, 'project' => $project->getId()]);
$url = Template::unParseURL($url);
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $project->getAttribute('name'));
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.magicSession.subject");
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.magicSession.hello"))
->setParam('{{name}}', '')
->setParam('{{body}}', $locale->getText("emails.magicSession.body"))
->setParam('{{redirect}}', $url)
->setParam('{{footer}}', $locale->getText("emails.magicSession.footer"))
->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks"))
->setParam('{{signature}}', $locale->getText("emails.magicSession.signature"))
->setParam('{{project}}', $project->getAttribute('name'))
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000');
$body = $body->render();
$mails
->setSubject($subject)
->setBody($body)
->setFrom($from)
->setType(MAIL_TYPE_MAGIC_SESSION)
->setRecipient($user->getAttribute('email'))
->setUrl($url)
->setLocale($locale->default)
->trigger()
;
@@ -781,7 +759,7 @@ App::post('/v1/account/sessions/magic-url')
App::put('/v1/account/sessions/magic-url')
->desc('Create Magic URL session (confirmation)')
->groups(['api', 'account', 'session'])
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('audits.event', 'session.update')
@@ -918,7 +896,7 @@ App::post('/v1/account/sessions/phone')
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->inject('request')
->inject('response')
@@ -1023,7 +1001,7 @@ App::post('/v1/account/sessions/phone')
App::put('/v1/account/sessions/phone')
->desc('Create Phone Session (confirmation)')
->groups(['api', 'account', 'session'])
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('usage.metric', 'sessions.{scope}.requests.create')
@@ -1138,7 +1116,7 @@ App::put('/v1/account/sessions/phone')
App::post('/v1/account/sessions/anonymous')
->desc('Create Anonymous Session')
->groups(['api', 'account', 'auth', 'session'])
->groups(['api', 'account', 'auth'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('auth.type', 'anonymous')
@@ -2089,35 +2067,12 @@ App::post('/v1/account/recovery')
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret, 'expire' => $expire]);
$url = Template::unParseURL($url);
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.recovery.subject");
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.recovery.hello"))
->setParam('{{name}}', $profile->getAttribute('name'))
->setParam('{{body}}', $locale->getText("emails.recovery.body"))
->setParam('{{redirect}}', $url)
->setParam('{{footer}}', $locale->getText("emails.recovery.footer"))
->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks"))
->setParam('{{signature}}', $locale->getText("emails.recovery.signature"))
->setParam('{{project}}', $projectName)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000');
$body = $body->render();
$mails
->setType(MAIL_TYPE_RECOVERY)
->setRecipient($profile->getAttribute('email', ''))
->setUrl($url)
->setLocale($locale->default)
->setName($profile->getAttribute('name'))
->setBody($body)
->setFrom($from)
->setSubject($subject)
->trigger();
;
@@ -2273,32 +2228,11 @@ App::post('/v1/account/verification')
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret, 'expire' => $expire]);
$url = Template::unParseURL($url);
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.verification.subject");
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.verification.hello"))
->setParam('{{name}}', $user->getAttribute('name'))
->setParam('{{body}}', $locale->getText("emails.verification.body"))
->setParam('{{redirect}}', $url)
->setParam('{{footer}}', $locale->getText("emails.verification.footer"))
->setParam('{{thanks}}', $locale->getText("emails.verification.thanks"))
->setParam('{{signature}}', $locale->getText("emails.verification.signature"))
->setParam('{{project}}', $projectName)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000');
$body = $body->render();
$mails
->setSubject($subject)
->setBody($body)
->setFrom($from)
->setType(MAIL_TYPE_VERIFICATION)
->setRecipient($user->getAttribute('email'))
->setUrl($url)
->setLocale($locale->default)
->setName($user->getAttribute('name'))
->trigger()
;
+3 -3
View File
@@ -1,7 +1,7 @@
<?php
use Appwrite\Extend\Exception;
use Utopia\Validator\URL;
use Appwrite\Network\Validator\URL;
use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
@@ -242,8 +242,8 @@ App::get('/v1/avatars/favicon')
case 'jpeg':
$size = \explode('x', \strtolower($sizes));
$sizeWidth = (int) ($size[0] ?? 0);
$sizeHeight = (int) ($size[1] ?? 0);
$sizeWidth = (int) $size[0] ?? 0;
$sizeHeight = (int) $size[1] ?? 0;
if (($sizeWidth * $sizeHeight) >= $space) {
$space = $sizeWidth * $sizeHeight;
+16 -3
View File
@@ -169,9 +169,22 @@ App::post('/v1/projects')
$dbForProject->createCollection($key, $attributes, $indexes);
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($project, Response::MODEL_PROJECT);
if ($dbForProject->exists($dbForProject->getDefaultDatabase(), 'videos_profiles')) {
foreach (Config::getParam('videos-profiles', []) as $profile) {
Authorization::skip(function () use ($project, $profile, $dbForProject) {
return $dbForProject->createDocument('videos_profiles', new Document([
'name' => $profile['name'],
'videoBitRate' => $profile['videoBitRate'],
'audioBitRate' => $profile['audioBitRate'],
'width' => $profile['width'],
'height' => $profile['height']
]));
});
}
}
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::get('/v1/projects')
+2
View File
@@ -568,6 +568,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
'$id' => $fileId,
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getInternalId(),
'name' => $fileName,
'path' => $path,
'signature' => $fileHash,
@@ -617,6 +618,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
'$id' => ID::custom($fileId),
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getInternalId(),
'name' => $fileName,
'path' => $path,
'signature' => '',
File diff suppressed because it is too large Load Diff
-1
View File
@@ -262,7 +262,6 @@ App::init()
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
+1 -1
View File
@@ -4,7 +4,7 @@ global $utopia, $request, $response;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Validator\Host;
use Appwrite\Network\Validator\Host;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
+1 -3
View File
@@ -215,7 +215,6 @@ App::init()
if ($type === 'bucket') {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
@@ -242,14 +241,12 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
}
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($data['contentType'])
->send(base64_decode($data['payload']))
;
$route->setIsActive(false);
} else {
$response->addHeader('X-Appwrite-Cache', 'miss');
@@ -499,6 +496,7 @@ App::shutdown()
$cacheLog = $dbForProject->getDocument('cache', $key);
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
+32 -25
View File
@@ -114,6 +114,7 @@ const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_BUILDS = '/storage/builds';
const APP_STORAGE_CACHE = '/storage/cache';
const APP_STORAGE_VIDEOS = '/storage/videos';
const APP_STORAGE_CERTIFICATES = '/storage/certificates';
const APP_STORAGE_CONFIG = '/storage/config';
const APP_STORAGE_READ_BUFFER = 20 * (1000 * 1000); //20MB other names `APP_STORAGE_MEMORY_LIMIT`, `APP_STORAGE_MEMORY_BUFFER`, `APP_STORAGE_READ_LIMIT`, `APP_STORAGE_BUFFER_LIMIT`
@@ -162,6 +163,7 @@ const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
const COMPRESSION_TYPE_NONE = 'none';
const COMPRESSION_TYPE_GZIP = 'gzip';
const COMPRESSION_TYPE_ZSTD = 'zstd';
const DELETE_TYPE_VIDEOS = 'videos';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@@ -175,7 +177,7 @@ const APP_AUTH_TYPE_KEY = 'Key';
const APP_AUTH_TYPE_ADMIN = 'Admin';
// Response related
const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
const TMP_HOST = 'http://127.0.0.1/';
$register = new Registry();
App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
@@ -209,6 +211,7 @@ Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
Config::load('videos-profiles', __DIR__ . '/config/videos-profiles.php');
$user = App::getEnv('_APP_REDIS_USER', '');
$pass = App::getEnv('_APP_REDIS_PASS', '');
@@ -514,7 +517,7 @@ $register->set('logger', function () {
return new Logger($adapter);
});
$register->set('dbPool', function () {
// Register DB connection
// Register DB connection
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
@@ -523,20 +526,20 @@ $register->set('dbPool', function () {
$pool = new PDOPool(
(new PDOConfig())
->withHost($dbHost)
->withPort($dbPort)
->withDbName($dbScheme)
->withCharset('utf8mb4')
->withUsername($dbUser)
->withPassword($dbPass)
->withOptions([
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => true,
PDO::ATTR_STRINGIFY_FETCHES => true,
]),
->withHost($dbHost)
->withPort($dbPort)
->withDbName($dbScheme)
->withCharset('utf8mb4')
->withUsername($dbUser)
->withPassword($dbPass)
->withOptions([
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => true,
PDO::ATTR_STRINGIFY_FETCHES => true,
]),
64
);
@@ -555,17 +558,17 @@ $register->set('redisPool', function () {
$pool = new RedisPool(
(new RedisConfig())
->withHost($redisHost)
->withPort($redisPort)
->withAuth($redisAuth)
->withDbIndex(0),
->withHost($redisHost)
->withPort($redisPort)
->withAuth($redisAuth)
->withDbIndex(0),
64
);
return $pool;
});
$register->set('influxdb', function () {
// Register DB connection
// Register DB connection
$host = App::getEnv('_APP_INFLUXDB_HOST', '');
$port = App::getEnv('_APP_INFLUXDB_PORT', '');
@@ -579,7 +582,7 @@ $register->set('influxdb', function () {
return $client;
});
$register->set('statsd', function () {
// Register DB connection
// Register DB connection
$host = App::getEnv('_APP_STATSD_HOST', 'telegraf');
$port = App::getEnv('_APP_STATSD_PORT', 8125);
@@ -626,7 +629,7 @@ $register->set('passwordsDictionary', function () {
return $content;
});
$register->set('db', function () {
// This is usually for our workers or CLI commands scope
// This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
@@ -645,7 +648,7 @@ $register->set('db', function () {
return $pdo;
});
$register->set('cache', function () {
// This is usually for our workers or CLI commands scope
// This is usually for our workers or CLI commands scope
$redis = new Redis();
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
@@ -904,6 +907,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
$projectId = '645905a0594ef29a8680';
if ($projectId === 'console') {
return $console;
@@ -950,7 +954,6 @@ App::setResource('console', function () {
App::setResource('dbForProject', function ($db, $cache, Document $project) {
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace("_{$project->getInternalId()}");
@@ -977,6 +980,10 @@ App::setResource('deviceFiles', function ($project) {
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceVideos', function ($project) {
return getDevice(APP_STORAGE_VIDEOS . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceFunctions', function ($project) {
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
}, ['project']);
+5 -2
View File
@@ -34,8 +34,11 @@ foreach (
realpath(__DIR__ . '/../vendor/matomo'),
realpath(__DIR__ . '/../vendor/symfony'),
realpath(__DIR__ . '/../vendor/mongodb'),
realpath(__DIR__ . '/../vendor/utopia-php/websocket'), // TODO: remove workerman autoload
realpath(__DIR__ . '/../vendor/utopia-php/cache'), // TODO: remove memcache autoload
realpath(__DIR__ . '/../vendor/utopia-php/websocket'),
realpath(__DIR__ . '/../vendor/utopia-php/cache'),
realpath(__DIR__ . '/../vendor/alchemy'),
realpath(__DIR__ . '/../vendor/php-ffmpeg'),
realpath(__DIR__ . '/../vendor/utopia-php/storage'),
] as $key => $value
) {
if ($value !== false) {
+39
View File
@@ -0,0 +1,39 @@
<?php
$mpd = $this->getParam('mpd', []);
echo '<?xml version="1.0" encoding="utf-8"?>'; ?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xlink="http://www.w3.org/1999/xlink"
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
profiles="<?php if (isset($mpd['profiles'])): echo ($mpd['profiles']); endif;?>" type="<?php if (isset($mpd['type'])): echo ($mpd['type']); endif;?>" mediaPresentationDuration="<?php if (isset($mpd['maxSegmentDuration'])): echo ($mpd['mediaPresentationDuration']); endif;?>" maxSegmentDuration="<?php if (isset($mpd['maxSegmentDuration'])): echo ($mpd['maxSegmentDuration']); endif;?>" minBufferTime="<?php if (isset($mpd['minBufferTime'])): echo ($mpd['minBufferTime']); endif;?>">
<ProgramInformation></ProgramInformation>
<ServiceDescription id="0"></ServiceDescription>
<Period id="0" start="PT0.0S">
<?php foreach ($this->getParam('renditions', []) as $adaptation):
$representation = $adaptation['representation'];
?>
<AdaptationSet <?php if (isset($adaptation['id'])):?> id="<?php echo ($adaptation['id']); endif;?>"<?php if (isset($adaptation['attributes']['contentType'])): ?> contentType="<?php echo $adaptation['attributes']['contentType']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['startWithSAP'])): ?> startWithSAP="<?php echo $adaptation['attributes']['startWithSAP']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['segmentAlignment'])): ?> segmentAlignment="<?php echo $adaptation['attributes']['segmentAlignment']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['bitstreamSwitching'])): ?> bitstreamSwitching="<?php echo $adaptation['attributes']['bitstreamSwitching']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['frameRate'])): ?> frameRate="<?php echo $adaptation['attributes']['frameRate']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['maxWidth'])): ?> maxWidth="<?php echo $adaptation['attributes']['maxWidth']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['par'])): ?> par="<?php echo $adaptation['attributes']['par']; ?>"<?php endif;?><?php if (isset($adaptation['attributes']['lang'])): ?> lang="<?php echo $adaptation['attributes']['lang']; ?>"<?php endif;?>>
<Representation <?php if (isset($representation['attributes']['id'])):?> id="<?php echo $representation['attributes']['id']; ?>"<?php endif;?><?php if (isset($representation['attributes']['mimeType'])):?> mimeType="<?php echo $representation['attributes']['mimeType']; ?>"<?php endif;?><?php if (isset($representation['attributes']['codecs'])):?> codecs="<?php echo $representation['attributes']['codecs']; ?>"<?php endif;?><?php if (isset($representation['attributes']['bandwidth'])):?> bandwidth="<?php echo $representation['attributes']['bandwidth']; ?>"<?php endif;?><?php if (isset($representation['attributes']['width'])):?> width="<?php echo $representation['attributes']['width']; ?>"<?php endif;?><?php if (isset($representation['attributes']['height'])):?> height="<?php echo $representation['attributes']['height']; ?>"<?php endif;?><?php if (isset($representation['attributes']['sar'])):?> sar="<?php echo $representation['attributes']['sar']; ?>"<?php endif;?><?php if (isset($representation['attributes']['audioSamplingRate'])):?> audioSamplingRate="<?php echo $representation['attributes']['audioSamplingRate']; ?>"<?php endif;?>>
<BaseURL><?php if (isset($adaptation['baseUrl'])): echo $this->escape($adaptation['baseUrl']); endif;?></BaseURL>
<?php if (isset($adaptation['attributes']['contentType']) && $adaptation['attributes']['contentType'] === 'audio' ):?>
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
<?php endif;?>
<SegmentList<?php if (isset($representation['segmentList']['attributes']['timescale'])): ?> timescale="<?php echo $representation['segmentList']['attributes']['timescale']; ?>"<?php endif;?><?php if (isset($representation['segmentList']['attributes']['duration'])): ?> duration="<?php echo $representation['segmentList']['attributes']['duration']; ?>"<?php endif;?><?php if (isset($representation['segmentList']['attributes']['startNumber'])): ?> startNumber="<?php echo $representation['segmentList']['attributes']['startNumber']; ?>"<?php endif;?>>
<Initialization sourceURL="<?php if (isset($representation['segmentList']['init'])):?><?php echo $representation['segmentList']['init']; ?><?php endif;?>"/>
<?php if (isset($representation['segmentList']['media'])):?>
<?php foreach ($representation['segmentList']['media'] as $segment): ?>
<SegmentURL media="<?php if (isset($segment)):?><?php echo $segment; ?><?php endif;?>"/>
<?php endforeach; ?>
<?php endif;?>
</SegmentList>
</Representation>
</AdaptationSet>
<?php endforeach; ?>
<?php foreach ($this->getParam('subtitles', []) as $subtitle):?>
<AdaptationSet id="<?php if (isset($subtitle['id'])): echo $subtitle['id']; endif;?>" lang="<?php if (isset($subtitle['name'])): echo $this->escape($subtitle['name']); endif;?>" mimeType="text/vtt">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle" />
<Representation>
<BaseURL><?php if (isset($subtitle['baseUrl'])): echo $this->escape($subtitle['baseUrl']); endif;?></BaseURL>
</Representation>
</AdaptationSet>
<?php endforeach; ?>
</Period>
</MPD>
+12
View File
@@ -0,0 +1,12 @@
#EXTM3U
#EXT-X-VERSION:4
<?php foreach ($this->getParam('audios', []) as $audio):?>
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio"<?php if($audio['name'] !== null):?>,NAME="<?php echo $audio['name'];?>"<?php endif;?>,AUTOSELECT=YES<?php if($audio['language'] !== null):?>,LANGUAGE="<?php echo $audio['language'];?>"<?php endif;?><?php if($audio['uri'] !== null):?>,URI="<?php echo $audio['uri'];?>"<?php endif;?><?php echo PHP_EOL?>
<?php endforeach;?><?php echo PHP_EOL?>
<?php foreach ($this->getParam('subtitles', []) as $subtitle):?>
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="<?php echo $this->escape($subtitle['name']); ?>",DEFAULT=<?php echo $subtitle['default']; ?>,AUTOSELECT=YES,FORCED=NO,LANGUAGE="<?php echo $this->escape($subtitle['code']); ?>",URI="<?php echo $subtitle['uri']; ?>"<?php echo PHP_EOL?>
<?php endforeach;?><?php echo PHP_EOL?>
<?php foreach ($this->getParam('renditions', []) as $rendition):?>
#EXT-X-STREAM-INF:BANDWIDTH="<?php echo $rendition['bandwidth']; ?>",RESOLUTION="<?php echo $rendition['resolution']; ?>",NAME="<?php echo $rendition['name']; ?>"<?php if($rendition['codecs'] !== null):?>,CODECS="<?php echo $rendition['codecs']; ?>"<?php endif;?><?php if($rendition['subs'] !== null):?>,SUBTITLES="<?php echo $rendition['subs'];?>"<?php endif;?><?php if($rendition['audio'] !== null):?>,AUDIO="<?php echo $rendition['audio'];?>"<?php endif;?> <?php echo PHP_EOL?>
<?php echo $rendition['uri'];?><?php echo PHP_EOL?>
<?php endforeach;?>
+10
View File
@@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:<?php echo $this->getParam('targetDuration', 0)?><?php echo PHP_EOL?>
#EXT-X-MEDIA-SEQUENCE:0
<?php foreach ($this->getParam('segments', []) as $segment): ?>
#EXTINF:<?php echo $segment['duration']; ?><?php echo PHP_EOL?>
<?php echo $segment['url']; ?><?php echo PHP_EOL?>
<?php endforeach; ?>
+10
View File
@@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:<?php echo $this->getParam('targetDuration', 0)?><?php echo PHP_EOL?>
#EXT-X-MEDIA-SEQUENCE:0
<?php foreach ($this->getParam('segments', []) as $segment): ?>
#EXTINF:<?php echo $segment['duration']; ?>,<?php echo PHP_EOL?>
<?php echo $segment['url']; ?><?php echo PHP_EOL?>
<?php endforeach; ?>
#EXT-X-ENDLIST
+54 -2
View File
@@ -64,6 +64,9 @@ class DeletesV1 extends Worker
case DELETE_TYPE_BUCKETS:
$this->deleteBucket($document, $project->getId());
break;
case DELETE_TYPE_VIDEOS:
$this->deleteVideo($document, $project->getId());
break;
default:
if (\str_starts_with($document->getCollection(), 'database_')) {
$this->deleteCollection($document, $project->getId());
@@ -776,9 +779,58 @@ class DeletesV1 extends Worker
{
$dbForProject = $this->getProjectDB($projectId);
$dbForProject->deleteCollection('bucket_' . $document->getInternalId());
$device = $this->getFilesDevice($projectId);
$device->deletePath($document->getId());
}
/**
* @param Document $document database document
* @param string $projectId
*/
protected function deleteVideo(Document $document, string $projectId): void
{
$videoId = $document->getId();
$dbForProject = $this->getProjectDB($projectId);
$previews = $dbForProject->find('videos_previews', [
new Query('videoId', Query::TYPE_EQUAL, [$videoId])
]);
foreach ($previews as $preview) {
$dbForProject->deleteDocument('videos_previews', $preview->getId());
}
$renditions = $dbForProject->find('videos_renditions', [
new Query('videoId', Query::TYPE_EQUAL, [$videoId])
]);
foreach ($renditions as $rendition) {
$this->deleteByGroup('videos_renditions_segments', [
new Query('renditionId', Query::TYPE_EQUAL, [$rendition->getId()])
], $dbForProject);
$dbForProject->deleteDocument('videos_renditions', $rendition->getId());
}
$subtitles = $dbForProject->find('videos_subtitles', [
new Query('videoId', Query::TYPE_EQUAL, [$videoId])
]);
foreach ($subtitles as $subtitle) {
$segments = $dbForProject->find('videos_subtitles_segments', [
new Query('subtitleId', Query::TYPE_EQUAL, [$subtitle->getId()])
]);
foreach ($segments as $segment) {
$dbForProject->deleteDocument('videos_subtitles_segments', $segment->getId());
}
$dbForProject->deleteDocument('videos_subtitles', $subtitle->getId());
}
$videosDevice = $this->getVideoDevice($projectId);
$videosPath = $videosDevice->getPath($videoId);
if ($videosDevice->deletePath($videosPath)) {
Console::success('Deleted video directory: ' . $videosPath);
} else {
Console::error('Failed to delete video directory: ' . $videosPath);
}
}
}
+863
View File
@@ -0,0 +1,863 @@
<?php
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Resque\Worker;
use Streaming\FFMpeg;
use FFMpeg\FFProbe;
use Streaming\Format\StreamFormat;
use Streaming\HLSSubtitle;
use Streaming\Media;
use Streaming\Representation;
use Mhor\MediaInfo\MediaInfo;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Storage\Compression\Algorithms\Zstd;
use Captioning\Format\SubripFile;
use Utopia\Storage\Device\Local;
require_once __DIR__ . '/../init.php';
Console::title('Video V1 Worker');
Console::success(APP_NAME . ' video worker v1 has started');
class VideosV1 extends Worker
{
/**
* Rendition Status
*/
private const STATUS_START = 'started';
private const STATUS_END = 'ended';
private const STATUS_UPLOADING = 'uploading';
private const STATUS_READY = 'ready';
private const STATUS_ERROR = 'error';
private const OUTPUT_HLS = 'hls';
private const OUTPUT_DASH = 'dash';
private string $basePath = '/tmp/';
//private string $basePath = '/usr/src/code/tests/tmp/';
private string $inDir;
private string $inPath;
private string $outDir;
private string $outPath;
private string $renditionName;
private Database $database;
private Document $video;
private string $action;
private Document $profile;
private Document $project;
private Document $file;
private Document $bucket;
private FFProbe $ffprobe;
private FFMpeg $ffmpeg;
private bool $isVideo = false;
public function getName(): string
{
return "Video v1";
}
public function init(): void
{
$this->video = new Document($this->args['video'] ?? []);
$this->profile = new Document($this->args['profile'] ?? []);
$this->project = new Document($this->args['project'] ?? []);
$this->action = $this->args['action'];
$this->basePath .= uniqid();
$this->inDir = $this->basePath . '/in/';
$this->outDir = $this->basePath . '/out/';
@mkdir($this->inDir, 0755, true);
@mkdir($this->outDir, 0755, true);
$this->outPath = $this->outDir . $this->video->getId();
}
public function run(): void
{
$this->database = $this->getProjectDB($this->project->getId());
$this->bucket = $this->database->getDocument('buckets', $this->video->getAttribute('bucketId'));
$this->file = $this->database->getDocument('bucket_' . $this->bucket->getInternalId(), $this->video->getAttribute('fileId'));
$path = basename($this->file->getAttribute('path'));
$this->inPath = $this->inDir . $path;
/**
* Write original asset to tmp
*/
$result = $this->write($this->project, $this->file);
if (empty($result)) {
console::error('Storage transfer error');
}
$this->renditionName = $this->profile->getAttribute('width')
. 'X' . $this->profile->getAttribute('height')
. '@' . ($this->profile->getAttribute('videoBitRate') + $this->profile->getAttribute('audioBitRate'));
/**
* FFMpeg init
*/
$this->ffprobe = FFProbe::create();
$this->ffmpeg = FFMpeg::create([
'timeout' => 0,
'ffmpeg.threads' => 4
]);
if (!$this->ffprobe->isValid($this->inPath)) {
console::error('Not an valid Video file "' . $this->inPath . '"');
}
if (empty($this->video->getAttribute('duration'))) {
/**
* Original asset metadata
*/
$mediaInfo = new MediaInfo();
$mediaInfoContainer = $mediaInfo->getInfo($this->inPath);
$general = $mediaInfoContainer->getGeneral();
$this->video
->setAttribute('duration', $general->has('duration') ? $general->get('duration')->getMilliseconds() : 0)
->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : '');
foreach ($mediaInfoContainer->getVideos() ?? [] as $video) {
$this->isVideo = true;
$this->video
->setAttribute('height', $video->has('height') ? $video->get('height')->getAbsoluteValue() : 0)
->setAttribute('width', $video->has('width') ? $video->get('width')->getAbsoluteValue() : 0)
->setAttribute('aspectRatio', $video->has('display_aspect_ratio') ? $video->get('display_aspect_ratio')->getTextValue() : '')
->setAttribute('videoFormat', $video->has('format') ? $video->get('format')->getShortName() : '')
->setAttribute('videoFormatProfile', $video->has('format_profile') ? $video->get('format_profile') : '')
->setAttribute('videoFrameRate', $video->has('frame_rate') ? strval($video->get('frame_rate')->getAbsoluteValue()) : '')
->setAttribute('videoFrameRateMode', $video->has('frame_rate_mode') ? $video->get('frame_rate_mode')->getFullName() : '')
->setAttribute('videoBitRate', $video->has('bit_rate') ? $video->get('bit_rate')->getAbsoluteValue() : 0);
}
foreach ($mediaInfoContainer->getAudios() ?? [] as $audio) {
$this->video
->setAttribute('audioFormat', $audio->has('format') ? strval($audio->get('format')->getShortName()) : '')
->setAttribute('audioSampleRate', $audio->has('sampling_rate') ? strval($audio->get('sampling_rate')->getAbsoluteValue()) : '')
->setAttribute('audioBitRate', $audio->has('bit_rate') ? $audio->get('bit_rate')->getAbsoluteValue() : 0);
}
console::info('Input video id: ' . $this->video->getId() . PHP_EOL .
'Input name: ' . $this->file->getAttribute('name') . PHP_EOL .
'Input width: ' . $this->video->getAttribute('width') . ' px' . PHP_EOL .
'Input height: ' . $this->video->getAttribute('height') . ' px' . PHP_EOL .
'Input duration: ' . ($this->video->getAttribute('duration') / 1000) . ' Sec' . PHP_EOL .
'Input size: ' . ($this->video->getAttribute('size') / 1024 / 1024) . ' MiB' . PHP_EOL .
'Input videoBitRate: ' . ($this->video->getAttribute('videoBitRate') / 1000) . ' kb/s' . PHP_EOL .
'Input audioBitRate: ' . ($this->video->getAttribute('audioBitRate') / 1000) . ' kb/s' . PHP_EOL);
$this->database->updateDocument(
'videos',
$this->video->getId(),
new document(
array_filter((array)$this->video, fn($value) => !is_null($value))
),
);
}
$this->video->getAttribute('videoBitRate');
$media = $this->ffmpeg->open($this->inPath);
match ($this->action) {
'preview' => $this->createPreview($media),
'timeline' => $this->createTimeLine($media),
default => $this->createOutput($media),
};
unset($media);
}
/**
* @throws Authorization
* @throws Throwable
* @throws Structure
*/
private function createPreview($media): void
{
if (!$this->isVideo) {
return;
}
console::info('Creating preview image from second ' . $this->args['second']);
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/preview/';
$name = 'preview.jpg';
$media
->filters()
->resize(new \FFMpeg\Coordinate\Dimension($this->video->getAttribute('width'), $this->video->getAttribute('height')))
->synchronize();
$media
->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($this->args['second']))
->save($this->outDir . $name);
/**
* Upload preview
*/
console::info('Uploading ' . $name);
$this->getVideoDevice($this->project->getId())->write(
$path . $name,
(new Local('/'))->read($this->outDir . $name),
mime_content_type($this->outDir . $name)
);
$preview = $this->database->findOne('videos_previews', [
Query::equal('videoInternalId', [$this->video->getInternalId()]),
Query::equal('type', [$this->action]),
Query::equal('name', [$name]),
]);
if (empty($preview)) {
$preview = $this->database->createDocument('videos_previews', new Document([
'videoId' => $this->video->getId(),
'videoInternalId' => $this->video->getInternalId(),
'type' => $this->action,
'name' => $name,
'path' => $path,
'second' => $this->args['second'],
]));
$this->video->setAttribute('previewId', $preview->getId());
$this->video->setAttribute('previewInternalId', $preview->getInternalId());
$this->database->updateDocument(
'videos',
$this->video->getId(),
new document(
array_filter((array)$this->video, fn($value) => !is_null($value))
),
);
} else {
$this->database->updateDocument(
'videos_previews',
$preview->getId(),
$preview->setAttribute('second', $this->args['second'])
);
/**
* Clean preview cache
*/
(new Delete())
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
->setResource('preview/' . $preview->getId())
->trigger();
}
}
/**
* @throws Authorization
* @throws Structure
* @throws Throwable
*/
private function createTimeLine($media): void
{
/*
* Title: PlayerJS Thumbnails & WebVTT Creator
* URI: https://playerjs.com/docs/q=thumbnailsphpwebvtt
* Version: 1.0
* Author: Playerjs.com
* Author URI: https://playerjs.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: playerjs
*/
if (!$this->isVideo) {
return;
}
$interval = 2;
$ranges = [
['from' => 120, 'to' => 600, 'interval' => 5],
['from' => 600, 'to' => 1800, 'interval' => 10],
['from' => 1800, 'to' => 3600, 'interval' => 20],
['from' => 3600, 'to' => 99999, 'interval' => 30],
];
foreach ($ranges as $range) {
if (
$this->video->getAttribute('duration') > $range['from'] &&
$this->video->getAttribute('duration') <= $range['to']
) {
$interval = $range['interval'];
break;
}
}
$timeline['aspect'] = $this->video->getAttribute('width') / $this->video->getAttribute('height');
$timeline['width'] = 160;
$timeline['height'] = round($timeline['width'] / $timeline['aspect']);
$timeline['size'] = '5x5';
$cmd = [
'/usr/bin/ffmpeg',
'-i ' . $this->inPath,
'-hide_banner',
'-loglevel error',
'-vsync vfr',
'-vf \'select=isnan(prev_selected_t)+gte(t-prev_selected_t\,' . $interval . '),scale=' . $timeline['width'] . ':' . $timeline['height'] . ',tile=' . $timeline['size'] . '\'',
'-qscale:v 3',
$this->outDir . 'sprite%d.jpg',
];
$result = shell_exec(implode(" ", $cmd));
/**
* Vtt creation*
*/
if ($result !== false) {
$size = explode('x', $timeline['size']);
$counter = 0;
$images = ceil((($this->video->getAttribute('duration') / 1000) / $interval) / ($size[0] * $size[1]));
$data = "WEBVTT";
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/timeline/';
for ($image = 1; $image <= $images; $image++) {
$sprite = $this->database->createDocument('videos_previews', new Document([
'videoId' => $this->video->getId(),
'videoInternalId' => $this->video->getInternalId(),
'type' => 'sprite',
'name' => 'sprite' . $image . '.jpg',
'path' => $path,
]));
$url = TMP_HOST . 'v1/videos/' . $this->video->getId() . '/preview/' . $sprite->getId() . '/';
for ($col = 0; $col < $size[0]; $col++) {
for ($row = 0; $row < $size[1]; $row++) {
$data .= "\n" . gmdate("H:i:s", $counter * $interval) . " --> " . gmdate("H:i:s", ($counter + 1) * $interval) . "\n" . $url . "#xywh=" . ($row * $timeline['width']) . "," . ($col * $timeline['height']) . "," . $timeline['width'] . "," . $timeline['height'];
$counter++;
}
}
}
if ($counter > 0) {
/**
* Upload vtt
*/
$this->getVideoDevice($this->project->getId())->write(
$this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/timeline/timeline.vtt'),
$data,
'text/vtt'
);
console::info('Uploading timeline vtt');
/**
* Upload sprites*
*/
$dir = new DirectoryIterator($this->outDir);
foreach ($dir as $fileinfo) {
if (!$fileinfo->isDot()) {
console::info('Uploading ' . $fileinfo->getFilename());
$this->getVideoDevice($this->project->getId())->write(
$path . $fileinfo->getFilename(),
(new Local('/'))->read($this->outDir . $fileinfo->getFilename()),
mime_content_type($this->outDir . $fileinfo->getFilename())
);
}
}
}
}
}
/**
* @throws Authorization
* @throws Exception
* @throws Throwable
* @throws Structure
*/
private function createOutput($media): void
{
$subs = [];
$subtitles = $this->database->find('videos_subtitles', [
Query::equal('videoInternalId', [$this->video->getInternalId()]),
Query::equal('status', [''])
]);
foreach ($subtitles as $subtitle) {
$subtitle->setAttribute('status', self::STATUS_START);
$this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle);
$bucket = $this->database->getDocument('buckets', $subtitle->getAttribute('bucketId'));
$file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId'));
$path = basename($file->getAttribute('path'));
$this->write($this->project, $file);
console::info('Transferring subtitle from storage to ' . $this->inDir);
$ext = pathinfo($path, PATHINFO_EXTENSION);
$subtitlePath = $this->inDir . $subtitle->getId() . '.vtt';
if ($ext === 'srt') {
$srt = new SubripFile($this->inDir . $path);
$srt->convertTo('webvtt')->save($subtitlePath);
}
$subs[] = [
'name' => $subtitle->getAttribute('name'),
'code' => $subtitle->getAttribute('code'),
'path' => $subtitlePath,
];
}
$query = $this->database->createDocument('videos_renditions', new Document([
'videoId' => $this->video->getId(),
'videoInternalId' => $this->video->getInternalId(),
'profileId' => $this->profile->getId(),
'profileInternalId' => $this->profile->getInternalId(),
'name' => $this->renditionName,
'startedAt' => DateTime::now(),
'status' => self::STATUS_START,
'width' => $this->profile->getAttribute('width'),
'height' => $this->profile->getAttribute('height'),
'videoBitRate' => $this->profile->getAttribute('videoBitRate'),
'audioBitRate' => $this->profile->getAttribute('audioBitRate'),
'output' => $this->args['output'],
]));
$this->send($query);
$renditionRootPath = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/';
$renditionPath = $renditionRootPath . $this->renditionName . '-' . $query->getId() . '/';
try {
$representation = (new Representation())
->setKiloBitRate($this->profile->getAttribute('videoBitRate'))
->setAudioKiloBitRate($this->profile->getAttribute('audioBitRate'))
->setResize($this->profile->getAttribute('width'), $this->profile->getAttribute('height'));
console::info('Output video id:' . $this->video->getId() . PHP_EOL .
'Output name: ' . $this->file->getAttribute('name') . PHP_EOL .
'Output width: ' . $this->profile->getAttribute('width') . PHP_EOL .
'Output height: ' . $this->profile->getAttribute('height') . PHP_EOL .
'Output video BitRate:' . $this->profile->getAttribute('videoBitRate') . PHP_EOL .
'Output audio BitRate:' . $this->profile->getAttribute('audioBitRate') . PHP_EOL .
'Output:' . $this->args['output'] . PHP_EOL);
$format = new Streaming\Format\X264();
$format->on('progress', function ($media, $format, $percentage) use ($query) {
if ($percentage % 3 === 0) {
$query->setAttribute('progress', (string)$percentage);
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
$this->send($query, 'update');
}
});
$this->transcode($media, $format, $representation, $subs);
//exec('/usr/bin/ffmpeg -y -i /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/in/637f59ab5bce0e36d05e.mp4 -c:v libx264 -c:a aac -bf 1 -keyint_min 25 -g 250 -sc_threshold 40 -use_timeline 0 -use_template 0 -seg_duration 10 -hls_playlist 0 -f dash -dn -sn -vf scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1 -b_strategy 1 -bf 3 -force_key_frames "expr:gte(t,n_forced*2)" -map 0 -s:v:0 1024x576 -b:v:0 2538k -b:a:0 128k -strict -2 -threads 12 /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/out/637f59c88f9ff0fe3b1f.mpd2>&1', $o, $v);
//var_dump($o);
//var_dump($v);
if ($this->args['output'] === self::OUTPUT_HLS) {
$streams = $this->getHlsSegmentsUrls($this->outDir . 'master.m3u8');
foreach ($streams as $stream) {
$m3u8 = $this->getSegments($this->outDir . $stream['path']);
if (!empty($m3u8['segments'])) {
foreach ($m3u8['segments'] as $segment) {
$this->database->createDocument('videos_renditions_segments', new Document([
'renditionId' => $query->getId(),
'renditionInternalId' => $query->getInternalId(),
'streamId' => (int)$stream['id'],
'fileName' => $segment['fileName'],
'path' => $renditionPath,
'duration' => $segment['duration'],
]));
}
}
$query->setAttribute('metadata', json_encode(['hls' => $streams]));
$query->setAttribute('targetDuration', $m3u8['targetDuration']);
}
} else {
$mpd = $this->getSegments($this->outPath . '.mpd');
if (!empty($mpd['segments'])) {
foreach ($mpd['segments'] as $segment) {
$this->database->createDocument('videos_renditions_segments', new Document([
'renditionId' => $query->getId(),
'renditionInternalId' => $query->getInternalId(),
'streamId' => $segment['streamId'],
'fileName' => $segment['fileName'],
'path' => $renditionPath,
'isInit' => $segment['isInit'],
]));
}
}
if (!empty($mpd['metadata'])) {
$query->setAttribute('metadata', json_encode(['mpd' => $mpd['metadata']]));
}
}
$query->setAttribute('status', self::STATUS_END);
$query->setAttribute('endedAt', DateTime::now());
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
$this->send($query, 'update');
foreach ($subtitles ?? [] as $subtitle) {
if ($this->args['output'] === self::OUTPUT_HLS) {
$m3u8 = $this->getSegments($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8');
foreach ($m3u8['segments'] ?? [] as $segment) {
$this->database->createDocument('videos_subtitles_segments', new Document([
'subtitleId' => $subtitle->getId(),
'subtitleInternalId' => $subtitle->getInternalId(),
'fileName' => $segment['fileName'],
'path' => $renditionRootPath . 'subtitles/',
'duration' => $segment['duration'],
]));
}
$subtitle->setAttribute('targetDuration', $m3u8['targetDuration']);
} else {
$this->getFilesDevice($this->project->getId())->transfer($this->inDir . $subtitle->getId() . '.vtt', $this->outDir . $subtitle->getId() . '.vtt', $this->getFilesDevice($this->project->getId()));
}
$subtitle->setAttribute('status', self::STATUS_READY);
$subtitle->setAttribute('path', $renditionRootPath . 'subtitles/');
$this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle);
}
console::info('Rendition ' . $query->getId() . ' conversion, done');
/** Upload **/
$dir = new DirectoryIterator($this->outDir);
foreach ($dir as $fileinfo) {
if (!$fileinfo->isDot()) {
$data = (new Local('/'))->read($this->outDir . $fileinfo->getFilename());
$to = $renditionPath;
if (str_contains($fileinfo->getFilename(), "_subtitles_") || str_contains($fileinfo->getFilename(), ".vtt")) {
$to = $renditionRootPath . 'subtitles/';
}
console::info('Uploading ' . $fileinfo->getFilename());
$this->getVideoDevice($this->project->getId())->write(
$to . $fileinfo->getFilename(),
$data,
mime_content_type($this->outDir . $fileinfo->getFilename())
);
if ($fileinfo->key() === 0) {
$query->setAttribute('progress', '100');
$query->setAttribute('status', self::STATUS_UPLOADING);
$query->setAttribute('path', $renditionPath);
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
$this->send($query, 'update');
}
}
}
$query->setAttribute('status', self::STATUS_READY);
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
$this->send($query, 'update');
} catch (\Throwable $th) {
$query->setAttribute('metadata', json_encode([
'code' => $th->getCode(),
'message' => substr($th->getMessage(), 0, 255),
]));
$query->setAttribute('status', self::STATUS_ERROR);
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
$this->send($query, 'update');
console::error('Error video id:' . $this->video->getId() . PHP_EOL
. 'Message: ' . $th->getMessage() . PHP_EOL
. 'File: ' . $th->getFile() . PHP_EOL
. 'line: ' . $th->getLine() . PHP_EOL);
}
}
/**
* @param $media Media
* @param $format StreamFormat
* @param $representation Representation
* @param array $subtitles
* @return void
*/
private function transcode(Media $media, StreamFormat $format, Representation $representation, array $subtitles): void
{
$additionalParams = [
'-dn',
'-sn',
'-vf', 'scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1', //keep frame size
'-crf', '22', // constant rate factor
'-bf', '3', // bframe
'-force_key_frames', 'expr:gte(t,n_forced*2)' //enforce strict key frame
];
$segmentSize = 6;
if ($this->args['output'] === self::OUTPUT_DASH) {
$media->dash()
->setFormat($format)
->setSegDuration($segmentSize)
->addRepresentation($representation)
->setAdditionalParams($additionalParams)
->save($this->outPath);
return;
}
$hls = $media->hls();
foreach ($subtitles as $subtitle) {
$sub = new HLSSubtitle($subtitle['path'], $subtitle['name'], $subtitle['code']);
$sub->default();
$hls->subtitle($sub);
}
$hls->setFormat($format)
->setHlsTime($segmentSize)
->addRepresentation($representation)
->setAdditionalParams($additionalParams)
->save($this->outPath);
}
/**
* @param string $path
* @return array
*/
private function getSegments(string $path): array
{
$segments = [];
if ($this->args['output'] === self::OUTPUT_DASH) {
$metadata = null;
$handle = fopen($path, "r");
if ($handle) {
$streamId = -1;
while (($line = fgets($handle)) !== false) {
$line = str_replace([",", "\r", "\n"], "", $line);
if (str_contains($line, "<AdaptationSet")) {
$streamId++;
}
if (!str_contains($line, "SegmentURL") && !str_contains($line, "Initialization")) {
$metadata .= $line . PHP_EOL;
} else {
$segments[] = [
'isInit' => str_contains($line, "Initialization") ? 1 : 0,
'streamId' => $streamId,
'fileName' => trim(str_replace(["<SegmentURL media=\"", "<Initialization sourceURL=\"", "\"/>", "\" />"], "", $line)),
];
}
}
fclose($handle);
}
return [
'metadata' => $metadata,
'segments' => $segments
];
}
$targetDuration = 0;
$handle = fopen($path, "r");
if ($handle) {
while (($line = fgets($handle)) !== false) {
$line = str_replace([",","\r","\n"], "", $line);
if (str_contains($line, "#EXT-X-TARGETDURATION")) {
$targetDuration = str_replace(["#EXT-X-TARGETDURATION:"], "", $line);
}
if (str_contains($line, "#EXTINF")) {
$duration = str_replace(["#EXTINF:"], "", $line);
}
if (str_contains($line, ".ts") || str_contains($line, ".vtt")) {
if (!empty($duration)) {
$segments[] = [
'fileName' => $line,
'duration' => $duration
];
$duration = null;
}
}
}
fclose($handle);
}
return [
'targetDuration' => $targetDuration,
'segments' => $segments
];
}
/**
* @param string $path
* @return array
*/
private function getHlsSegmentsUrls(string $path): array
{
$files = [];
$handle = fopen($path, "r");
if ($handle) {
while (($line = fgets($handle)) !== false) {
$tmp = [];
$line = str_replace(['"'], '', $line);
$attributes = explode(',', $line);
foreach ($attributes as $key => $attribute) {
$parts = explode('=', $attribute);
switch (true) {
case str_contains($parts[0], 'LANGUAGE'):
$attr['language'] = $parts[1];
break;
case str_contains($parts[0], 'BANDWIDTH'):
$attr['bandwidth'] = $parts[1];
break;
case str_contains($parts[0], 'RESOLUTION'):
$attr['resolution'] = $parts[1];
break;
case str_contains($parts[0], 'CODECS'):
$attr['codecs'] = trim($parts[1], " \n\r\t\v\x00");
if (!empty($attributes[$key + 1])) {
$attr['codecs'] .= ',' . trim($attributes[$key + 1], " \n\r\t\v\x00");
}
break;
}
}
$end = strpos($line, 'm3u8');
if ($end !== false) {
$start = strpos($line, $this->video->getId());
if ($start !== false) {
$path = substr($line, $start, ($end - $start) + 4);
$parts = explode('_', $path);
$tmp = [
'id' => $parts[1],
'path' => $path
];
if (str_contains($line, "TYPE=AUDIO")) {
$tmp['type'] = 'audio';
if (!empty($attr['language'])) {
$tmp['language'] = $attr['language'];
}
} else {
$tmp['type'] = 'video';
if (!empty($attr['resolution'])) {
$tmp['resolution'] = $attr['resolution'];
}
if (!empty($attr['bandwidth'])) {
$tmp['bandwidth'] = $attr['bandwidth'];
}
if (!empty($attr['codecs'])) {
$tmp['codecs'] = $attr['codecs'];
}
}
}
$files[] = $tmp;
}
}
fclose($handle);
}
return $files;
}
/**
* Send realtime updates
*
* @param string $action
* @param Document $payload
* @throws Exception
*/
private function send(Document $payload, string $action = 'create')
{
unset($payload['metadata']);
$allEvents = Event::generateEvents('videos.[videoId].renditions.[renditionId].' . $action, [
'videoId' => $payload['videoId'],
'renditionId' => $payload->getId()
]);
$payload->setAttribute('$permissions', $this->bucket->getAttribute('fileSecurity', false)
? \array_merge($this->bucket->getAttribute('$permissions'), $this->file->getAttribute('$permissions'))
: $this->bucket->getAttribute('$permissions'));
$target = Realtime::fromPayload(
event: $allEvents[0],
payload: $payload
);
Realtime::send(
projectId: 'console',
payload: $payload->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
/**
* Write files
*
* @param $project Document
* @param $file Document
* @return boolean
* @throws \Exception
*/
private function write(Document $project, Document $file): bool
{
console::info('Writing file to ' . $this->inDir);
$fullPath = $file->getAttribute('path');
$path = basename($file->getAttribute('path'));
$local = new Local('/');
if (
!empty($file->getAttribute('openSSLCipher')) ||
$file->getAttribute('algorithm', 'none') !== 'none'
) {
$data = $this->getFilesDevice($project->getId())->read($fullPath);
if (!empty($file->getAttribute('openSSLCipher'))) {
$data = OpenSSL::decrypt(
$data,
$file->getAttribute('openSSLCipher'),
App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
0,
\hex2bin($file->getAttribute('openSSLIV')),
\hex2bin($file->getAttribute('openSSLTag'))
);
}
$algorithm = $file->getAttribute('algorithm', 'none');
switch ($algorithm) {
case 'zstd':
$compressor = new Zstd();
$data = $compressor->decompress($data);
break;
case 'gzip':
$compressor = new GZIP();
$data = $compressor->decompress($data);
break;
}
$result = $local
->write($this->inDir . $path, $data, $file->getAttribute('mimeType'));
} else {
$result = $this->getFilesDevice(
$project->getId()
)->transfer($fullPath, $this->inDir . $path, $local);
}
return $result;
}
private function cleanup(): bool
{
$stdout = '';
$stderr = '';
$stdin = '';
return Console::execute("rm -rf {$this->basePath}", $stdin, $stdout, $stderr, 3) === 0;
}
public function shutdown(): void
{
$result = $this->cleanup();
if (!$result) {
Console::error('Failed Removing files from [' . $this->basePath . ']');
}
Console::info('Removing files from [' . $this->basePath . ']');
}
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=0.1 QUEUE='v1-videos' APP_INCLUDE='/usr/src/code/app/workers/videos.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
+12 -4
View File
@@ -41,6 +41,7 @@
"ext-openssl": "*",
"ext-zlib": "*",
"ext-sockets": "*",
"ext-fileinfo": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.25.*",
@@ -59,7 +60,7 @@
"utopia-php/orchestration": "0.6.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "0.14.*",
"utopia-php/storage": "dev-feat-transfer as 0.13.2",
"utopia-php/swoole": "0.5.*",
"utopia-php/websocket": "0.1.*",
"resque/php-resque": "1.3.6",
@@ -70,17 +71,24 @@
"chillerlan/php-qrcode": "4.3.3",
"adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0",
"webonyx/graphql-php": "14.11.*"
"aminyazdanpanah/php-ffmpeg-video-streaming": "dev-master",
"captioning/captioning": "2.*",
"ext-simplexml": "*",
"webonyx/graphql-php": "14.11.*",
"mhor/php-mediainfo": "5.4.3"
},
"repositories": [
{
"url": "https://github.com/appwrite/runtimes.git",
"type": "git"
},
{
"url": "https://github.com/shimonewman/PHP-FFmpeg-video-streaming.git",
"type": "git"
}
],
"require-dev": {
"appwrite/sdk-generator": "0.32.*",
"ext-fileinfo": "*",
"appwrite/sdk-generator": "dev-shimon-video-test",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.6",
"swoole/ide-helper": "4.8.9",
Generated
+1213 -209
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -73,6 +73,7 @@ services:
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-videos:/storage/videos:rw
- appwrite-cache:/storage/cache:rw
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -84,6 +85,9 @@ services:
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
- ./dev:/usr/local/dev
- ./tests/tmp:/tmp
- ./vendor/utopia-php/storage:/usr/src/code/vendor/utopia-php/storage
depends_on:
- mariadb
- redis
@@ -225,6 +229,65 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-videos:
entrypoint: worker-videos
<<: *x-logging
container_name: appwrite-worker-videos
build:
context: .
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- appwrite-uploads:/storage/uploads:rw
- appwrite-videos:/storage/videos:rw
#- ./tests:/usr/src/code/tests #TODO remove when done!!
- ./vendor/aminyazdanpanah/php-ffmpeg-video-streaming:/usr/src/code/vendor/aminyazdanpanah/php-ffmpeg-video-streaming #TODO remove when done!!
- ./vendor/php-ffmpeg/php-ffmpeg:/usr/src/code/vendor/php-ffmpeg/php-ffmpeg #TODO remove when done!!
- ./vendor/alchemy/binary-driver:/usr/src/code/vendor/alchemy/binary-driver
- ./vendor/utopia-php/storage:/usr/src/code/vendor/utopia-php/storage
#- ./vendor/symfony/process:/usr/src/code/vendor/symfony/process
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-audits:
entrypoint: worker-audits
<<: *x-logging
@@ -290,6 +353,7 @@ services:
- mariadb
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-videos:/storage/videos:rw
- appwrite-cache:/storage/cache:rw
- appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw
@@ -846,6 +910,7 @@ volumes:
appwrite-redis:
appwrite-cache:
appwrite-uploads:
appwrite-videos:
appwrite-certificates:
appwrite-functions:
appwrite-builds:
+1
View File
@@ -32,6 +32,7 @@
<directory>./tests/e2e/Services/Teams</directory>
<directory>./tests/e2e/Services/Users</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Videos</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
+3
View File
@@ -32,6 +32,9 @@ class Event
public const BUILDS_QUEUE_NAME = 'v1-builds';
public const BUILDS_CLASS_NAME = 'BuildsV1';
public const VIDEO_QUEUE_NAME = 'v1-videos';
public const VIDEO_CLASS_NAME = 'VideosV1';
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace Appwrite\Event;
use Resque;
use Utopia\Database\Document;
class Video extends Event
{
protected ?Document $video = null;
protected ?Document $profile = null;
protected int $second = 5;
protected string $output = '';
protected string $action = '';
public function __construct()
{
parent::__construct(Event::VIDEO_QUEUE_NAME, Event::VIDEO_CLASS_NAME);
}
/**
* Sets output.
*
* @param string $output
* @return self
*/
public function setOutput(string $output): self
{
$this->output = $output;
return $this;
}
/**
* Returns output.
*
* @return string
*/
public function getOutput(): string
{
return $this->output;
}
/**
* Sets action.
*
* @param string $action
* @return self
*/
public function setAction(string $action): self
{
$this->action = $action;
return $this;
}
/**
* Returns action.
*
* @return string
*/
public function getAction(): string
{
return $this->action;
}
/**
* Sets video.
*
* @param Document $video
* @return self
*/
public function setVideo(Document $video): self
{
$this->video = $video;
return $this;
}
/**
* Returns video.
*
* @return null|Document
*/
public function getVideo(): ?string
{
return $this->video;
}
/**
* Sets profile.
*
* @param Document $profile
* @return self
*/
public function setProfile(Document $profile): self
{
$this->profile = $profile;
return $this;
}
/**
* Returns profile.
*
* @return null|Document
*/
public function getProfile(): ?Document
{
return $this->profile;
}
/**
* Sets second from duration.
*
* @param int $second
* @return self
*/
public function setSecond(int $second): self
{
$this->second = $second;
return $this;
}
/**
* Returns second from duration.
*
* @return int
*/
public function getSecond(): int
{
return $this->second;
}
/**
* Executes the function event and sends it to the functions worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
$keys = [
'action' => $this->action,
'project' => $this->project,
'user' => $this->user,
'video' => $this->video,
'second' => $this->getSecond(),
];
if (!empty($this->getOutput())) {
$keys = array_merge($keys, [
'output' => $this->getOutput(),
]);
}
if (!empty($this->getProfile())) {
$keys = array_merge($keys, [
'profile' => $this->getProfile(),
]);
}
return Resque::enqueue($this->queue, $this->class, $keys);
}
}
+16
View File
@@ -31,6 +31,7 @@ class Exception extends \Exception
* - Keys
* - Platform
* - Domain
* * Video
* - GraphQL
*/
@@ -184,6 +185,21 @@ class Exception extends \Exception
public const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
public const DOMAIN_TARGET_INVALID = 'domain_target_invalid';
/** Video */
public const VIDEO_PROFILE_NOT_FOUND = 'video_profile_not_found';
public const VIDEO_NOT_VALID = 'video_not_valid';
public const VIDEO_SECOND_OUT_OF_RANGE = 'video_second_out_of_range';
public const VIDEO_TIMELINE_NOT_FOUND = 'video_timeline_not_found';
public const VIDEO_PREVIEW_NOT_FOUND = 'video_preview_not_found';
public const VIDEO_RENDITION_NOT_FOUND = 'video_rendition_not_found';
public const VIDEO_SUBTITLE_NOT_FOUND = 'video_subtitle_not_found';
public const VIDEO_NOT_FOUND = 'video_not_found';
public const VIDEO_RENDITION_SEGMENT_NOT_FOUND = 'video_rendition_segment_not_found';
public const VIDEO_LANGUAGE_CODE_NOT_VALID = 'video_language_code_not_valid';
public const VIDEO_SUBTITLE_SEGMENT_NOT_FOUND = 'video_subtitle_segment_not_found';
public const VIDEO_SUBTITLE_NOT_VALID = 'video_subtitle_not_valid';
/** GraphqQL */
public const GRAPHQL_NO_QUERY = 'graphql_no_query';
public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries';
@@ -299,6 +299,7 @@ class Realtime extends Adapter
if ($bucket->isEmpty()) {
throw new \Exception('Bucket needs to be passed to Realtime for File events in the Storage.');
}
$channels[] = 'files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files.' . $payload->getId();
@@ -325,6 +326,18 @@ class Realtime extends Adapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'videos':
if ($parts[2] === 'renditions') {
$projectId = 'console';
$channels[] = 'console';
$channels[] = 'renditions';
$channels[] = 'renditions.' . $payload->getId();
$channels[] = 'videos.' . $payload->getAttribute('videoId');
$roles = $payload->getRead();
}
break;
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* Domain
*
* Validate that an variable is a valid domain address
*
* @package Utopia\Validator
*/
class Domain extends Validator
{
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Value must be a valid domain';
}
/**
* Is valid
*
* Validation will pass when $value is valid domain.
*
* Validates domain names against RFC 1034, RFC 1035, RFC 952, RFC 1123, RFC 2732, RFC 2181, and RFC 1123.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (empty($value)) {
return false;
}
if (!is_string($value)) {
return false;
}
if (\filter_var($value, FILTER_VALIDATE_DOMAIN) === false) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator\Hostname;
use Utopia\Validator;
/**
* Host
*
* Validate that a host is allowed from given whitelisted hosts list
*
* @package Utopia\Validator
*/
class Host extends Validator
{
protected $whitelist = [];
/**
* @param array $whitelist
*/
public function __construct(array $whitelist)
{
$this->whitelist = $whitelist;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'URL host must be one of: ' . \implode(', ', $this->whitelist);
}
/**
* Is valid
*
* Validation will pass when $value starts with one of the given hosts
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
// Check if value is valid URL
$urlValidator = new URL();
if (!$urlValidator->isValid($value)) {
return false;
}
$hostname = \parse_url($value, PHP_URL_HOST);
$hostnameValidator = new Hostname($this->whitelist);
return $hostnameValidator->isValid($hostname);
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Network\Validator;
use Exception;
use Utopia\Validator;
/**
* IP
*
* Validate that an variable is a valid IP address
*
* @package Utopia\Validator
*/
class IP extends Validator
{
public const ALL = 'all';
public const V4 = 'ipv4';
public const V6 = 'ipv6';
/**
* @var string
*/
protected $type = self::ALL;
/**
* Constructor
*
* Set a the type of IP check.
*
* @param string $type
*/
public function __construct(string $type = self::ALL)
{
if (!in_array($type, [self::ALL, self::V4, self::V6])) {
throw new Exception('Unsupported IP type');
}
$this->type = $type;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Value must be a valid IP address';
}
/**
* Is valid
*
* Validation will pass when $value is valid IP address.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
switch ($this->type) {
case self::ALL:
if (\filter_var($value, FILTER_VALIDATE_IP)) {
return true;
}
break;
case self::V4:
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return true;
}
break;
case self::V6:
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return true;
}
break;
default:
return false;
break;
}
return false;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* URL
*
* Validate that an variable is a valid URL
*
* @package Appwrite\Network\Validator
*/
class URL extends Validator
{
protected array $allowedSchemes;
/**
* @param array $allowedSchemes
*/
public function __construct(array $allowedSchemes = [])
{
$this->allowedSchemes = $allowedSchemes;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
if (!empty($this->allowedSchemes)) {
return 'Value must be a valid URL with following schemes (' . \implode(', ', $this->allowedSchemes) . ')';
}
return 'Value must be a valid URL';
}
/**
* Is valid
*
* Validation will pass when $value is valid URL.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
$sanitizedURL = '';
foreach (str_split($value) as $character) {
$sanitizedURL .= (ord($character) > 127) ? rawurlencode($character) : $character;
}
if (\filter_var($sanitizedURL, FILTER_VALIDATE_URL) === false) {
return false;
}
if (!empty($this->allowedSchemes) && !\in_array(\parse_url($sanitizedURL, PHP_URL_SCHEME), $this->allowedSchemes)) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
+9
View File
@@ -285,6 +285,15 @@ abstract class Worker
return $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
}
/**
* Get Video Storage Device
* @param string $projectId of the project
* @return Device
*/
protected function getVideoDevice(string $projectId): Device
{
return $this->getDevice(APP_STORAGE_VIDEOS . '/app-' . $projectId);
}
/**
* Get Builds Storage Device
+23
View File
@@ -85,6 +85,10 @@ use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\Video;
use Appwrite\Utopia\Response\Model\VideoProfile;
use Appwrite\Utopia\Response\Model\VideoRendition;
use Appwrite\Utopia\Response\Model\VideoSubtitle;
/**
* @method int getStatusCode()
@@ -161,6 +165,16 @@ class Response extends SwooleResponse
public const MODEL_BUCKET = 'bucket';
public const MODEL_BUCKET_LIST = 'bucketList';
//video
public const MODEL_VIDEO = 'video';
public const MODEL_VIDEO_LIST = 'videoList';
public const MODEL_PROFILE = 'profile';
public const MODEL_PROFILE_LIST = 'profileList';
public const MODEL_RENDITION = 'rendition';
public const MODEL_RENDITION_LIST = 'renditionList';
public const MODEL_SUBTITLE = 'subtitle';
public const MODEL_SUBTITLE_LIST = 'subtitleList';
// Locale
public const MODEL_LOCALE = 'locale';
public const MODEL_COUNTRY = 'country';
@@ -280,6 +294,10 @@ class Response extends SwooleResponse
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Profile List', self::MODEL_PROFILE_LIST, 'profiles', self::MODEL_PROFILE))
->setModel(new BaseList('Rendition List', self::MODEL_RENDITION_LIST, 'renditions', self::MODEL_RENDITION))
->setModel(new BaseList('Subtitle List', self::MODEL_SUBTITLE_LIST, 'subtitles', self::MODEL_SUBTITLE))
->setModel(new BaseList('Video List', self::MODEL_VIDEO_LIST, 'videos', self::MODEL_VIDEO))
// Entities
->setModel(new Database())
->setModel(new Collection())
@@ -349,6 +367,11 @@ class Response extends SwooleResponse
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new ConsoleVariables())
->setModel(new Video())
->setModel(new VideoProfile())
->setModel(new VideoRendition())
->setModel(new VideoSubtitle())
// Verification
// Recovery
// Tests (keep last)
@@ -0,0 +1,147 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Video extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('bucketId', [
'type' => self::TYPE_STRING,
'description' => 'Bucket ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('fileId', [
'type' => self::TYPE_STRING,
'description' => 'File ID.',
'default' => '',
'example' => 'd5fg5ehg1c168g7c',
])
->addRule('previewId', [
'type' => self::TYPE_STRING,
'description' => 'File preview image ID.',
'default' => '',
'example' => 'd5fg5ehg56c168g5b',
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'File size.',
'default' => 0,
'example' => 23647142,
])
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'Format.',
'default' => '',
'example' => 'MPEG-4',
])
->addRule('aspectRatio', [
'type' => self::TYPE_STRING,
'description' => 'Aspect ratio .',
'default' => '',
'example' => '16:9',
])
->addRule('duration', [
'type' => self::TYPE_STRING,
'require' => false,
'description' => 'Video duration.',
'default' => 0,
'example' => 92.739989,
])
->addRule('width', [
'type' => self::TYPE_INTEGER,
'require' => false,
'description' => 'Video width.',
'default' => 0,
'example' => 960,
])
->addRule('height', [
'type' => self::TYPE_INTEGER,
'require' => false,
'description' => 'Video height.',
'default' => 0,
'example' => 544,
])
->addRule('videoFormat', [
'type' => self::TYPE_STRING,
'require' => false,
'description' => 'Video format.',
'default' => '',
'example' => 'AVC',
])
->addRule('videoFormatProfile', [
'type' => self::TYPE_STRING,
'require' => false,
'description' => 'Video format profile.',
'default' => '',
'example' => 'Baseline@L3.1',
])
->addRule('videoBitRate', [
'type' => self::TYPE_INTEGER,
'require' => false,
'description' => 'Video bitrate.',
'default' => 0,
'example' => 564790,
])
->addRule('videoFrameRate', [
'type' => self::TYPE_STRING,
'require' => false,
'description' => 'Video frame rate.',
'default' => 0,
'example' => '231947377/4638947',
])
->addRule('audioFormat', [
'type' => self::TYPE_STRING,
'require' => false,
'description' => 'Audio format.',
'default' => '',
'example' => 'AAC',
])
->addRule('audioBitRate', [
'type' => self::TYPE_INTEGER,
'require' => false,
'description' => 'Audio bitrate.',
'default' => 0,
'example' => 127999,
])
->addRule('audioSampleRate', [
'type' => self::TYPE_INTEGER,
'require' => false,
'description' => 'Audio sample rate.',
'default' => '0',
'example' => '',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Video';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_VIDEO;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class VideoProfile extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'ID.',
'default' => null,
'example' => '5e5ea5c16897e',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Video profile name.',
'default' => null,
'example' => '360P',
])
->addRule('videoBitRate', [
'type' => self::TYPE_INTEGER,
'description' => 'Video bitrate.',
'default' => 0,
'example' => 3050,
])
->addRule('audioBitRate', [
'type' => self::TYPE_INTEGER,
'description' => 'Audio bitrate.',
'default' => 0,
'example' => 64,
])
->addRule('width', [
'type' => self::TYPE_INTEGER,
'description' => 'Video width.',
'default' => 0,
'example' => 300,
])
->addRule('height', [
'type' => self::TYPE_INTEGER,
'description' => 'Video height.',
'default' => 0,
'example' => 400,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Video profile';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PROFILE;
}
}
@@ -0,0 +1,113 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class VideoRendition extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'ID.',
'default' => null,
'example' => '5e5ea5c16897e',
])
->addRule('videoId', [
'type' => self::TYPE_STRING,
'description' => 'Video ID.',
'default' => null,
'example' => '5e5ea5c16897e',
])
->addRule('profileId', [
'type' => self::TYPE_STRING,
'description' => 'profile ID.',
'default' => null,
'example' => 'd5fg5ehg1c168g7c',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Rendition name.',
'default' => null,
'example' => '720P',
])
->addRule('startedAt', [
'type' => self::TYPE_INTEGER,
'description' => 'Transcoding time started in Unix timestamp.',
'default' => 0,
'example' => 1592981220,
])
->addRule('endedAt', [
'type' => self::TYPE_INTEGER,
'description' => 'Transcoding time ended in Unix timestamp.',
'default' => 0,
'example' => 1592981290,
])
->addRule('width', [
'type' => self::TYPE_INTEGER,
'description' => 'Video width.',
'default' => 0,
'example' => 300,
])
->addRule('height', [
'type' => self::TYPE_INTEGER,
'description' => 'Video height.',
'default' => 0,
'example' => 400,
])
->addRule('videoBitRate', [
'type' => self::TYPE_INTEGER,
'description' => 'Video bitrate.',
'default' => 0,
'example' => 3050,
])
->addRule('audioBitRate', [
'type' => self::TYPE_INTEGER,
'description' => 'Audio bitrate.',
'default' => 0,
'example' => 64,
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Rendition transcoding status',
'default' => null,
'example' => 'ready',
])
->addRule('progress', [
'type' => self::TYPE_STRING,
'description' => 'Rendition transcoding progress',
'default' => 0,
'example' => 88,
])
->addRule('output', [
'type' => self::TYPE_STRING,
'description' => 'Rendition output type',
'default' => null,
'example' => 'hls',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Video rendition';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_RENDITION;
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class VideoSubtitle extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('videoId', [
'type' => self::TYPE_STRING,
'description' => 'Video ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('bucketId', [
'type' => self::TYPE_STRING,
'description' => 'Bucket ID.',
'default' => '',
'example' => 'd5fg5ehg1c168g7c',
])
->addRule('fileId', [
'type' => self::TYPE_STRING,
'description' => 'file ID.',
'default' => '',
'example' => 'c5fg5emg1c168grr',
])
->addRule('path', [
'type' => self::TYPE_STRING,
'description' => 'Subtitle path.',
'default' => '',
'example' => '640x360@500',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Subtitle name.',
'default' => '',
'example' => 'English',
])
->addRule('code', [
'type' => self::TYPE_STRING,
'description' => 'Subtitle code.',
'default' => '',
'example' => 'Eng',
])
->addRule('default', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Subtitle default',
'default' => '',
'example' => false,
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Subtitle packaging status',
'default' => '',
'example' => 'ready',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Video subtitle';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SUBTITLE;
}
}
+2
View File
@@ -81,6 +81,8 @@ trait ProjectCustom
'locale.read',
'avatars.read',
'health.read',
'videos.write',
'videos.read',
],
]);
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Tests\E2E\Scopes;
use Tests\E2E\Client;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
trait VideoCustom
{
use ProjectCustom;
/**
* @var array
*/
protected static $bucket = [];
protected static $video = [];
protected static $subtitle = [];
/**
* @return array
*/
public function getBucket(): array
{
if (!empty(self::$bucket)) {
return self::$bucket;
}
$_bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'name' => 'My Video bucket ',
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::create(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
self::$bucket = [
'$id' => $_bucket['body']['$id'],
];
return self::$bucket;
}
/**
* @return array
*/
public function getVideo(): array
{
if (!empty(self::$video)) {
return self::$video;
}
$source = __DIR__ . "/../../resources/disk-a/video-srt.mp4";
$totalSize = \filesize($source);
$chunkSize = 5 * 1024 * 1024;
$handle = @fopen($source, "rb");
$fileId = 'unique()';
$mimeType = mime_content_type($source);
$counter = 0;
$size = filesize($source);
$headers = [
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
while (!feof($handle)) {
$curlFile = new \CURLFile('data:' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'video-srt.mp4');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size;
if (!empty($id)) {
$headers['x-appwrite-id'] = $id;
}
$_file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $this->getBucket()['$id'] . '/files', array_merge($headers, $this->getHeaders()), [
'fileId' => $fileId,
'file' => $curlFile,
]);
$counter++;
$id = $_file['body']['$id'];
}
@fclose($handle);
self::$video = [
'$id' => $_file['body']['$id'],
];
return self::$video;
}
/**
* @return array
*/
public function getSubtitle(): array
{
if (!empty(self::$subtitle)) {
return self::$subtitle;
}
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $this->getBucket()['$id'] . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'file' => new \CURLFile(realpath(__DIR__ . '/../../resources/disk-a//../../resources/disk-a/video-srt.srt'), 'text/plain', 'video-srt.srt'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
self::$subtitle = [
'$id' => $res['body']['$id'],
];
return self::$subtitle;
}
}
@@ -0,0 +1,450 @@
<?php
namespace Tests\E2E\Services\Videos;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Client;
use Tests\E2E\Scopes\VideoCustom;
use Tests\E2E\Services\Videos\VideosPermissionsScope;
use Utopia\Database\Helpers\ID;
class VideosCustomClientTest extends Scope
{
use ProjectCustom;
use VideoCustom;
use SideClient;
use VideosPermissionsScope;
private array $outputs = ['hls', 'dash'];
public function testDeleteProfiles()
{
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals(3, $response['body']['total']);
$profiles = $response['body']['profiles'];
foreach ($profiles as $profile) {
$response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(204, $response['headers']['status-code']);
}
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profile['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals('Video profile not found.', $response['body']['message']);
}
public function testTranscodeWithSubs()
{
/**
* Create video
*/
$response = $this->client->call(Client::METHOD_POST, '/videos', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => $this->getBucket()['$id'],
'fileId' => $this->getVideo()['$id']
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$videoId = $response['body']['$id'];
$email = ID::unique() . '@localhost.test';
$password = 'password';
$user2 = $this->createUser('user2', $email, $password);
$file = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId, [
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
]);
$this->assertEquals($file['headers']['status-code'], 401);
/**
* Create preview
*/
$response = $this->client->call(Client::METHOD_POST, '/v1/videos/' . $videoId . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'second' => 'one',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/v1/videos/' . $videoId . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'second' => 99999999,
]);
$this->assertEquals(400, $response['headers']['status-code']);
sleep(5);
$response = $this->client->call(Client::METHOD_POST, '/v1/videos/' . $videoId . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'second' => 10,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(92810, $response['body']['duration']);
/**
* timeline vtt
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/videos/' . $videoId . '/timeline', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
$this->assertEquals(25, count($match[0]));
/**
* timeline preview image
*/
$response = $this->client->call(Client::METHOD_GET, '/v1' . $match[0][0], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('image/jpeg', $response['headers']['content-type']);
$this->assertEquals('miss', $response['headers']['x-appwrite-cache']);
sleep(3);
/**
* timeline preview image (response from cache)
*/
$response = $this->client->call(Client::METHOD_GET, '/v1' . $match[0][0], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals('image/jpeg', $response['headers']['content-type']);
$this->assertEquals('hit', $response['headers']['x-appwrite-cache']);
$response = $this->client->call(Client::METHOD_GET, '/v1/videos/' . $videoId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$previewId = $response['body']['previewId'];
/**
* preview image
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/videos/' . $videoId . '/preview/' . $previewId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals('image/jpeg', $response['headers']['content-type']);
$this->assertEquals('miss', $response['headers']['x-appwrite-cache']);
sleep(4);
/**
* response from cache
*/
$response = $this->client->call(Client::METHOD_GET, '/v1/videos/' . $videoId . '/preview/' . $previewId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertNotEmpty($response['body']);
$this->assertEquals('image/jpeg', $response['headers']['content-type']);
$this->assertEquals('hit', $response['headers']['x-appwrite-cache']);
$response = $this->client->call(Client::METHOD_POST, '/v1/videos/' . $videoId . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'second' => 15,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
sleep(4);
$response = $this->client->call(Client::METHOD_GET, '/v1/videos/' . $videoId . '/preview/' . $previewId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'width' => 300,
'height' => 100,
'output' => 'png',
]);
$this->assertNotEmpty($response['body']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertEquals('miss', $response['headers']['x-appwrite-cache']);
/**
* Create subtitles
*/
$response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => $this->getBucket()['$id'],
'fileId' => $this->getSubtitle()['$id'],
'name' => 'English',
'code' => 'Eng',
'default' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => $this->getBucket()['$id'],
'fileId' => $this->getSubtitle()['$id'],
'name' => 'Italian',
'code' => 'Ita',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
/**
* Create profiles
*/
$response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'name' => 'Profile A',
'videoBitRate' => 770,
'audioBitRate' => 64,
'width' => 600,
'height' => 400,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'name' => 'Profile B',
'videoBitRate' => 570,
'audioBitRate' => 64,
'width' => 300,
'height' => 200,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertNotEmpty($response['body']['profiles']);
$profiles = $response['body']['profiles'];
foreach ($profiles as $index => $profile) {
$response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/rendition', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'output' => $this->outputs[$index % 2],
'profileId' => $profile['$id'],
]);
$this->assertEquals(204, $response['headers']['status-code']);
}
sleep(30);
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/hls/master.m3u8', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
$this->assertEquals(4, count($match[0]));
$renditionUri = $match[0][0];
$subtitleUri = $match[0][1];
$response = $this->client->call(Client::METHOD_GET, $renditionUri, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
$this->assertEquals(10, count($match[0]));
$segmentUri = $match[0][0];
$response = $this->client->call(Client::METHOD_GET, $segmentUri, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, strlen($response['body']));
$response = $this->client->call(Client::METHOD_GET, $subtitleUri, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
$segmentUri = $match[0][0];
$response = $this->client->call(Client::METHOD_GET, $segmentUri, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1508, strlen($response['body']));
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/dash/master.mpd', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$xml = simplexml_load_string($response['body']);
$this->assertEquals("PT1M32.7S", $xml->attributes()->mediaPresentationDuration);
$this->assertEquals("PT10.0S", $xml->attributes()->maxSegmentDuration);
$this->assertEquals("PT20.0S", $xml->attributes()->minBufferTime);
$subsCount = 0;
$isVideo = false;
$isAudio = false;
$subs[] = ['id' => '2', 'lang' => 'English',];
$subs[] = ['id' => '3', 'lang' => 'Italian',];
foreach ($xml->Period->AdaptationSet as $adaptation) {
if ((string)$adaptation['contentType'] === 'video') {
$isVideo = true;
$this->assertEquals("50/1", $adaptation['frameRate']);
$this->assertEquals("300", $adaptation['maxWidth']);
$this->assertEquals("30:17", $adaptation['par']);
$this->assertEquals("und", $adaptation['lang']);
foreach ($adaptation->Representation as $representation) {
$this->assertEquals("video/mp4", $representation['mimeType']);
$this->assertEquals("avc1.640015", $representation['codecs']);
$this->assertEquals("300", $representation['width']);
$this->assertEquals("200", $representation['height']);
$this->assertEquals("20:17", $representation['sar']);
$this->assertEquals(10, $representation->SegmentList->SegmentURL->count());
$videoSegmentBaseUrl = (string)$representation->BaseURL;
$videoSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL'];
$videoSegmentId = (string)$representation->SegmentList->SegmentURL['media'];
$response = $this->client->call(Client::METHOD_GET, $videoSegmentBaseUrl . $videoSegmentInitialization, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders());
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, strlen($response['body']));
$response = $this->client->call(Client::METHOD_GET, $videoSegmentBaseUrl . $videoSegmentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders());
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, strlen($response['body']));
}
} elseif ((string)$adaptation['contentType'] === 'audio') {
$isAudio = true;
foreach ($adaptation->Representation as $representation) {
$this->assertEquals("audio/mp4", $representation['mimeType']);
$this->assertEquals("mp4a.40.2", $representation['codecs']);
$this->assertEquals(10, $representation->SegmentList->SegmentURL->count());
$audioSegmentBaseUrl = (string)$representation->BaseURL;
$audioSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL'];
$audioSegmentId = (string)$representation->SegmentList->SegmentURL['media'];
$response = $this->client->call(Client::METHOD_GET, $audioSegmentBaseUrl . $audioSegmentInitialization, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, strlen($response['body']));
$response = $this->client->call(Client::METHOD_GET, $audioSegmentBaseUrl . $audioSegmentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, strlen($response['body']));
}
} elseif ((string)$adaptation['mimeType'] === 'text/vtt') {
$this->assertEquals($subs[$subsCount]['id'], $adaptation['id']);
$this->assertEquals($subs[$subsCount]['lang'], $adaptation['lang']);
$subsCount++;
}
}
$this->assertEquals($subsCount, 2);
$this->assertTrue($isVideo);
$this->assertTrue($isAudio);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
<?php
namespace Tests\E2E\Services\Videos;
use Tests\E2E\Client;
trait VideosPermissionsScope
{
public array $users = [];
public array $teams = [];
public function createUser(string $id, string $email, string $password = 'test123!'): array
{
$user = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => $id,
'email' => $email,
'password' => $password
]);
$this->assertEquals(201, $user['headers']['status-code']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => $email,
'password' => $password,
]);
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$user = [
'$id' => $user['body']['$id'],
'email' => $user['body']['email'],
'session' => $session,
];
$this->users[$id] = $user;
return $user;
}
public function getCreatedUser(string $id): array
{
return $this->users[$id] ?? [];
}
public function createTeam(string $id, string $name): array
{
$team = $this->client->call(Client::METHOD_POST, '/teams', $this->getServerHeader(), [
'teamId' => $id,
'name' => $name
]);
$this->teams[$id] = $team['body'];
return $team['body'];
}
public function addToTeam(string $user, string $team, array $roles = []): array
{
$membership = $this->client->call(Client::METHOD_POST, '/teams/' . $team . '/memberships', $this->getServerHeader(), [
'teamId' => $team,
'email' => $this->getCreatedUser($user)['email'],
'roles' => $roles,
'url' => 'http://localhost:5000/join-us#title'
]);
return [
'user' => $membership['body']['userId'],
'membership' => $membership['body']['$id']
];
}
public function getServerHeader(): array
{
return [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
];
}
}
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
1
00:00:16,823 --> 00:00:18,616
Good evening, have a good week.
2
00:00:18,663 --> 00:00:21,130
Happy birthday to the ILF,
Israeli Leadership Forum.
3
00:00:21,483 --> 00:00:24,192
Today we have 500 members,
4
00:00:24,217 --> 00:00:28,495
a powerful community bringing
leaders together
5
00:00:28,520 --> 00:00:30,253
out of a true desire to create change.
6
00:00:30,833 --> 00:00:34,999
Thank you very much, dear members
of the ILF. I'm glad to be a part of ILF.
7
00:00:35,120 --> 00:00:37,713
There is an inconceivable power here,
a power of people.
8
00:00:37,803 --> 00:00:42,240
A much larger power
than we each have as individuals.
9
00:00:42,360 --> 00:00:46,701
And it's really exciting to see
that the values
10
00:00:46,726 --> 00:00:49,165
are the guiding light for everyone
sitting in this room,
11
00:00:49,190 --> 00:00:50,980
in order to change the world.
12
00:00:51,120 --> 00:00:55,575
We sit looking at a new immigrant
who just came here three years ago,
13
00:00:55,600 --> 00:00:58,120
making change, dramatic change.
14
00:00:58,360 --> 00:01:01,792
He represents the Israel
we don't see on the news.
15
00:01:01,817 --> 00:01:03,017
The normal Israel.
16
00:01:06,397 --> 00:01:09,003
Man of the Year in Israel,
Silvan Adams.
17
00:01:10,960 --> 00:01:13,978
So believe it or not,
18
00:01:14,003 --> 00:01:21,965
because they suspect us,
I'd say,
19
00:01:21,990 --> 00:01:24,395
because of the news reports from over here,
20
00:01:24,420 --> 00:01:26,870
we need always show
the best face of Israel.