mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
144
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.2.0
|
||||
branch = video-scopes-support
|
||||
|
||||
+11
-1
@@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM appwrite/base:0.2.0 as final
|
||||
FROM appwrite/base:0.1.0 as final
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3219,6 +3219,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,
|
||||
@@ -3548,6 +3559,912 @@ $collections = [
|
||||
],
|
||||
],
|
||||
],
|
||||
'videos' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos',
|
||||
'$name' => 'Videos',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'bucketId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'bucketInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'previewId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'previewInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'size',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'format',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'width',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'height',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'aspectRatio',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoFormat',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoFormatProfile',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoFrameRateMode',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoFrameRate',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'audioFormat',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'audioBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'audioSampleRate',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_bucket_file',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['bucketId', 'fileId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_previews' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_previews',
|
||||
'$name' => 'Videos_previews',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'videoId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'type',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'name',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'second',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_video_type',
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['videoId', 'type', 'name'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_renditions' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_renditions',
|
||||
'$name' => 'Videos_renditions',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'videoId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'profileId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'profileInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'name',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'startedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'endedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'targetDuration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'width',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'height',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'audioBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'metadata',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 4096,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'progress',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'output',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_video_output_profile',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['videoId', 'output', 'profileId'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_renditions_segments' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_rendition_segments',
|
||||
'$name' => 'Videos_rendition_segments',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'renditionId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'renditionInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileName',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'streamId',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'isInit',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_renditionId',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['renditionId'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_profiles' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_profiles',
|
||||
'$name' => 'Videos_profiles',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'name',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'audioBitRate',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'width',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'height',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_profile',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['width', 'height', 'videoBitRate', 'audioBitRate'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_subtitles' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_subtitles',
|
||||
'$name' => 'Videos_subtitles',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'videoId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'videoInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'bucketId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'bucketInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'name',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'code',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'default',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'targetDuration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
|
||||
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_bucket_video_subtitle',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['videoId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_subtitles_segments' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_subtitles_segments',
|
||||
'$name' => 'Videos_subtitles_segments',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'subtitleId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'subtitleInternalId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileName',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_subtitleId',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['subtitleId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
return $collections;
|
||||
|
||||
@@ -549,6 +549,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
@@ -233,5 +233,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',
|
||||
]
|
||||
];
|
||||
|
||||
@@ -186,4 +186,17 @@ return [
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/graphql.png',
|
||||
],
|
||||
'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,40 @@
|
||||
<?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' => '576p',
|
||||
'videoBitRate' => 2538,
|
||||
'audioBitRate' => 128,
|
||||
'width' => 1024,
|
||||
'height' => 576,
|
||||
],
|
||||
[
|
||||
'name' => '1080p',
|
||||
'videoBitRate' => 3551,
|
||||
'audioBitRate' => 128,
|
||||
'width' => 1280,
|
||||
'height' => 720,
|
||||
],
|
||||
[
|
||||
'name' => '720p',
|
||||
'videoBitRate' => 4800,
|
||||
'audioBitRate' => 128,
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
[
|
||||
'name' => '2160p',
|
||||
'videoBitRate' => 16000,
|
||||
'audioBitRate' => 356,
|
||||
'width' => 4096,
|
||||
'height' => 2160,
|
||||
],
|
||||
];
|
||||
+1
-1
Submodule app/console updated: 5e2a40c1e3...2fac7c1f39
@@ -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;
|
||||
@@ -62,7 +62,7 @@ App::post('/v1/account')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ACCOUNT)
|
||||
->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', '', new Password(), 'User password. Must be at least 8 chars.')
|
||||
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||
@@ -140,7 +140,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')
|
||||
@@ -371,7 +371,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')
|
||||
@@ -633,7 +633,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')
|
||||
@@ -725,33 +725,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()
|
||||
;
|
||||
|
||||
@@ -773,7 +751,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')
|
||||
@@ -910,7 +888,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')
|
||||
@@ -1015,7 +993,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')
|
||||
@@ -1130,7 +1108,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')
|
||||
@@ -2049,35 +2027,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();
|
||||
;
|
||||
|
||||
@@ -2233,32 +2188,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;
|
||||
|
||||
@@ -33,8 +33,8 @@ use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Locale\Locale;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\URL;
|
||||
use Appwrite\Network\Validator\IP;
|
||||
use Appwrite\Network\Validator\URL;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Limit;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Offset;
|
||||
@@ -69,6 +69,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
|
||||
$filters = $attribute->getAttribute('filters', []); // filters are hidden from the endpoint
|
||||
$default = $attribute->getAttribute('default');
|
||||
|
||||
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
@@ -162,7 +163,7 @@ App::post('/v1/databases')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DATABASE) // Model for database needs to be created
|
||||
->param('databaseId', '', 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('databaseId', '', 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('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -383,7 +384,7 @@ App::get('/v1/databases/:databaseId/logs')
|
||||
|
||||
App::put('/v1/databases/:databaseId')
|
||||
->desc('Update Database')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'databases.write')
|
||||
->label('event', 'databases.[databaseId].update')
|
||||
->label('audits.event', 'database.update')
|
||||
@@ -397,7 +398,7 @@ App::put('/v1/databases/:databaseId')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DATABASE)
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('name', null, new Text(128), 'Database name. Max length: 128 chars.')
|
||||
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
@@ -426,7 +427,7 @@ App::put('/v1/databases/:databaseId')
|
||||
|
||||
App::delete('/v1/databases/:databaseId')
|
||||
->desc('Delete Database')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'databases.write')
|
||||
->label('event', 'databases.[databaseId].delete')
|
||||
->label('audits.event', 'database.delete')
|
||||
@@ -488,9 +489,9 @@ App::post('/v1/databases/:databaseId/collections')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_COLLECTION)
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', 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('collectionId', '', 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('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default no user is granted with any permissions. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](/docs/permissions).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -728,7 +729,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
|
||||
App::put('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default'])
|
||||
->desc('Update Collection')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.write')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].update')
|
||||
->label('audits.event', 'collection.update')
|
||||
@@ -745,7 +746,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default the current permission are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(), 'Is collection enabled?', true)
|
||||
->inject('response')
|
||||
@@ -796,7 +797,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
|
||||
App::delete('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default'])
|
||||
->desc('Delete Collection')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.write')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].delete')
|
||||
->label('audits.event', 'collection.delete')
|
||||
@@ -853,7 +854,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default'])
|
||||
->desc('Create String Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -903,7 +904,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default'])
|
||||
->desc('Create Email Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -947,7 +948,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default'])
|
||||
->desc('Create Enum Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1007,7 +1008,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default'])
|
||||
->desc('Create IP Address Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1051,7 +1052,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default'])
|
||||
->desc('Create URL Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1095,7 +1096,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default'])
|
||||
->desc('Create Integer Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1168,7 +1169,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default'])
|
||||
->desc('Create Float Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1244,7 +1245,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
|
||||
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default'])
|
||||
->desc('Create Boolean Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
@@ -1443,7 +1444,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
|
||||
App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
|
||||
->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default'])
|
||||
->desc('Delete Attribute')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.write')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete')
|
||||
->label('audits.event', 'attribute.delete')
|
||||
@@ -1752,14 +1753,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
|
||||
$indexes = $collection->getAttribute('indexes');
|
||||
|
||||
// Search for index
|
||||
$indexIndex = array_search($key, array_map(fn($idx) => $idx['key'], $indexes));
|
||||
$indexIndex = array_search($key, array_column($indexes, 'key'));
|
||||
|
||||
if ($indexIndex === false) {
|
||||
throw new Exception(Exception::INDEX_NOT_FOUND);
|
||||
}
|
||||
|
||||
$index = $indexes[$indexIndex];
|
||||
$index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId);
|
||||
$index = new Document([\array_merge($indexes[$indexIndex], [
|
||||
'collectionId' => $database->getInternalId() . '_' . $collectionId,
|
||||
])]);
|
||||
|
||||
$response->dynamic($index, Response::MODEL_INDEX);
|
||||
});
|
||||
@@ -1855,10 +1857,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
->label('sdk.offline.model', '/databases/{databaseId}/collections/{collectionId}/documents')
|
||||
->label('sdk.offline.key', '{documentId}')
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('documentId', '', new CustomId(), 'Document 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('documentId', '', new CustomId(), 'Document 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('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object.')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default the current user is granted with all permissions. [Learn more about permissions](/docs/permissions).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
@@ -2251,7 +2253,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
|
||||
@@ -6,9 +6,9 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Network\Validator\CNAME;
|
||||
use Utopia\Validator\Domain as DomainValidator;
|
||||
use Appwrite\Network\Validator\Domain as DomainValidator;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Utopia\Validator\URL;
|
||||
use Appwrite\Network\Validator\URL;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
@@ -55,7 +55,7 @@ App::post('/v1/projects')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', 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('projectId', '', 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('name', null, new Text(128), 'Project name. Max length: 128 chars.')
|
||||
->param('teamId', '', new UID(), 'Team unique ID.')
|
||||
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn($config) => !$config['disabled']))), 'Project Region.', true)
|
||||
@@ -80,7 +80,7 @@ App::post('/v1/projects')
|
||||
}
|
||||
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
||||
$auths = ['limit' => 0, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
||||
foreach ($auth as $index => $method) {
|
||||
$auths[$method['key'] ?? ''] = true;
|
||||
}
|
||||
@@ -169,9 +169,22 @@ App::post('/v1/projects')
|
||||
$dbForProject->createCollection($key, $attributes, $indexes);
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($project, Response::MODEL_PROJECT);
|
||||
if ($dbForProject->exists($dbForProject->getDefaultDatabase(), 'videos_profiles')) {
|
||||
foreach (Config::getParam('videos-profiles', []) as $profile) {
|
||||
Authorization::skip(function () use ($project, $profile, $dbForProject) {
|
||||
return $dbForProject->createDocument('videos_profiles', new Document([
|
||||
'name' => $profile['name'],
|
||||
'videoBitRate' => $profile['videoBitRate'],
|
||||
'audioBitRate' => $profile['audioBitRate'],
|
||||
'width' => $profile['width'],
|
||||
'height' => $profile['height']
|
||||
]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::get('/v1/projects')
|
||||
@@ -575,37 +588,6 @@ App::patch('/v1/projects/:projectId/auth/:method')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
||||
->desc('Update Project user sessions limit')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updateAuthSessionsLimit')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('limit', false, new Range(1, APP_LIMIT_USER_SESSIONS_MAX), 'Set the max number of users allowed in this project. Value allowed is between 1-' . APP_LIMIT_USER_SESSIONS_MAX . '. Default is ' . APP_LIMIT_USER_SESSIONS_DEFAULT)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['maxSessions'] = $limit;
|
||||
|
||||
$dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::delete('/v1/projects/:projectId')
|
||||
->desc('Delete Project')
|
||||
->groups(['api', 'projects'])
|
||||
@@ -1114,7 +1096,7 @@ App::post('/v1/projects/:projectId/platforms')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PLATFORM)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('type', null, new WhiteList([Origin::CLIENT_TYPE_WEB, Origin::CLIENT_TYPE_FLUTTER_WEB, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_LINUX, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_APPLE_IOS, Origin::CLIENT_TYPE_APPLE_MACOS, Origin::CLIENT_TYPE_APPLE_WATCHOS, Origin::CLIENT_TYPE_APPLE_TVOS, Origin::CLIENT_TYPE_ANDROID, Origin::CLIENT_TYPE_UNITY], true), 'Platform type.')
|
||||
->param('type', null, new WhiteList([Origin::CLIENT_TYPE_WEB, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_LINUX, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_APPLE_IOS, Origin::CLIENT_TYPE_APPLE_MACOS, Origin::CLIENT_TYPE_APPLE_WATCHOS, Origin::CLIENT_TYPE_APPLE_TVOS, Origin::CLIENT_TYPE_ANDROID, Origin::CLIENT_TYPE_UNITY], true), 'Platform type.')
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
|
||||
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
|
||||
|
||||
@@ -567,6 +567,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
'$id' => $fileId,
|
||||
'$permissions' => $permissions,
|
||||
'bucketId' => $bucket->getId(),
|
||||
'bucketInternalId' => $bucket->getInternalId(),
|
||||
'name' => $fileName,
|
||||
'path' => $path,
|
||||
'signature' => $fileHash,
|
||||
@@ -616,6 +617,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' => '',
|
||||
|
||||
@@ -7,7 +7,7 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Validator\Host;
|
||||
use Appwrite\Network\Validator\Host;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries;
|
||||
@@ -54,7 +54,7 @@ App::post('/v1/teams')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TEAM)
|
||||
->param('teamId', '', new CustomId(), 'Team 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('teamId', '', new CustomId(), 'Team 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('name', null, new Text(128), 'Team name. Max length: 128 chars.')
|
||||
->param('roles', ['owner'], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', true)
|
||||
->inject('response')
|
||||
@@ -439,37 +439,14 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$url = Template::unParseURL($url);
|
||||
|
||||
if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode
|
||||
$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 = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
|
||||
$body->setParam('{{owner}}', $user->getAttribute('name'));
|
||||
$body->setParam('{{team}}', $team->getAttribute('name'));
|
||||
|
||||
$body
|
||||
->setParam('{{subject}}', $subject)
|
||||
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
|
||||
->setParam('{{name}}', $user->getAttribute('name'))
|
||||
->setParam('{{body}}', $locale->getText("emails.invitation.body"))
|
||||
->setParam('{{redirect}}', $url)
|
||||
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
|
||||
->setParam('{{signature}}', $locale->getText("emails.invitation.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)
|
||||
->setRecipient($invitee->getAttribute('email'))
|
||||
->setName($invitee->getAttribute('name'))
|
||||
->setType(MAIL_TYPE_INVITATION)
|
||||
->setRecipient($email)
|
||||
->setUrl($url)
|
||||
->setName($name)
|
||||
->setLocale($locale->default)
|
||||
->setTeam($team)
|
||||
->setUser($user)
|
||||
->trigger()
|
||||
;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -217,7 +217,6 @@ App::init()
|
||||
|
||||
if ($type === 'bucket') {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
@@ -244,14 +243,12 @@ App::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
|
||||
->addHeader('X-Appwrite-Cache', 'hit')
|
||||
->setContentType($data['contentType'])
|
||||
->send(base64_decode($data['payload']))
|
||||
;
|
||||
|
||||
$route->setIsActive(false);
|
||||
} else {
|
||||
$response->addHeader('X-Appwrite-Cache', 'miss');
|
||||
@@ -501,6 +498,7 @@ App::shutdown()
|
||||
$cacheLog = $dbForProject->getDocument('cache', $key);
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
|
||||
$now = DateTime::now();
|
||||
|
||||
if ($cacheLog->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
|
||||
+32
-25
@@ -113,6 +113,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`
|
||||
@@ -161,6 +162,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';
|
||||
@@ -174,7 +176,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));
|
||||
@@ -208,6 +210,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', '');
|
||||
@@ -502,7 +505,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', '');
|
||||
@@ -511,20 +514,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
|
||||
);
|
||||
|
||||
@@ -543,17 +546,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', '');
|
||||
|
||||
@@ -567,7 +570,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);
|
||||
|
||||
@@ -608,7 +611,7 @@ $register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2023-01.mmdb');
|
||||
});
|
||||
$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', '');
|
||||
@@ -627,7 +630,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);
|
||||
@@ -886,6 +889,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
|
||||
/** @var Utopia\Database\Document $console */
|
||||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
|
||||
$projectId = '64575eb69a8995fbde21';
|
||||
|
||||
if ($projectId === 'console') {
|
||||
return $console;
|
||||
@@ -932,7 +936,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()}");
|
||||
@@ -959,6 +962,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
@@ -62,6 +62,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());
|
||||
@@ -720,9 +723,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,852 @@
|
||||
<?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\Query;
|
||||
use Utopia\Storage\Compression\Algorithms\GZIP;
|
||||
use Utopia\Storage\Compression\Algorithms\Zstd;
|
||||
use Captioning\Format\SubripFile;
|
||||
use Utopia\Storage\Device\Local;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
Console::title('Video V1 Worker');
|
||||
Console::success(APP_NAME . ' video worker v1 has started');
|
||||
|
||||
class VideosV1 extends Worker
|
||||
{
|
||||
/**
|
||||
* Rendition Status
|
||||
*/
|
||||
private const STATUS_START = 'started';
|
||||
private const STATUS_END = 'ended';
|
||||
private const STATUS_UPLOADING = 'uploading';
|
||||
private const STATUS_READY = 'ready';
|
||||
private const STATUS_ERROR = 'error';
|
||||
|
||||
private const OUTPUT_HLS = 'hls';
|
||||
private const OUTPUT_DASH = 'dash';
|
||||
|
||||
private string $basePath = '/tmp/';
|
||||
//private string $basePath = '/usr/src/code/tests/tmp/';
|
||||
private string $inDir;
|
||||
private string $inPath;
|
||||
private string $outDir;
|
||||
private string $outPath;
|
||||
private string $renditionName;
|
||||
private Database $database;
|
||||
private Document $video;
|
||||
private string $action;
|
||||
private Document $profile;
|
||||
private Document $project;
|
||||
private Document $file;
|
||||
private Document $bucket;
|
||||
private FFProbe $ffprobe;
|
||||
private FFMpeg $ffmpeg;
|
||||
private bool $isVideo = false;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "Video v1";
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$this->video = new Document($this->args['video'] ?? []);
|
||||
$this->profile = new Document($this->args['profile'] ?? []);
|
||||
$this->project = new Document($this->args['project'] ?? []);
|
||||
$this->action = $this->args['action'];
|
||||
$this->basePath .= uniqid();
|
||||
$this->inDir = $this->basePath . '/in/';
|
||||
$this->outDir = $this->basePath . '/out/';
|
||||
@mkdir($this->inDir, 0755, true);
|
||||
@mkdir($this->outDir, 0755, true);
|
||||
$this->outPath = $this->outDir . $this->video->getId();
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
|
||||
$this->database = $this->getProjectDB($this->project->getId());
|
||||
$this->bucket = $this->database->getDocument('buckets', $this->video->getAttribute('bucketId'));
|
||||
$this->file = $this->database->getDocument('bucket_' . $this->bucket->getInternalId(), $this->video->getAttribute('fileId'));
|
||||
$path = basename($this->file->getAttribute('path'));
|
||||
$this->inPath = $this->inDir . $path;
|
||||
|
||||
/**
|
||||
* Write original asset to tmp
|
||||
*/
|
||||
$result = $this->write($this->project, $this->file);
|
||||
|
||||
if (empty($result)) {
|
||||
console::error('Storage transfer error');
|
||||
}
|
||||
|
||||
$this->renditionName = $this->profile->getAttribute('width')
|
||||
. 'X' . $this->profile->getAttribute('height')
|
||||
. '@' . ($this->profile->getAttribute('videoBitRate') + $this->profile->getAttribute('audioBitRate'));
|
||||
|
||||
/**
|
||||
* FFMpeg init
|
||||
*/
|
||||
$this->ffprobe = FFProbe::create();
|
||||
$this->ffmpeg = FFMpeg::create([
|
||||
'timeout' => 0,
|
||||
'ffmpeg.threads' => 12
|
||||
]);
|
||||
|
||||
if (!$this->ffprobe->isValid($this->inPath)) {
|
||||
console::error('Not an valid Video file "' . $this->inPath . '"');
|
||||
}
|
||||
|
||||
if (empty($this->video->getAttribute('duration'))) {
|
||||
/**
|
||||
* Original asset metadata
|
||||
*/
|
||||
$mediaInfo = new MediaInfo();
|
||||
$mediaInfoContainer = $mediaInfo->getInfo($this->inPath);
|
||||
$general = $mediaInfoContainer->getGeneral();
|
||||
$this->video
|
||||
->setAttribute('duration', $general->has('duration') ? $general->get('duration')->getMilliseconds() : 0)
|
||||
->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : '');
|
||||
|
||||
foreach ($mediaInfoContainer->getVideos() ?? [] as $video) {
|
||||
$this->isVideo = true;
|
||||
$this->video
|
||||
->setAttribute('height', $video->has('height') ? $video->get('height')->getAbsoluteValue() : 0)
|
||||
->setAttribute('width', $video->has('width') ? $video->get('width')->getAbsoluteValue() : 0)
|
||||
->setAttribute('aspectRatio', $video->has('display_aspect_ratio') ? $video->get('display_aspect_ratio')->getTextValue() : '')
|
||||
->setAttribute('videoFormat', $video->has('format') ? $video->get('format')->getShortName() : '')
|
||||
->setAttribute('videoFormatProfile', $video->has('format_profile') ? $video->get('format_profile') : '')
|
||||
->setAttribute('videoFrameRate', $video->has('frame_rate') ? strval($video->get('frame_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('videoFrameRateMode', $video->has('frame_rate_mode') ? $video->get('frame_rate_mode')->getFullName() : '')
|
||||
->setAttribute('videoBitRate', $video->has('bit_rate') ? $video->get('bit_rate')->getAbsoluteValue() : 0);
|
||||
}
|
||||
|
||||
foreach ($mediaInfoContainer->getAudios() ?? [] as $audio) {
|
||||
$this->video
|
||||
->setAttribute('audioFormat', $audio->has('format') ? strval($audio->get('format')->getShortName()) : '')
|
||||
->setAttribute('audioSampleRate', $audio->has('sampling_rate') ? strval($audio->get('sampling_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('audioBitRate', $audio->has('bit_rate') ? $audio->get('bit_rate')->getAbsoluteValue() : 0);
|
||||
}
|
||||
|
||||
console::info('Input video id: ' . $this->video->getId() . PHP_EOL .
|
||||
'Input name: ' . $this->file->getAttribute('name') . PHP_EOL .
|
||||
'Input width: ' . $this->video->getAttribute('width') . ' px' . PHP_EOL .
|
||||
'Input height: ' . $this->video->getAttribute('height') . ' px' . PHP_EOL .
|
||||
'Input duration: ' . ($this->video->getAttribute('duration') / 1000) . ' Sec' . PHP_EOL .
|
||||
'Input size: ' . ($this->video->getAttribute('size') / 1024 / 1024) . ' MiB' . PHP_EOL .
|
||||
'Input videoBitRate: ' . ($this->video->getAttribute('videoBitRate') / 1000) . ' kb/s' . PHP_EOL .
|
||||
'Input audioBitRate: ' . ($this->video->getAttribute('audioBitRate') / 1000) . ' kb/s' . PHP_EOL);
|
||||
|
||||
$this->database->updateDocument(
|
||||
'videos',
|
||||
$this->video->getId(),
|
||||
new document(
|
||||
array_filter((array)$this->video, fn($value) => !is_null($value))
|
||||
),
|
||||
);
|
||||
}
|
||||
$this->video->getAttribute('videoBitRate');
|
||||
$media = $this->ffmpeg->open($this->inPath);
|
||||
|
||||
switch ($this->action) {
|
||||
case 'preview':
|
||||
$this->createPreview($media);
|
||||
break;
|
||||
|
||||
case 'timeline':
|
||||
$this->createTimeLine($media);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->createOutput($media);
|
||||
break;
|
||||
}
|
||||
|
||||
unset($media);
|
||||
}
|
||||
|
||||
|
||||
private function createPreview($media): void
|
||||
{
|
||||
|
||||
if (!$this->isVideo) {
|
||||
return;
|
||||
}
|
||||
console::info('Creating preview image from second ' . $this->args['second']);
|
||||
|
||||
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/preview/';
|
||||
$name = 'preview.jpg';
|
||||
$media
|
||||
->filters()
|
||||
->resize(new \FFMpeg\Coordinate\Dimension($this->video->getAttribute('width'), $this->video->getAttribute('height')))
|
||||
->synchronize();
|
||||
$media
|
||||
->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($this->args['second']))
|
||||
->save($this->outDir . $name);
|
||||
|
||||
/**
|
||||
* Upload preview
|
||||
*/
|
||||
console::info('Uploading ' . $name);
|
||||
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$path . $name,
|
||||
(new Local('/'))->read($this->outDir . $name),
|
||||
mime_content_type($this->outDir . $name)
|
||||
);
|
||||
|
||||
$preview = $this->database->findOne('videos_previews', [
|
||||
Query::equal('videoId', [$this->video->getId()]),
|
||||
Query::equal('type', [$this->action]),
|
||||
Query::equal('name', [$name]),
|
||||
]);
|
||||
|
||||
if (empty($preview)) {
|
||||
$preview = $this->database->createDocument('videos_previews', new Document([
|
||||
'videoId' => $this->video->getId(),
|
||||
'videoInternalId' => $this->video->getInternalId(),
|
||||
'type' => $this->action,
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
'second' => $this->args['second'],
|
||||
]));
|
||||
|
||||
$this->video->setAttribute('previewId', $preview->getId());
|
||||
$this->database->updateDocument(
|
||||
'videos',
|
||||
$this->video->getId(),
|
||||
new document(
|
||||
array_filter((array)$this->video, fn($value) => !is_null($value))
|
||||
),
|
||||
);
|
||||
} else {
|
||||
$this->database->updateDocument(
|
||||
'videos_previews',
|
||||
$preview->getId(),
|
||||
$preview->setAttribute('second', $this->args['second'])
|
||||
);
|
||||
/**
|
||||
* Clean preview cache
|
||||
*/
|
||||
(new Delete())
|
||||
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
|
||||
->setResource('preview/' . $preview->getId())
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
private function createTimeLine($media): void
|
||||
{
|
||||
|
||||
/*
|
||||
* Title: PlayerJS Thumbnails & WebVTT Creator
|
||||
* URI: https://playerjs.com/docs/q=thumbnailsphpwebvtt
|
||||
* Version: 1.0
|
||||
* Author: Playerjs.com
|
||||
* Author URI: https://playerjs.com
|
||||
* License: GPL-2.0+
|
||||
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
||||
* Text Domain: playerjs
|
||||
*/
|
||||
if (!$this->isVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
$interval = 2;
|
||||
$ranges = [
|
||||
['from' => 120, 'to' => 600, 'interval' => 5],
|
||||
['from' => 600, 'to' => 1800, 'interval' => 10],
|
||||
['from' => 1800, 'to' => 3600, 'interval' => 20],
|
||||
['from' => 3600, 'to' => 99999, 'interval' => 30],
|
||||
];
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
if (
|
||||
$this->video->getAttribute('duration') > $range['from'] &&
|
||||
$this->video->getAttribute('duration') <= $range['to']
|
||||
) {
|
||||
$interval = $range['interval'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$timeline['aspect'] = $this->video->getAttribute('width') / $this->video->getAttribute('height');
|
||||
$timeline['width'] = 160;
|
||||
$timeline['height'] = round($timeline['width'] / $timeline['aspect']);
|
||||
$timeline['size'] = '5x5';
|
||||
|
||||
$cmd = [
|
||||
'/usr/bin/ffmpeg',
|
||||
'-i ' . $this->inPath,
|
||||
'-hide_banner',
|
||||
'-loglevel error',
|
||||
'-vsync vfr',
|
||||
'-vf \'select=isnan(prev_selected_t)+gte(t-prev_selected_t\,' . $interval . '),scale=' . $timeline['width'] . ':' . $timeline['height'] . ',tile=' . $timeline['size'] . '\'',
|
||||
'-qscale:v 3',
|
||||
$this->outDir . 'sprite%d.jpg',
|
||||
];
|
||||
$result = shell_exec(implode(" ", $cmd));
|
||||
|
||||
/**
|
||||
* Vtt creation*
|
||||
*/
|
||||
if ($result !== false) {
|
||||
$size = explode('x', $timeline['size']);
|
||||
$counter = 0;
|
||||
$images = ceil((($this->video->getAttribute('duration') / 1000) / $interval) / ($size[0] * $size[1]));
|
||||
$data = "WEBVTT";
|
||||
$path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/timeline/';
|
||||
for ($image = 1; $image <= $images; $image++) {
|
||||
$sprite = $this->database->createDocument('videos_previews', new Document([
|
||||
'videoId' => $this->video->getId(),
|
||||
'videoInternalId' => $this->video->getInternalId(),
|
||||
'type' => 'sprite',
|
||||
'name' => 'sprite' . $image . '.jpg',
|
||||
'path' => $path,
|
||||
]));
|
||||
|
||||
$url = TMP_HOST . 'v1/videos/' . $this->video->getId() . '/preview/' . $sprite->getId() . '/';
|
||||
for ($col = 0; $col < $size[0]; $col++) {
|
||||
for ($row = 0; $row < $size[1]; $row++) {
|
||||
$data .= "\n" . gmdate("H:i:s", $counter * $interval) . " --> " . gmdate("H:i:s", ($counter + 1) * $interval) . "\n" . $url . "#xywh=" . ($row * $timeline['width']) . "," . ($col * $timeline['height']) . "," . $timeline['width'] . "," . $timeline['height'];
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($counter > 0) {
|
||||
/**
|
||||
* Upload vtt
|
||||
*/
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/timeline/timeline.vtt'),
|
||||
$data,
|
||||
'text/vtt'
|
||||
);
|
||||
|
||||
console::info('Uploading timeline vtt');
|
||||
|
||||
/**
|
||||
* Upload sprites*
|
||||
*/
|
||||
$dir = new DirectoryIterator($this->outDir);
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (!$fileinfo->isDot()) {
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$path . $fileinfo->getFilename(),
|
||||
(new Local('/'))->read($this->outDir . $fileinfo->getFilename()),
|
||||
mime_content_type($this->outDir . $fileinfo->getFilename())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createOutput($media): void
|
||||
{
|
||||
$subs = [];
|
||||
$subtitles = $this->database->find('videos_subtitles', [
|
||||
Query::equal('videoId', [$this->video->getId()]),
|
||||
Query::equal('status', [''])
|
||||
]);
|
||||
|
||||
foreach ($subtitles as $subtitle) {
|
||||
$subtitle->setAttribute('status', self::STATUS_START);
|
||||
$this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle);
|
||||
$bucket = $this->database->getDocument('buckets', $subtitle->getAttribute('bucketId'));
|
||||
$file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId'));
|
||||
$path = basename($file->getAttribute('path'));
|
||||
$this->write($this->project, $file);
|
||||
|
||||
console::info('Transferring subtitle from storage to ' . $this->inDir);
|
||||
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
||||
$subtitlePath = $this->inDir . $subtitle->getId() . '.vtt';
|
||||
|
||||
if ($ext === 'srt') {
|
||||
$srt = new SubripFile($this->inDir . $path);
|
||||
$srt->convertTo('webvtt')->save($subtitlePath);
|
||||
}
|
||||
|
||||
$subs[] = [
|
||||
'name' => $subtitle->getAttribute('name'),
|
||||
'code' => $subtitle->getAttribute('code'),
|
||||
'path' => $subtitlePath,
|
||||
];
|
||||
}
|
||||
|
||||
$query = $this->database->createDocument('videos_renditions', new Document([
|
||||
'videoId' => $this->video->getId(),
|
||||
'videoInternalId' => $this->video->getInternalId(),
|
||||
'profileId' => $this->profile->getId(),
|
||||
'profileInternalId' => $this->profile->getInternalId(),
|
||||
'name' => $this->renditionName,
|
||||
'startedAt' => DateTime::now(),
|
||||
'status' => self::STATUS_START,
|
||||
'width' => $this->profile->getAttribute('width'),
|
||||
'height' => $this->profile->getAttribute('height'),
|
||||
'videoBitRate' => $this->profile->getAttribute('videoBitRate'),
|
||||
'audioBitRate' => $this->profile->getAttribute('audioBitRate'),
|
||||
'output' => $this->args['output'],
|
||||
]));
|
||||
|
||||
$this->send($query);
|
||||
|
||||
$renditionRootPath = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/';
|
||||
$renditionPath = $renditionRootPath . $this->renditionName . '-' . $query->getId() . '/';
|
||||
|
||||
try {
|
||||
$representation = (new Representation())
|
||||
->setKiloBitRate($this->profile->getAttribute('videoBitRate'))
|
||||
->setAudioKiloBitRate($this->profile->getAttribute('audioBitRate'))
|
||||
->setResize($this->profile->getAttribute('width'), $this->profile->getAttribute('height'));
|
||||
|
||||
console::info('Output video id:' . $this->video->getId() . PHP_EOL .
|
||||
'Output name: ' . $this->file->getAttribute('name') . PHP_EOL .
|
||||
'Output width: ' . $this->profile->getAttribute('width') . PHP_EOL .
|
||||
'Output height: ' . $this->profile->getAttribute('height') . PHP_EOL .
|
||||
'Output video BitRate:' . $this->profile->getAttribute('videoBitRate') . PHP_EOL .
|
||||
'Output audio BitRate:' . $this->profile->getAttribute('audioBitRate') . PHP_EOL .
|
||||
'Output:' . $this->args['output'] . PHP_EOL);
|
||||
|
||||
$format = new Streaming\Format\X264();
|
||||
$format->on('progress', function ($media, $format, $percentage) use ($query) {
|
||||
if ($percentage % 3 === 0) {
|
||||
$query->setAttribute('progress', (string)$percentage);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
}
|
||||
});
|
||||
|
||||
$this->transcode($media, $format, $representation, $subs);
|
||||
|
||||
//exec('/usr/bin/ffmpeg -y -i /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/in/637f59ab5bce0e36d05e.mp4 -c:v libx264 -c:a aac -bf 1 -keyint_min 25 -g 250 -sc_threshold 40 -use_timeline 0 -use_template 0 -seg_duration 10 -hls_playlist 0 -f dash -dn -sn -vf scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1 -b_strategy 1 -bf 3 -force_key_frames "expr:gte(t,n_forced*2)" -map 0 -s:v:0 1024x576 -b:v:0 2538k -b:a:0 128k -strict -2 -threads 12 /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/out/637f59c88f9ff0fe3b1f.mpd2>&1', $o, $v);
|
||||
//var_dump($o);
|
||||
//var_dump($v);
|
||||
|
||||
if ($this->args['output'] === self::OUTPUT_HLS) {
|
||||
$streams = $this->getHlsSegmentsUrls($this->outDir . 'master.m3u8');
|
||||
foreach ($streams as $stream) {
|
||||
$m3u8 = $this->getSegments($this->outDir . $stream['path']);
|
||||
if (!empty($m3u8['segments'])) {
|
||||
foreach ($m3u8['segments'] as $segment) {
|
||||
$this->database->createDocument('videos_renditions_segments', new Document([
|
||||
'renditionId' => $query->getId(),
|
||||
'renditionInternalId' => $query->getInternalId(),
|
||||
'streamId' => (int)$stream['id'],
|
||||
'fileName' => $segment['fileName'],
|
||||
'path' => $renditionPath,
|
||||
'duration' => $segment['duration'],
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
$query->setAttribute('metadata', json_encode(['hls' => $streams]));
|
||||
$query->setAttribute('targetDuration', $m3u8['targetDuration']);
|
||||
}
|
||||
} else {
|
||||
$mpd = $this->getSegments($this->outPath . '.mpd');
|
||||
|
||||
if (!empty($mpd['segments'])) {
|
||||
foreach ($mpd['segments'] as $segment) {
|
||||
$this->database->createDocument('videos_renditions_segments', new Document([
|
||||
'renditionId' => $query->getId(),
|
||||
'renditionInternalId' => $query->getInternalId(),
|
||||
'streamId' => $segment['streamId'],
|
||||
'fileName' => $segment['fileName'],
|
||||
'path' => $renditionPath,
|
||||
'isInit' => $segment['isInit'],
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($mpd['metadata'])) {
|
||||
$query->setAttribute('metadata', json_encode(['mpd' => $mpd['metadata']]));
|
||||
}
|
||||
}
|
||||
|
||||
$query->setAttribute('status', self::STATUS_END);
|
||||
$query->setAttribute('endedAt', DateTime::now());
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
|
||||
foreach ($subtitles ?? [] as $subtitle) {
|
||||
if ($this->args['output'] === self::OUTPUT_HLS) {
|
||||
$m3u8 = $this->getSegments($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8');
|
||||
foreach ($m3u8['segments'] ?? [] as $segment) {
|
||||
$this->database->createDocument('videos_subtitles_segments', new Document([
|
||||
'subtitleId' => $subtitle->getId(),
|
||||
'subtitleInternalId' => $subtitle->getInternalId(),
|
||||
'fileName' => $segment['fileName'],
|
||||
'path' => $renditionRootPath . 'subtitles/',
|
||||
'duration' => $segment['duration'],
|
||||
]));
|
||||
}
|
||||
$subtitle->setAttribute('targetDuration', $m3u8['targetDuration']);
|
||||
} else {
|
||||
$this->getFilesDevice($this->project->getId())->transfer($this->inDir . $subtitle->getId() . '.vtt', $this->outDir . $subtitle->getId() . '.vtt', $this->getFilesDevice($this->project->getId()));
|
||||
}
|
||||
|
||||
$subtitle->setAttribute('status', self::STATUS_READY);
|
||||
$subtitle->setAttribute('path', $renditionRootPath . 'subtitles/');
|
||||
$this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle);
|
||||
}
|
||||
|
||||
console::info('Rendition ' . $query->getId() . ' conversion, done');
|
||||
|
||||
/** Upload **/
|
||||
$dir = new DirectoryIterator($this->outDir);
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (!$fileinfo->isDot()) {
|
||||
$data = (new Local('/'))->read($this->outDir . $fileinfo->getFilename());
|
||||
$to = $renditionPath;
|
||||
if (str_contains($fileinfo->getFilename(), "_subtitles_") || str_contains($fileinfo->getFilename(), ".vtt")) {
|
||||
$to = $renditionRootPath . 'subtitles/';
|
||||
}
|
||||
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$to . $fileinfo->getFilename(),
|
||||
$data,
|
||||
mime_content_type($this->outDir . $fileinfo->getFilename())
|
||||
);
|
||||
|
||||
if ($fileinfo->key() === 0) {
|
||||
$query->setAttribute('progress', '100');
|
||||
$query->setAttribute('status', self::STATUS_UPLOADING);
|
||||
$query->setAttribute('path', $renditionPath);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query->setAttribute('status', self::STATUS_READY);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
} catch (\Throwable $th) {
|
||||
$query->setAttribute('metadata', json_encode([
|
||||
'code' => $th->getCode(),
|
||||
'message' => substr($th->getMessage(), 0, 255),
|
||||
]));
|
||||
|
||||
$query->setAttribute('status', self::STATUS_ERROR);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
|
||||
console::error('Error video id:' . $this->video->getId() . PHP_EOL
|
||||
. 'Message: ' . $th->getMessage() . PHP_EOL
|
||||
. 'File: ' . $th->getFile() . PHP_EOL
|
||||
. 'line: ' . $th->getLine() . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $media Media
|
||||
* @param $format StreamFormat
|
||||
* @param $representation Representation
|
||||
* @param array $subtitles
|
||||
* @return void
|
||||
*/
|
||||
private function transcode(Media $media, StreamFormat $format, Representation $representation, array $subtitles): void
|
||||
{
|
||||
|
||||
$additionalParams = [
|
||||
'-dn',
|
||||
'-sn',
|
||||
'-vf', 'scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1',
|
||||
'-bf', '3',
|
||||
'-force_key_frames', 'expr:gte(t,n_forced*2)' //enforce strict key frame
|
||||
];
|
||||
|
||||
$segmentSize = 8;
|
||||
if ($this->args['output'] === self::OUTPUT_DASH) {
|
||||
$media->dash()
|
||||
->setFormat($format)
|
||||
->setSegDuration($segmentSize)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->save($this->outPath);
|
||||
return;
|
||||
}
|
||||
|
||||
$hls = $media->hls();
|
||||
foreach ($subtitles as $subtitle) {
|
||||
$sub = new HLSSubtitle($subtitle['path'], $subtitle['name'], $subtitle['code']);
|
||||
$sub->default();
|
||||
$hls->subtitle($sub);
|
||||
}
|
||||
|
||||
$hls->setFormat($format)
|
||||
->setHlsTime($segmentSize)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->save($this->outPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
private function getSegments(string $path): array
|
||||
{
|
||||
|
||||
$segments = [];
|
||||
if ($this->args['output'] === self::OUTPUT_DASH) {
|
||||
$metadata = null;
|
||||
$handle = fopen($path, "r");
|
||||
if ($handle) {
|
||||
$streamId = -1;
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = str_replace([",", "\r", "\n"], "", $line);
|
||||
if (str_contains($line, "<AdaptationSet")) {
|
||||
$streamId++;
|
||||
}
|
||||
|
||||
if (!str_contains($line, "SegmentURL") && !str_contains($line, "Initialization")) {
|
||||
$metadata .= $line . PHP_EOL;
|
||||
} else {
|
||||
$segments[] = [
|
||||
'isInit' => str_contains($line, "Initialization") ? 1 : 0,
|
||||
'streamId' => $streamId,
|
||||
'fileName' => trim(str_replace(["<SegmentURL media=\"", "<Initialization sourceURL=\"", "\"/>", "\" />"], "", $line)),
|
||||
];
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
return [
|
||||
'metadata' => $metadata,
|
||||
'segments' => $segments
|
||||
];
|
||||
}
|
||||
|
||||
$targetDuration = 0;
|
||||
$handle = fopen($path, "r");
|
||||
if ($handle) {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = str_replace([",","\r","\n"], "", $line);
|
||||
if (str_contains($line, "#EXT-X-TARGETDURATION")) {
|
||||
$targetDuration = str_replace(["#EXT-X-TARGETDURATION:"], "", $line);
|
||||
}
|
||||
if (str_contains($line, "#EXTINF")) {
|
||||
$duration = str_replace(["#EXTINF:"], "", $line);
|
||||
}
|
||||
if (str_contains($line, ".ts") || str_contains($line, ".vtt")) {
|
||||
if (!empty($duration)) {
|
||||
$segments[] = [
|
||||
'fileName' => $line,
|
||||
'duration' => $duration
|
||||
];
|
||||
$duration = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
return [
|
||||
'targetDuration' => $targetDuration,
|
||||
'segments' => $segments
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
private function getHlsSegmentsUrls(string $path): array
|
||||
{
|
||||
|
||||
$files = [];
|
||||
$handle = fopen($path, "r");
|
||||
if ($handle) {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$tmp = [];
|
||||
$line = str_replace(['"'], '', $line);
|
||||
$attributes = explode(',', $line);
|
||||
foreach ($attributes as $key => $attribute) {
|
||||
$parts = explode('=', $attribute);
|
||||
switch (true) {
|
||||
case str_contains($parts[0], 'LANGUAGE'):
|
||||
$attr['language'] = $parts[1];
|
||||
break;
|
||||
case str_contains($parts[0], 'BANDWIDTH'):
|
||||
$attr['bandwidth'] = $parts[1];
|
||||
break;
|
||||
case str_contains($parts[0], 'RESOLUTION'):
|
||||
$attr['resolution'] = $parts[1];
|
||||
break;
|
||||
case str_contains($parts[0], 'CODECS'):
|
||||
$attr['codecs'] = trim($parts[1], " \n\r\t\v\x00");
|
||||
|
||||
if (!empty($attributes[$key + 1])) {
|
||||
$attr['codecs'] .= ',' . trim($attributes[$key + 1], " \n\r\t\v\x00");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$end = strpos($line, 'm3u8');
|
||||
if ($end !== false) {
|
||||
$start = strpos($line, $this->video->getId());
|
||||
if ($start !== false) {
|
||||
$path = substr($line, $start, ($end - $start) + 4);
|
||||
$parts = explode('_', $path);
|
||||
$tmp = [
|
||||
'id' => $parts[1],
|
||||
'path' => $path
|
||||
];
|
||||
if (str_contains($line, "TYPE=AUDIO")) {
|
||||
$tmp['type'] = 'audio';
|
||||
if (!empty($attr['language'])) {
|
||||
$tmp['language'] = $attr['language'];
|
||||
}
|
||||
} else {
|
||||
$tmp['type'] = 'video';
|
||||
if (!empty($attr['resolution'])) {
|
||||
$tmp['resolution'] = $attr['resolution'];
|
||||
}
|
||||
if (!empty($attr['bandwidth'])) {
|
||||
$tmp['bandwidth'] = $attr['bandwidth'];
|
||||
}
|
||||
if (!empty($attr['codecs'])) {
|
||||
$tmp['codecs'] = $attr['codecs'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$files[] = $tmp;
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send realtime updates
|
||||
*
|
||||
* @param string $action
|
||||
* @param Document $payload
|
||||
* @throws Exception
|
||||
*/
|
||||
private function send(Document $payload, string $action = 'create')
|
||||
{
|
||||
unset($payload['metadata']);
|
||||
|
||||
$allEvents = Event::generateEvents('videos.[videoId].renditions.[renditionId].' . $action, [
|
||||
'videoId' => $payload['videoId'],
|
||||
'renditionId' => $payload->getId()
|
||||
]);
|
||||
|
||||
$payload->setAttribute('$permissions', $this->bucket->getAttribute('fileSecurity', false)
|
||||
? \array_merge($this->bucket->getAttribute('$permissions'), $this->file->getAttribute('$permissions'))
|
||||
: $this->bucket->getAttribute('$permissions'));
|
||||
|
||||
$target = Realtime::fromPayload(
|
||||
event: $allEvents[0],
|
||||
payload: $payload
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $payload->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write files
|
||||
*
|
||||
* @param $project Document
|
||||
* @param $file Document
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function write(Document $project, Document $file): bool
|
||||
{
|
||||
console::info('Writing file to ' . $this->inDir);
|
||||
|
||||
$fullPath = $file->getAttribute('path');
|
||||
$path = basename($file->getAttribute('path'));
|
||||
$local = new Local('/');
|
||||
if (
|
||||
!empty($file->getAttribute('openSSLCipher')) ||
|
||||
$file->getAttribute('algorithm', 'none') !== 'none'
|
||||
) {
|
||||
$data = $this->getFilesDevice($project->getId())->read($fullPath);
|
||||
if (!empty($file->getAttribute('openSSLCipher'))) {
|
||||
$data = OpenSSL::decrypt(
|
||||
$data,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
\hex2bin($file->getAttribute('openSSLIV')),
|
||||
\hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
$algorithm = $file->getAttribute('algorithm', 'none');
|
||||
switch ($algorithm) {
|
||||
case 'zstd':
|
||||
$compressor = new Zstd();
|
||||
$data = $compressor->decompress($data);
|
||||
break;
|
||||
case 'gzip':
|
||||
$compressor = new GZIP();
|
||||
$data = $compressor->decompress($data);
|
||||
break;
|
||||
}
|
||||
$result = $local
|
||||
->write($this->inDir . $path, $data, $file->getAttribute('mimeType'));
|
||||
} else {
|
||||
$result = $this->getFilesDevice(
|
||||
$project->getId()
|
||||
)->transfer($fullPath, $this->inDir . $path, $local);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function cleanup(): bool
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$stdin = '';
|
||||
|
||||
return Console::execute("rm -rf {$this->basePath}", $stdin, $stdout, $stderr, 3) === 0;
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$result = $this->cleanup();
|
||||
if (!$result) {
|
||||
Console::error('Failed Removing files from [' . $this->basePath . ']');
|
||||
}
|
||||
Console::info('Removing files from [' . $this->basePath . ']');
|
||||
}
|
||||
}
|
||||
@@ -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.18.*",
|
||||
@@ -59,7 +60,7 @@
|
||||
"utopia-php/messaging": "0.1.*",
|
||||
"utopia-php/orchestration": "0.6.*",
|
||||
"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.29.*",
|
||||
"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
+1255
-249
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
|
||||
@@ -847,6 +911,7 @@ volumes:
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
appwrite-uploads:
|
||||
appwrite-videos:
|
||||
appwrite-certificates:
|
||||
appwrite-functions:
|
||||
appwrite-builds:
|
||||
|
||||
@@ -31,6 +31,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,167 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Resque;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Video extends Event
|
||||
{
|
||||
protected ?Document $video = null;
|
||||
protected ?Document $profile = null;
|
||||
protected int $second = 5;
|
||||
protected string $output = '';
|
||||
protected string $action = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Event::VIDEO_QUEUE_NAME, Event::VIDEO_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets output.
|
||||
*
|
||||
* @param string $output
|
||||
* @return self
|
||||
*/
|
||||
public function setOutput(string $output): self
|
||||
{
|
||||
$this->output = $output;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns output.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutput(): string
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets action.
|
||||
*
|
||||
* @param string $action
|
||||
* @return self
|
||||
*/
|
||||
public function setAction(string $action): self
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets video.
|
||||
*
|
||||
* @param Document $video
|
||||
* @return self
|
||||
*/
|
||||
public function setVideo(Document $video): self
|
||||
{
|
||||
$this->video = $video;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns video.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getVideo(): ?string
|
||||
{
|
||||
return $this->video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets profile.
|
||||
*
|
||||
* @param Document $profile
|
||||
* @return self
|
||||
*/
|
||||
public function setProfile(Document $profile): self
|
||||
{
|
||||
$this->profile = $profile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns profile.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getProfile(): ?Document
|
||||
{
|
||||
return $this->profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets second from duration.
|
||||
*
|
||||
* @param int $second
|
||||
* @return self
|
||||
*/
|
||||
public function setSecond(int $second): self
|
||||
{
|
||||
$this->second = $second;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns second from duration.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSecond(): int
|
||||
{
|
||||
return $this->second;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the function event and sends it to the functions worker.
|
||||
*
|
||||
* @return string|bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
$keys = [
|
||||
'action' => $this->action,
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'video' => $this->video,
|
||||
'second' => $this->getSecond(),
|
||||
];
|
||||
|
||||
if (!empty($this->getOutput())) {
|
||||
$keys = array_merge($keys, [
|
||||
'output' => $this->getOutput(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($this->getProfile())) {
|
||||
$keys = array_merge($keys, [
|
||||
'profile' => $this->getProfile(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Resque::enqueue($this->queue, $this->class, $keys);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class Exception extends \Exception
|
||||
* - Keys
|
||||
* - Platform
|
||||
* - Domain
|
||||
* * Video
|
||||
* - GraphQL
|
||||
*/
|
||||
|
||||
@@ -179,6 +180,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
|
||||
|
||||
@@ -83,6 +83,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()
|
||||
@@ -158,6 +162,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';
|
||||
@@ -274,6 +288,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())
|
||||
@@ -341,6 +359,11 @@ class Response extends SwooleResponse
|
||||
->setModel(new UsageFunctions())
|
||||
->setModel(new UsageFunction())
|
||||
->setModel(new UsageProject())
|
||||
->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;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,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', 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', 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