mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
153
Commits
docsdb-fixes
...
__stream
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da53a50cfd | ||
|
|
7d6375fc62 | ||
|
|
d28803e2e7 | ||
|
|
d3475030bf | ||
|
|
e0f4673574 | ||
|
|
e28cbce5ee | ||
|
|
767b5c8541 | ||
|
|
af80accc15 | ||
|
|
f3508e1701 | ||
|
|
3c69c211e9 | ||
|
|
927bead147 | ||
|
|
6af8fd6660 | ||
|
|
a187aa4bc5 | ||
|
|
60098d71ee | ||
|
|
38c64996ac | ||
|
|
89dfbfba25 | ||
|
|
578c8d9277 | ||
|
|
dbd58966db | ||
|
|
d6e1d0f6f6 | ||
|
|
741a87f464 | ||
|
|
db4506b0fe | ||
|
|
14575a7e40 | ||
|
|
1830138455 | ||
|
|
4cd273e976 | ||
|
|
fc991e89f1 | ||
|
|
9f6589d21b | ||
|
|
c09f600e73 | ||
|
|
2b60b18a3c | ||
|
|
053d688754 | ||
|
|
bccd1a96c0 | ||
|
|
4a8ef126bd | ||
|
|
61e3faa964 | ||
|
|
22b4555cc0 | ||
|
|
aa8334e7d8 | ||
|
|
8a8a2d37e4 | ||
|
|
883d48c2e5 | ||
|
|
cb217b43b4 | ||
|
|
7340968d1f | ||
|
|
5785145e58 | ||
|
|
9f94560a2c | ||
|
|
92f75bd3d9 | ||
|
|
658cfee38e | ||
|
|
26251e94f6 | ||
|
|
dbad8c2a85 | ||
|
|
d6d6a7753a | ||
|
|
7a0255d011 | ||
|
|
81ca8a5fc9 | ||
|
|
8d496d66d1 | ||
|
|
cfb53be8ac | ||
|
|
4b227c7428 | ||
|
|
b9fa699c03 | ||
|
|
a1918d1512 | ||
|
|
d26aabb083 | ||
|
|
ca90711c30 | ||
|
|
d1c0e0d9a2 | ||
|
|
4ac062edfe | ||
|
|
f71f18095f | ||
|
|
e2df63b78a | ||
|
|
1b6adce14f | ||
|
|
3d1228ba13 | ||
|
|
edaa93936f | ||
|
|
65a1921d48 | ||
|
|
f7a1cb14f2 | ||
|
|
f1fa22c4fc | ||
|
|
331c9070b3 | ||
|
|
7c81a1ea88 | ||
|
|
e4972cc126 | ||
|
|
1af48bd895 | ||
|
|
403e1c2d64 | ||
|
|
2bb45c36d8 | ||
|
|
7ea695937b | ||
|
|
19388cc1a6 | ||
|
|
7daa198f99 | ||
|
|
89f325fe87 | ||
|
|
19ad9914a3 | ||
|
|
5546de13e3 | ||
|
|
c55599516c | ||
|
|
0172a6c393 | ||
|
|
19a38cd06a | ||
|
|
de6359cf9e | ||
|
|
59c5c037c4 | ||
|
|
104bd58c45 | ||
|
|
110c38d03d | ||
|
|
c9b0c6f906 | ||
|
|
baea836574 | ||
|
|
aef8c1b207 | ||
|
|
1aed3a1db5 | ||
|
|
53b020fac0 | ||
|
|
9060d86145 | ||
|
|
ca0017ca40 | ||
|
|
115b032f3a | ||
|
|
b13c9f58cc | ||
|
|
fcd29baacc | ||
|
|
735072f0a4 | ||
|
|
b26a28d4fb | ||
|
|
33e608e625 | ||
|
|
6d2d115fd3 | ||
|
|
ed93e54480 | ||
|
|
ebe794926c | ||
|
|
59b188755c | ||
|
|
5db53f7db8 | ||
|
|
e843c0059d | ||
|
|
93d777a412 | ||
|
|
9ffd52d030 | ||
|
|
d6dc025331 | ||
|
|
b9bca60e09 | ||
|
|
f2fbdf8a7c | ||
|
|
e8c96e8452 | ||
|
|
6ca361d0b0 | ||
|
|
2f1a116e22 | ||
|
|
fd3a4fd485 | ||
|
|
49f8eae3f6 | ||
|
|
fe6a47252e | ||
|
|
b403a829de | ||
|
|
e232f2bbd2 | ||
|
|
6896ff3999 | ||
|
|
1bcd2d6bd5 | ||
|
|
8340198077 | ||
|
|
5b01232ecc | ||
|
|
076f333f77 | ||
|
|
fbe63a884e | ||
|
|
cb0ab3c823 | ||
|
|
817793a587 | ||
|
|
2a02784b0c | ||
|
|
a7d7879d37 | ||
|
|
86b7215375 | ||
|
|
aeebbbe91d | ||
|
|
4efbc92cf2 | ||
|
|
7f44f45bf6 | ||
|
|
33d942c356 | ||
|
|
4089b81b3b | ||
|
|
9f4ebcf208 | ||
|
|
ec8eaa1ddd | ||
|
|
6c7c5c1891 | ||
|
|
d8f4d097d1 | ||
|
|
bc43989aa4 | ||
|
|
dc38b63160 | ||
|
|
01eaa65d6b | ||
|
|
5e903daf6f | ||
|
|
e48e1dae2c | ||
|
|
c2e599222e | ||
|
|
bb172f8a3e | ||
|
|
48320d40ab | ||
|
|
c89b028797 | ||
|
|
c7534760a0 | ||
|
|
d6007b24ec | ||
|
|
c8c282afb2 | ||
|
|
6491cea581 | ||
|
|
100fb73323 | ||
|
|
aa6ff513a6 | ||
|
|
56545a7344 | ||
|
|
e97957d46e | ||
|
|
329c43f860 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -3206,6 +3206,28 @@ $collections = [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'resourceType',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('mimeType'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255, // https://tools.ietf.org/html/rfc4288#section-4.2
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'accessedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
@@ -3262,6 +3284,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,
|
||||
@@ -3591,6 +3624,890 @@ $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' => '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;
|
||||
|
||||
@@ -579,6 +579,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
@@ -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
@@ -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 => [
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
];
|
||||
|
||||
@@ -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
@@ -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,
|
||||
],
|
||||
];
|
||||
@@ -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')
|
||||
@@ -2096,35 +2074,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();
|
||||
;
|
||||
|
||||
@@ -2280,32 +2235,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()
|
||||
;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -175,9 +175,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')
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -195,28 +195,25 @@ App::init()
|
||||
$database->setProject($project);
|
||||
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_CREATE, fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_DELETE, fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
|
||||
if ($useCache) {
|
||||
$key = md5($request->getURI() . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER;
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
);
|
||||
$timestamp = 60 * 60 * 24 * 30;
|
||||
$data = $cache->load($key, $timestamp);
|
||||
|
||||
if (!empty($data)) {
|
||||
$data = json_decode($data, true);
|
||||
$parts = explode('/', $data['resourceType']);
|
||||
if (!empty($data) && !$cacheLog->isEmpty()) {
|
||||
$parts = explode('/', $cacheLog->getAttribute('resourceType'));
|
||||
$type = $parts[0] ?? null;
|
||||
|
||||
if ($type === 'bucket') {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -225,11 +222,12 @@ App::init()
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$validator = new Authorization(Database::PERMISSION_READ);
|
||||
$valid = $validator->isValid($bucket->getRead());
|
||||
|
||||
if (!$fileSecurity && !$valid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$parts = explode('/', $data['resource']);
|
||||
$parts = explode('/', $cacheLog->getAttribute('resource'));
|
||||
$fileId = $parts[1] ?? null;
|
||||
|
||||
if ($fileSecurity && !$valid) {
|
||||
@@ -246,8 +244,8 @@ App::init()
|
||||
$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']))
|
||||
->setContentType($cacheLog->getAttribute('mimeType'))
|
||||
->send($data)
|
||||
;
|
||||
|
||||
$route->setIsActive(false);
|
||||
@@ -475,7 +473,6 @@ App::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
|
||||
if (!empty($data['payload'])) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (!empty($pattern)) {
|
||||
@@ -488,21 +485,17 @@ App::shutdown()
|
||||
}
|
||||
|
||||
$key = md5($request->getURI() . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER;
|
||||
$data = json_encode([
|
||||
'resourceType' => $resourceType,
|
||||
'resource' => $resource,
|
||||
'contentType' => $response->getContentType(),
|
||||
'payload' => base64_encode($data['payload']),
|
||||
]) ;
|
||||
|
||||
$signature = md5($data);
|
||||
$cacheLog = $dbForProject->getDocument('cache', $key);
|
||||
$signature = md5($data['payload']);
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
|
||||
$now = DateTime::now();
|
||||
if ($cacheLog->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'resourceType' => $resourceType,
|
||||
'mimeType' => $response->getContentType(),
|
||||
'accessedAt' => $now,
|
||||
'signature' => $signature,
|
||||
])));
|
||||
@@ -515,7 +508,7 @@ App::shutdown()
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
);
|
||||
$cache->save($key, $data);
|
||||
$cache->save($key, $data['payload']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-25
@@ -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 = '64635cdd5f809d98d3ad';
|
||||
|
||||
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
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;?>
|
||||
@@ -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; ?>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,820 @@
|
||||
<?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 Document $subtitle;
|
||||
private string $action;
|
||||
private Document $profile;
|
||||
private Document $project;
|
||||
private Document $file;
|
||||
private Document $bucket;
|
||||
private FFProbe $ffprobe;
|
||||
private FFMpeg $ffmpeg;
|
||||
|
||||
|
||||
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->subtitle = new Document($this->args['subtitle'] ?? []);
|
||||
|
||||
$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;
|
||||
|
||||
match ($this->action) {
|
||||
'timeline' => $this->createTimeLine(),
|
||||
'subtitle' => $this->createSubtitle(),
|
||||
default => $this->createOutput(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create timeline
|
||||
*
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function createTimeLine(): 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
|
||||
*/
|
||||
|
||||
$this->download($this->project, $this->file);
|
||||
|
||||
if (empty($this->video->getAttribute('duration'))) {
|
||||
$this->createInfo();
|
||||
}
|
||||
|
||||
if (empty($this->video->getAttribute('videoBitRate'))) {
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create subtitle
|
||||
*
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function createSubtitle(): void
|
||||
{
|
||||
|
||||
if (empty($this->video->getAttribute('duration'))) {
|
||||
$this->download($this->project, $this->file);
|
||||
$this->createInfo();
|
||||
}
|
||||
|
||||
$segments = $this->database->find('videos_subtitles_segments', [
|
||||
Query::equal('subtitleInternalId', [$this->subtitle->getInternalId()]),
|
||||
]);
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
$this->database->deleteDocument('videos_subtitles_segments', $segment->getId());
|
||||
}
|
||||
|
||||
$this->subtitle->setAttribute('status', self::STATUS_START);
|
||||
$this->database->updateDocument('videos_subtitles', $this->subtitle->getId(), $this->subtitle);
|
||||
|
||||
$bucket = $this->database->getDocument('buckets', $this->subtitle->getAttribute('bucketId'));
|
||||
$file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->subtitle->getAttribute('fileId'));
|
||||
$path = basename($file->getAttribute('path'));
|
||||
|
||||
$this->download($this->project, $file);
|
||||
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
||||
$subtitlePath = $this->inDir . $this->subtitle->getId() . '.vtt';
|
||||
|
||||
if ($ext === 'srt') {
|
||||
$srt = new SubripFile($this->inDir . $path);
|
||||
$srt->convertTo('webvtt')->save($subtitlePath);
|
||||
}
|
||||
|
||||
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/subtitles/';
|
||||
$this->database->createDocument('videos_subtitles_segments', new Document([
|
||||
'subtitleId' => $this->subtitle->getId(),
|
||||
'subtitleInternalId' => $this->subtitle->getInternalId(),
|
||||
'fileName' => $this->subtitle->getId() . '.vtt',
|
||||
'path' => $path,
|
||||
'duration' => (string)number_format(($this->video->getAttribute('duration') / 1000), 1),
|
||||
]));
|
||||
|
||||
/**
|
||||
* Upload subtitle
|
||||
*/
|
||||
console::info('Uploading ' . $this->subtitle->getId() . '.vtt');
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$path . $this->subtitle->getId() . '.vtt',
|
||||
(new Local('/'))->read($this->inDir . $this->subtitle->getId() . '.vtt'),
|
||||
'text/vtt'
|
||||
);
|
||||
|
||||
$this->subtitle->setAttribute('targetDuration', (string)number_format(($this->video->getAttribute('duration') / 1000), 1));
|
||||
$this->subtitle->setAttribute('status', self::STATUS_READY);
|
||||
$this->subtitle->setAttribute('path', $path);
|
||||
$this->database->updateDocument('videos_subtitles', $this->subtitle->getId(), $this->subtitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create output
|
||||
*
|
||||
* @throws Authorization
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
* @throws Structure
|
||||
*/
|
||||
private function createOutput(): void
|
||||
{
|
||||
|
||||
$this->download($this->project, $this->file);
|
||||
if (empty($this->video->getAttribute('duration'))) {
|
||||
$this->createInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 . '"');
|
||||
}
|
||||
|
||||
$media = $this->ffmpeg->open($this->inPath);
|
||||
|
||||
$this->renditionName = $this->profile->getAttribute('width')
|
||||
. 'X' . $this->profile->getAttribute('height')
|
||||
. '@' . ($this->profile->getAttribute('videoBitRate') + $this->profile->getAttribute('audioBitRate'));
|
||||
|
||||
|
||||
$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);
|
||||
|
||||
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);
|
||||
|
||||
unset($media);
|
||||
|
||||
//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);
|
||||
|
||||
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/' . $this->renditionName . '-' . $query->getId() . '/';
|
||||
|
||||
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' => $path,
|
||||
'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' => $path,
|
||||
'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');
|
||||
|
||||
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());
|
||||
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$path . $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', $path);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create media Info
|
||||
*
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function createInfo(): void
|
||||
{
|
||||
|
||||
$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->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))
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcode
|
||||
*
|
||||
* @param $media Media
|
||||
* @param $format StreamFormat
|
||||
* @param $representation Representation
|
||||
* @return void
|
||||
*/
|
||||
private function transcode(Media $media, StreamFormat $format, Representation $representation): 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;
|
||||
}
|
||||
|
||||
$media->hls()
|
||||
->setFormat($format)
|
||||
->setHlsTime($segmentSize)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->save($this->outPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HLS|DASH segments
|
||||
*
|
||||
* @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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get HLS segments urls
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param string $action
|
||||
* @param Document $payload
|
||||
* @throws Exception|\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']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Download
|
||||
*
|
||||
* @param $project Document
|
||||
* @param $file Document
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function download(Document $project, Document $file)
|
||||
{
|
||||
|
||||
$fullPath = $file->getAttribute('path');
|
||||
$path = basename($file->getAttribute('path'));
|
||||
|
||||
console::info('Downloading file: ' . $path . ' to local storage ' . $this->inDir);
|
||||
|
||||
$local = new Local('/');
|
||||
try {
|
||||
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;
|
||||
}
|
||||
$local->
|
||||
write($this->inDir . $path, $data, $file->getAttribute('mimeType'));
|
||||
} else {
|
||||
$this->getFilesDevice(
|
||||
$project->getId()
|
||||
)->transfer($fullPath, $this->inDir . $path, $local);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
console::error($th->getMessage() . ' on file: ' . $th->getFile() . ' line: ' . $th->getLine);
|
||||
}
|
||||
}
|
||||
|
||||
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 . ']');
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.33.*",
|
||||
"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
+1260
-258
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Resque;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Video extends Event
|
||||
{
|
||||
protected ?Document $video = null;
|
||||
protected ?Document $profile = null;
|
||||
protected ?Document $subtitle = null;
|
||||
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 subtitle.
|
||||
*
|
||||
* @param Document $subtitle
|
||||
* @return self
|
||||
*/
|
||||
public function setSubtitle(Document $subtitle): self
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Subtitle.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getSubtitle(): ?Document
|
||||
{
|
||||
return $this->subtitle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
];
|
||||
|
||||
if (!empty($this->getSubtitle())) {
|
||||
$keys = array_merge($keys, [
|
||||
'subtitle' => $this->getSubtitle(),
|
||||
]);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class Exception extends \Exception
|
||||
* - Keys
|
||||
* - Platform
|
||||
* - Domain
|
||||
* * Video
|
||||
* - GraphQL
|
||||
*/
|
||||
|
||||
@@ -186,6 +187,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,8 @@ trait ProjectCustom
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'health.read',
|
||||
'videos.write',
|
||||
'videos.read',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -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.
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user