Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
·
5acd9c3
0
Parent(s):
initial commit log 🪵🦫
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +9 -0
- .gitignore +53 -0
- .metadata +45 -0
- DEPLOYMENT.md +73 -0
- Dockerfile +52 -0
- NOTES.md +0 -0
- README.md +48 -0
- analysis_options.yaml +28 -0
- android/.gitignore +13 -0
- android/app/build.gradle +44 -0
- android/app/src/debug/AndroidManifest.xml +7 -0
- android/app/src/main/AndroidManifest.xml +45 -0
- android/app/src/main/kotlin/com/example/aitube2/MainActivity.kt +5 -0
- android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- android/app/src/main/res/drawable/launch_background.xml +12 -0
- android/app/src/main/res/mipmap-hdpi/ic_launcher.png +3 -0
- android/app/src/main/res/mipmap-mdpi/ic_launcher.png +3 -0
- android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +3 -0
- android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +3 -0
- android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +3 -0
- android/app/src/main/res/values-night/styles.xml +18 -0
- android/app/src/main/res/values/styles.xml +18 -0
- android/app/src/profile/AndroidManifest.xml +7 -0
- android/build.gradle +18 -0
- android/gradle.properties +3 -0
- android/gradle/wrapper/gradle-wrapper.properties +5 -0
- android/settings.gradle +25 -0
- api.py +445 -0
- api_config.py +184 -0
- api_core.py +746 -0
- docs/for-bots/flutter-videos/fvp.md +223 -0
- docs/for-bots/flutter-videos/fvp_usage_example__main.dart +369 -0
- docs/for-bots/flutter-videos/fvp_usage_example__multi_textures.dart +116 -0
- docs/for-bots/flutter-videos/video_player.md +145 -0
- docs/for-bots/huggingface/hf-api.md +0 -0
- ios/.gitignore +34 -0
- ios/Flutter/AppFrameworkInfo.plist +26 -0
- ios/Flutter/Debug.xcconfig +2 -0
- ios/Flutter/Release.xcconfig +2 -0
- ios/Podfile +44 -0
- ios/Runner.xcodeproj/project.pbxproj +616 -0
- ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +98 -0
- ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- ios/Runner/AppDelegate.swift +13 -0
- ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
.gitattributes
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.ico filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# secret codes
|
2 |
+
lib/config/secrets.dart
|
3 |
+
run_locally_with_secrets.sh
|
4 |
+
|
5 |
+
# Python venv
|
6 |
+
.python_venv/
|
7 |
+
|
8 |
+
# Miscellaneous
|
9 |
+
*.class
|
10 |
+
*.log
|
11 |
+
*.pyc
|
12 |
+
*.swp
|
13 |
+
.DS_Store
|
14 |
+
.atom/
|
15 |
+
.buildlog/
|
16 |
+
.history
|
17 |
+
.svn/
|
18 |
+
migrate_working_dir/
|
19 |
+
|
20 |
+
# IntelliJ related
|
21 |
+
*.iml
|
22 |
+
*.ipr
|
23 |
+
*.iws
|
24 |
+
.idea/
|
25 |
+
|
26 |
+
# The .vscode folder contains launch configuration and tasks you configure in
|
27 |
+
# VS Code which you may wish to be included in version control, so this line
|
28 |
+
# is commented out by default.
|
29 |
+
#.vscode/
|
30 |
+
|
31 |
+
# Flutter/Dart/Pub related
|
32 |
+
**/doc/api/
|
33 |
+
**/ios/Flutter/.last_build_id
|
34 |
+
.dart_tool/
|
35 |
+
.flutter-plugins
|
36 |
+
.flutter-plugins-dependencies
|
37 |
+
.pub-cache/
|
38 |
+
.pub/
|
39 |
+
/build/
|
40 |
+
!/build/web
|
41 |
+
|
42 |
+
# Symbolication related
|
43 |
+
app.*.symbols
|
44 |
+
|
45 |
+
# Obfuscation related
|
46 |
+
app.*.map.json
|
47 |
+
|
48 |
+
# Android Studio will place build artifacts here
|
49 |
+
/android/app/debug
|
50 |
+
/android/app/profile
|
51 |
+
/android/app/release
|
52 |
+
|
53 |
+
/assets/config/
|
.metadata
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file tracks properties of this Flutter project.
|
2 |
+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
3 |
+
#
|
4 |
+
# This file should be version controlled and should not be manually edited.
|
5 |
+
|
6 |
+
version:
|
7 |
+
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
8 |
+
channel: "stable"
|
9 |
+
|
10 |
+
project_type: app
|
11 |
+
|
12 |
+
# Tracks metadata for the flutter migrate command
|
13 |
+
migration:
|
14 |
+
platforms:
|
15 |
+
- platform: root
|
16 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
17 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
18 |
+
- platform: android
|
19 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
20 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
21 |
+
- platform: ios
|
22 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
23 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
24 |
+
- platform: linux
|
25 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
26 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
27 |
+
- platform: macos
|
28 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
29 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
30 |
+
- platform: web
|
31 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
32 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
33 |
+
- platform: windows
|
34 |
+
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
35 |
+
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
36 |
+
|
37 |
+
# User provided section
|
38 |
+
|
39 |
+
# List of Local paths (relative to this file) that should be
|
40 |
+
# ignored by the migrate tool.
|
41 |
+
#
|
42 |
+
# Files that are not part of the templates will be ignored by default.
|
43 |
+
unmanaged_files:
|
44 |
+
- 'lib/main.dart'
|
45 |
+
- 'ios/Runner.xcodeproj/project.pbxproj'
|
DEPLOYMENT.md
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
## Deploying aitube2 to https://aitube.at
|
3 |
+
|
4 |
+
Note: this document is meant for aitube administrators only, not the general public.
|
5 |
+
|
6 |
+
Aitube is not an app/tool but a website, it is not designed to be cloned (technically you can, but since this is not a goal it is not documented).
|
7 |
+
|
8 |
+
### Setup the domain
|
9 |
+
|
10 |
+
TODO
|
11 |
+
|
12 |
+
### Seting up the Python virtual environment
|
13 |
+
|
14 |
+
```bash
|
15 |
+
python3 -m venv .python_venv
|
16 |
+
source .python_venv/bin/activate
|
17 |
+
python3 -m pip install --no-cache-dir --upgrade -r requirements.txt
|
18 |
+
```
|
19 |
+
|
20 |
+
### Deployment to production
|
21 |
+
|
22 |
+
To deploy the aitube2 api to production:
|
23 |
+
|
24 |
+
$ git push space main
|
25 |
+
|
26 |
+
To deploy the aitube2 client to production, simply run:
|
27 |
+
|
28 |
+
$ flutter run web
|
29 |
+
|
30 |
+
and upload the assets to:
|
31 |
+
|
32 |
+
https://huggingface.co/spaces/jbilcke-hf/aitube2/tree/main/public
|
33 |
+
|
34 |
+
#### Running a rendering node
|
35 |
+
|
36 |
+
Current aitube uses `jbilcke-hf/LTX-Video-0-9-6-HFIE` as a rendering node.
|
37 |
+
|
38 |
+
aitube uses a round-robin schedule implemented on the gateway.
|
39 |
+
This helps ensuring a smooth attribution of requests.
|
40 |
+
|
41 |
+
What works well is to use the target number of users in parallel (eg. 3) and use 50% more capability to make sure we can handle the load, so in this case about 5 servers.
|
42 |
+
|
43 |
+
```bash
|
44 |
+
# note: you need to replace <YOUR_ACCOUNT_NAME>, <ROUND_ROBIN_INDEX> and <YOUR_HF_TOKEN>
|
45 |
+
|
46 |
+
curl https://api.endpoints.huggingface.cloud/v2/endpoint/<YOUR_ACCOUNT_NAME> -X POST -d '{"cacheHttpResponses":false,"compute":{"accelerator":"gpu","instanceSize":"x1","instanceType":"nvidia-l40s","scaling":{"maxReplica":1,"measure":{"hardwareUsage":80},"minReplica":0,"scaleToZeroTimeout":120,"metric":"hardwareUsage"}},"model":{"env":{},"framework":"custom","image":{"huggingface":{}},"repository":"jbilcke-hf/LTX-Video-0-9-6-HFIE","secrets":{},"task":"custom","fromCatalog":false},"name":"ltx-video-0-9-6-round-robin-<ROUND_ROBIN_INDEX>","provider":{"region":"us-east-1","vendor":"aws"},"tags":[""],"type":"protected"}' -H "Content-Type: application/json" -H "Authorization: Bearer <YOUR_HF_TOKEN>"
|
47 |
+
```
|
48 |
+
|
49 |
+
#### Running the gateway scheduler
|
50 |
+
|
51 |
+
```bash
|
52 |
+
# load the environment
|
53 |
+
# (if you haven't done it already for this shell session)
|
54 |
+
source .python_venv/bin/activate
|
55 |
+
|
56 |
+
HF_TOKEN="<USE YOUR OWN TOKEN>" \
|
57 |
+
SECRET_TOKEN="<USE YOUR OWN TOKEN>" \
|
58 |
+
VIDEO_ROUND_ROBIN_SERVER_1="https:/<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
59 |
+
VIDEO_ROUND_ROBIN_SERVER_2="https://<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
60 |
+
VIDEO_ROUND_ROBIN_SERVER_3="https://<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
61 |
+
VIDEO_ROUND_ROBIN_SERVER_4="https://<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
62 |
+
HF_IMAGE_MODEL="https://<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
63 |
+
HF_TEXT_MODEL="https://<USE YOUR OWN SERVER>.endpoints.huggingface.cloud" \
|
64 |
+
python3 api.py
|
65 |
+
```
|
66 |
+
|
67 |
+
### Run the client (web)
|
68 |
+
|
69 |
+
```bash
|
70 |
+
|
71 |
+
flutter run --dart-define=CONFIG_PATH=assets/config/aitube_low.yaml -d chrome
|
72 |
+
```
|
73 |
+
|
Dockerfile
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvidia/cuda:12.4.0-devel-ubuntu22.04
|
2 |
+
|
3 |
+
ARG DEBIAN_FRONTEND=noninteractive
|
4 |
+
|
5 |
+
ENV PYTHONUNBUFFERED=1
|
6 |
+
|
7 |
+
RUN apt-get update && apt-get install --no-install-recommends -y \
|
8 |
+
build-essential \
|
9 |
+
python3.11 \
|
10 |
+
python3-pip \
|
11 |
+
python3-dev \
|
12 |
+
git \
|
13 |
+
curl \
|
14 |
+
ffmpeg \
|
15 |
+
libglib2.0-0 \
|
16 |
+
libsm6 \
|
17 |
+
libxrender1 \
|
18 |
+
libxext6 \
|
19 |
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
20 |
+
|
21 |
+
WORKDIR /code
|
22 |
+
|
23 |
+
COPY ./requirements.txt /code/requirements.txt
|
24 |
+
|
25 |
+
# Set up a new user named "user" with user ID 1000
|
26 |
+
RUN useradd -m -u 1000 user
|
27 |
+
# Switch to the "user" user
|
28 |
+
USER user
|
29 |
+
# Set home to the user's home directory
|
30 |
+
ENV HOME=/home/user \
|
31 |
+
PATH=/home/user/.local/bin:$PATH
|
32 |
+
|
33 |
+
# Set home to the user's home directory
|
34 |
+
ENV PYTHONPATH=$HOME/app \
|
35 |
+
PYTHONUNBUFFERED=1 \
|
36 |
+
DATA_ROOT=/tmp/data
|
37 |
+
|
38 |
+
RUN echo "Installing requirements.txt"
|
39 |
+
RUN pip3 install --no-cache-dir --upgrade -r /code/requirements.txt
|
40 |
+
|
41 |
+
# yeah.. this is manual for now
|
42 |
+
#RUN flutter build web
|
43 |
+
|
44 |
+
WORKDIR $HOME/app
|
45 |
+
|
46 |
+
COPY --chown=user . $HOME/app
|
47 |
+
|
48 |
+
EXPOSE 8080
|
49 |
+
|
50 |
+
ENV PORT 8080
|
51 |
+
|
52 |
+
CMD python3 api.py
|
NOTES.md
ADDED
File without changes
|
README.md
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: aitube2
|
3 |
+
emoji: 🎥
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: yellow
|
6 |
+
sdk: docker
|
7 |
+
app_file: api.py
|
8 |
+
pinned: true
|
9 |
+
short_description: A Latent YouTube
|
10 |
+
---
|
11 |
+
|
12 |
+
|
13 |
+
# AiTube2
|
14 |
+
|
15 |
+
## News
|
16 |
+
|
17 |
+
aitube2 is coming sooner than expected!
|
18 |
+
|
19 |
+
Stay hooked at @flngr on X!
|
20 |
+
|
21 |
+
|
22 |
+
## What is AiTube?
|
23 |
+
|
24 |
+
AiTube 2 is a reboot of [AiTube 1](https://x.com/danielpikl/status/1737882643625078835), a project made in 2023 which generated AI videos in the background using LLM agents, to simulate an AI generated YouTube.
|
25 |
+
|
26 |
+
In [AiTube 2](https://x.com/flngr/status/1864127796945011016), this concept is put upside down: now the content is generated on demand (when the user types something in the latent search input) and on the fly (video chunks are generated within a few seconds and streamed continuously).
|
27 |
+
|
28 |
+
This allows for new ways of consuming AI generated content, such as collaborative and interactive prompting.
|
29 |
+
|
30 |
+
# Where can I use it?
|
31 |
+
|
32 |
+
AiTube 2 is not ready yet: this is an experimental side project and the [platform](https://aitube.at), code and documentation will be in development for most of 2025.
|
33 |
+
|
34 |
+
# Why can't I use it?
|
35 |
+
|
36 |
+
As this is a personal project I only have limited ressources to develop it on the side, but there are also technological bottlenecks.
|
37 |
+
|
38 |
+
Right now it is not economically viable to operate a platform like AiTube, it requires hardware that is too expensive and/or not powerful enough to give an enjoyable and reactive streaming experience.
|
39 |
+
|
40 |
+
I am evaluating various options to make it available sooner for people with the financial ressources to try it, such as creating a system to deploy render nodes to Hugging Face, GPU-on-demand blockchains.. etc.
|
41 |
+
|
42 |
+
# When can I use it?
|
43 |
+
|
44 |
+
I estimate it will take up to 1 to 2 years for more powerful and/or cheaper hardware to become available.
|
45 |
+
|
46 |
+
I already have a open-source prototype of AiTube which I use for R&D, based on free (as in "can run on your own machine") AI video models that can run fast with low quality settings (such as LTX Video).
|
47 |
+
|
48 |
+
It's not representative of the final experience, but that's a start and I use that as a basis to imagine the experiences of the future (collaborative generation, broadcasted live streams, interactive gaming, and artistic experiences that are hybrid between video and gaming).
|
analysis_options.yaml
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file configures the analyzer, which statically analyzes Dart code to
|
2 |
+
# check for errors, warnings, and lints.
|
3 |
+
#
|
4 |
+
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
5 |
+
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
6 |
+
# invoked from the command line by running `flutter analyze`.
|
7 |
+
|
8 |
+
# The following line activates a set of recommended lints for Flutter apps,
|
9 |
+
# packages, and plugins designed to encourage good coding practices.
|
10 |
+
include: package:flutter_lints/flutter.yaml
|
11 |
+
|
12 |
+
linter:
|
13 |
+
# The lint rules applied to this project can be customized in the
|
14 |
+
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
15 |
+
# included above or to enable additional rules. A list of all available lints
|
16 |
+
# and their documentation is published at https://dart.dev/lints.
|
17 |
+
#
|
18 |
+
# Instead of disabling a lint rule for the entire project in the
|
19 |
+
# section below, it can also be suppressed for a single line of code
|
20 |
+
# or a specific dart file by using the `// ignore: name_of_lint` and
|
21 |
+
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
22 |
+
# producing the lint.
|
23 |
+
rules:
|
24 |
+
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
25 |
+
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
26 |
+
|
27 |
+
# Additional information about this file can be found at
|
28 |
+
# https://dart.dev/guides/language/analysis-options
|
android/.gitignore
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradle-wrapper.jar
|
2 |
+
/.gradle
|
3 |
+
/captures/
|
4 |
+
/gradlew
|
5 |
+
/gradlew.bat
|
6 |
+
/local.properties
|
7 |
+
GeneratedPluginRegistrant.java
|
8 |
+
|
9 |
+
# Remember to never publicly share your keystore.
|
10 |
+
# See https://flutter.dev/to/reference-keystore
|
11 |
+
key.properties
|
12 |
+
**/*.keystore
|
13 |
+
**/*.jks
|
android/app/build.gradle
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
plugins {
|
2 |
+
id "com.android.application"
|
3 |
+
id "kotlin-android"
|
4 |
+
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
5 |
+
id "dev.flutter.flutter-gradle-plugin"
|
6 |
+
}
|
7 |
+
|
8 |
+
android {
|
9 |
+
namespace = "com.example.aitube2"
|
10 |
+
compileSdk = flutter.compileSdkVersion
|
11 |
+
ndkVersion = flutter.ndkVersion
|
12 |
+
|
13 |
+
compileOptions {
|
14 |
+
sourceCompatibility = JavaVersion.VERSION_1_8
|
15 |
+
targetCompatibility = JavaVersion.VERSION_1_8
|
16 |
+
}
|
17 |
+
|
18 |
+
kotlinOptions {
|
19 |
+
jvmTarget = JavaVersion.VERSION_1_8
|
20 |
+
}
|
21 |
+
|
22 |
+
defaultConfig {
|
23 |
+
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
24 |
+
applicationId = "com.example.aitube2"
|
25 |
+
// You can update the following values to match your application needs.
|
26 |
+
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
27 |
+
minSdk = flutter.minSdkVersion
|
28 |
+
targetSdk = flutter.targetSdkVersion
|
29 |
+
versionCode = flutter.versionCode
|
30 |
+
versionName = flutter.versionName
|
31 |
+
}
|
32 |
+
|
33 |
+
buildTypes {
|
34 |
+
release {
|
35 |
+
// TODO: Add your own signing config for the release build.
|
36 |
+
// Signing with the debug keys for now, so `flutter run --release` works.
|
37 |
+
signingConfig = signingConfigs.debug
|
38 |
+
}
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
flutter {
|
43 |
+
source = "../.."
|
44 |
+
}
|
android/app/src/debug/AndroidManifest.xml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
2 |
+
<!-- The INTERNET permission is required for development. Specifically,
|
3 |
+
the Flutter tool needs it to communicate with the running application
|
4 |
+
to allow setting breakpoints, to provide hot reload, etc.
|
5 |
+
-->
|
6 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
7 |
+
</manifest>
|
android/app/src/main/AndroidManifest.xml
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
2 |
+
<application
|
3 |
+
android:label="aitube2"
|
4 |
+
android:name="${applicationName}"
|
5 |
+
android:icon="@mipmap/ic_launcher">
|
6 |
+
<activity
|
7 |
+
android:name=".MainActivity"
|
8 |
+
android:exported="true"
|
9 |
+
android:launchMode="singleTop"
|
10 |
+
android:taskAffinity=""
|
11 |
+
android:theme="@style/LaunchTheme"
|
12 |
+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
13 |
+
android:hardwareAccelerated="true"
|
14 |
+
android:windowSoftInputMode="adjustResize">
|
15 |
+
<!-- Specifies an Android theme to apply to this Activity as soon as
|
16 |
+
the Android process has started. This theme is visible to the user
|
17 |
+
while the Flutter UI initializes. After that, this theme continues
|
18 |
+
to determine the Window background behind the Flutter UI. -->
|
19 |
+
<meta-data
|
20 |
+
android:name="io.flutter.embedding.android.NormalTheme"
|
21 |
+
android:resource="@style/NormalTheme"
|
22 |
+
/>
|
23 |
+
<intent-filter>
|
24 |
+
<action android:name="android.intent.action.MAIN"/>
|
25 |
+
<category android:name="android.intent.category.LAUNCHER"/>
|
26 |
+
</intent-filter>
|
27 |
+
</activity>
|
28 |
+
<!-- Don't delete the meta-data below.
|
29 |
+
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
30 |
+
<meta-data
|
31 |
+
android:name="flutterEmbedding"
|
32 |
+
android:value="2" />
|
33 |
+
</application>
|
34 |
+
<!-- Required to query activities that can process text, see:
|
35 |
+
https://developer.android.com/training/package-visibility and
|
36 |
+
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
37 |
+
|
38 |
+
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
39 |
+
<queries>
|
40 |
+
<intent>
|
41 |
+
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
42 |
+
<data android:mimeType="text/plain"/>
|
43 |
+
</intent>
|
44 |
+
</queries>
|
45 |
+
</manifest>
|
android/app/src/main/kotlin/com/example/aitube2/MainActivity.kt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package com.example.aitube2
|
2 |
+
|
3 |
+
import io.flutter.embedding.android.FlutterActivity
|
4 |
+
|
5 |
+
class MainActivity: FlutterActivity()
|
android/app/src/main/res/drawable-v21/launch_background.xml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
2 |
+
<!-- Modify this file to customize your launch splash screen -->
|
3 |
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
4 |
+
<item android:drawable="?android:colorBackground" />
|
5 |
+
|
6 |
+
<!-- You can insert your own image assets here -->
|
7 |
+
<!-- <item>
|
8 |
+
<bitmap
|
9 |
+
android:gravity="center"
|
10 |
+
android:src="@mipmap/launch_image" />
|
11 |
+
</item> -->
|
12 |
+
</layer-list>
|
android/app/src/main/res/drawable/launch_background.xml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
2 |
+
<!-- Modify this file to customize your launch splash screen -->
|
3 |
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
4 |
+
<item android:drawable="@android:color/white" />
|
5 |
+
|
6 |
+
<!-- You can insert your own image assets here -->
|
7 |
+
<!-- <item>
|
8 |
+
<bitmap
|
9 |
+
android:gravity="center"
|
10 |
+
android:src="@mipmap/launch_image" />
|
11 |
+
</item> -->
|
12 |
+
</layer-list>
|
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
ADDED
![]() |
Git LFS Details
|
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
ADDED
![]() |
Git LFS Details
|
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
ADDED
![]() |
Git LFS Details
|
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
ADDED
![]() |
Git LFS Details
|
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
ADDED
![]() |
Git LFS Details
|
android/app/src/main/res/values-night/styles.xml
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
2 |
+
<resources>
|
3 |
+
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
4 |
+
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
5 |
+
<!-- Show a splash screen on the activity. Automatically removed when
|
6 |
+
the Flutter engine draws its first frame -->
|
7 |
+
<item name="android:windowBackground">@drawable/launch_background</item>
|
8 |
+
</style>
|
9 |
+
<!-- Theme applied to the Android Window as soon as the process has started.
|
10 |
+
This theme determines the color of the Android Window while your
|
11 |
+
Flutter UI initializes, as well as behind your Flutter UI while its
|
12 |
+
running.
|
13 |
+
|
14 |
+
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
15 |
+
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
16 |
+
<item name="android:windowBackground">?android:colorBackground</item>
|
17 |
+
</style>
|
18 |
+
</resources>
|
android/app/src/main/res/values/styles.xml
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
2 |
+
<resources>
|
3 |
+
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
4 |
+
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
5 |
+
<!-- Show a splash screen on the activity. Automatically removed when
|
6 |
+
the Flutter engine draws its first frame -->
|
7 |
+
<item name="android:windowBackground">@drawable/launch_background</item>
|
8 |
+
</style>
|
9 |
+
<!-- Theme applied to the Android Window as soon as the process has started.
|
10 |
+
This theme determines the color of the Android Window while your
|
11 |
+
Flutter UI initializes, as well as behind your Flutter UI while its
|
12 |
+
running.
|
13 |
+
|
14 |
+
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
15 |
+
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
16 |
+
<item name="android:windowBackground">?android:colorBackground</item>
|
17 |
+
</style>
|
18 |
+
</resources>
|
android/app/src/profile/AndroidManifest.xml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
2 |
+
<!-- The INTERNET permission is required for development. Specifically,
|
3 |
+
the Flutter tool needs it to communicate with the running application
|
4 |
+
to allow setting breakpoints, to provide hot reload, etc.
|
5 |
+
-->
|
6 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
7 |
+
</manifest>
|
android/build.gradle
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
allprojects {
|
2 |
+
repositories {
|
3 |
+
google()
|
4 |
+
mavenCentral()
|
5 |
+
}
|
6 |
+
}
|
7 |
+
|
8 |
+
rootProject.buildDir = "../build"
|
9 |
+
subprojects {
|
10 |
+
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
11 |
+
}
|
12 |
+
subprojects {
|
13 |
+
project.evaluationDependsOn(":app")
|
14 |
+
}
|
15 |
+
|
16 |
+
tasks.register("clean", Delete) {
|
17 |
+
delete rootProject.buildDir
|
18 |
+
}
|
android/gradle.properties
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
2 |
+
android.useAndroidX=true
|
3 |
+
android.enableJetifier=true
|
android/gradle/wrapper/gradle-wrapper.properties
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
distributionBase=GRADLE_USER_HOME
|
2 |
+
distributionPath=wrapper/dists
|
3 |
+
zipStoreBase=GRADLE_USER_HOME
|
4 |
+
zipStorePath=wrapper/dists
|
5 |
+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
android/settings.gradle
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pluginManagement {
|
2 |
+
def flutterSdkPath = {
|
3 |
+
def properties = new Properties()
|
4 |
+
file("local.properties").withInputStream { properties.load(it) }
|
5 |
+
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
6 |
+
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
7 |
+
return flutterSdkPath
|
8 |
+
}()
|
9 |
+
|
10 |
+
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
11 |
+
|
12 |
+
repositories {
|
13 |
+
google()
|
14 |
+
mavenCentral()
|
15 |
+
gradlePluginPortal()
|
16 |
+
}
|
17 |
+
}
|
18 |
+
|
19 |
+
plugins {
|
20 |
+
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
21 |
+
id "com.android.application" version "8.1.0" apply false
|
22 |
+
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
23 |
+
}
|
24 |
+
|
25 |
+
include ":app"
|
api.py
ADDED
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
import os
|
5 |
+
import pathlib
|
6 |
+
from aiohttp import web, WSMsgType
|
7 |
+
from typing import Dict, Any
|
8 |
+
from api_core import VideoGenerationAPI
|
9 |
+
|
10 |
+
from api_config import *
|
11 |
+
|
12 |
+
# Configure logging
|
13 |
+
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
16 |
+
)
|
17 |
+
logger = logging.getLogger(__name__)
|
18 |
+
|
19 |
+
async def process_generic_request(data: dict, ws: web.WebSocketResponse, api) -> None:
|
20 |
+
"""Handle general requests that don't fit into specialized queues"""
|
21 |
+
try:
|
22 |
+
request_id = data.get('requestId')
|
23 |
+
action = data.get('action')
|
24 |
+
|
25 |
+
def error_response(message: str):
|
26 |
+
return {
|
27 |
+
'action': action,
|
28 |
+
'requestId': request_id,
|
29 |
+
'success': False,
|
30 |
+
'error': message
|
31 |
+
}
|
32 |
+
|
33 |
+
if action == 'heartbeat':
|
34 |
+
# Include user role info in heartbeat response
|
35 |
+
user_role = getattr(ws, 'user_role', 'anon')
|
36 |
+
await ws.send_json({
|
37 |
+
'action': 'heartbeat',
|
38 |
+
'requestId': request_id,
|
39 |
+
'success': True,
|
40 |
+
'user_role': user_role
|
41 |
+
})
|
42 |
+
|
43 |
+
elif action == 'get_user_role':
|
44 |
+
# Return the user role information
|
45 |
+
user_role = getattr(ws, 'user_role', 'anon')
|
46 |
+
await ws.send_json({
|
47 |
+
'action': 'get_user_role',
|
48 |
+
'requestId': request_id,
|
49 |
+
'success': True,
|
50 |
+
'user_role': user_role
|
51 |
+
})
|
52 |
+
|
53 |
+
elif action == 'generate_caption':
|
54 |
+
title = data.get('params', {}).get('title')
|
55 |
+
description = data.get('params', {}).get('description')
|
56 |
+
|
57 |
+
if not title or not description:
|
58 |
+
await ws.send_json(error_response('Missing title or description'))
|
59 |
+
return
|
60 |
+
|
61 |
+
caption = await api.generate_caption(title, description)
|
62 |
+
await ws.send_json({
|
63 |
+
'action': action,
|
64 |
+
'requestId': request_id,
|
65 |
+
'success': True,
|
66 |
+
'caption': caption
|
67 |
+
})
|
68 |
+
|
69 |
+
elif action == 'generate_thumbnail':
|
70 |
+
title = data.get('params', {}).get('title')
|
71 |
+
description = data.get('params', {}).get('description')
|
72 |
+
|
73 |
+
if not title or not description:
|
74 |
+
await ws.send_json(error_response('Missing title or description'))
|
75 |
+
return
|
76 |
+
|
77 |
+
thumbnail = await api.generate_thumbnail(title, description)
|
78 |
+
await ws.send_json({
|
79 |
+
'action': action,
|
80 |
+
'requestId': request_id,
|
81 |
+
'success': True,
|
82 |
+
'thumbnailUrl': thumbnail
|
83 |
+
})
|
84 |
+
|
85 |
+
else:
|
86 |
+
await ws.send_json(error_response(f'Unknown action: {action}'))
|
87 |
+
|
88 |
+
except Exception as e:
|
89 |
+
logger.error(f"Error processing generic request: {str(e)}")
|
90 |
+
try:
|
91 |
+
await ws.send_json({
|
92 |
+
'action': data.get('action'),
|
93 |
+
'requestId': data.get('requestId'),
|
94 |
+
'success': False,
|
95 |
+
'error': f'Internal server error: {str(e)}'
|
96 |
+
})
|
97 |
+
except Exception as send_error:
|
98 |
+
logger.error(f"Error sending error response: {send_error}")
|
99 |
+
|
100 |
+
async def process_search_queue(queue: asyncio.Queue, ws: web.WebSocketResponse, api):
|
101 |
+
"""Medium priority queue for search operations"""
|
102 |
+
while True:
|
103 |
+
try:
|
104 |
+
data = await queue.get()
|
105 |
+
request_id = data.get('requestId')
|
106 |
+
query = data.get('query', '').strip()
|
107 |
+
search_count = data.get('searchCount', 0)
|
108 |
+
attempt_count = data.get('attemptCount', 0)
|
109 |
+
|
110 |
+
logger.info(f"Processing search request: query='{query}', search_count={search_count}, attempt={attempt_count}")
|
111 |
+
|
112 |
+
if not query:
|
113 |
+
logger.warning(f"Empty query received in request: {data}")
|
114 |
+
result = {
|
115 |
+
'action': 'search',
|
116 |
+
'requestId': request_id,
|
117 |
+
'success': False,
|
118 |
+
'error': 'No search query provided'
|
119 |
+
}
|
120 |
+
else:
|
121 |
+
try:
|
122 |
+
search_result = await api.search_video(
|
123 |
+
query,
|
124 |
+
search_count=search_count,
|
125 |
+
attempt_count=attempt_count
|
126 |
+
)
|
127 |
+
|
128 |
+
if search_result:
|
129 |
+
logger.info(f"Search successful for query '{query}' (#{search_count})")
|
130 |
+
result = {
|
131 |
+
'action': 'search',
|
132 |
+
'requestId': request_id,
|
133 |
+
'success': True,
|
134 |
+
'result': search_result
|
135 |
+
}
|
136 |
+
else:
|
137 |
+
logger.warning(f"No results found for query '{query}' (#{search_count})")
|
138 |
+
result = {
|
139 |
+
'action': 'search',
|
140 |
+
'requestId': request_id,
|
141 |
+
'success': False,
|
142 |
+
'error': 'No results found'
|
143 |
+
}
|
144 |
+
except Exception as e:
|
145 |
+
logger.error(f"Search error for query '{query}' (#{search_count}, attempt {attempt_count}): {str(e)}")
|
146 |
+
result = {
|
147 |
+
'action': 'search',
|
148 |
+
'requestId': request_id,
|
149 |
+
'success': False,
|
150 |
+
'error': f'Search error: {str(e)}'
|
151 |
+
}
|
152 |
+
|
153 |
+
await ws.send_json(result)
|
154 |
+
|
155 |
+
except Exception as e:
|
156 |
+
logger.error(f"Error in search queue processor: {str(e)}")
|
157 |
+
try:
|
158 |
+
error_response = {
|
159 |
+
'action': 'search',
|
160 |
+
'requestId': data.get('requestId') if 'data' in locals() else None,
|
161 |
+
'success': False,
|
162 |
+
'error': f'Internal server error: {str(e)}'
|
163 |
+
}
|
164 |
+
await ws.send_json(error_response)
|
165 |
+
except Exception as send_error:
|
166 |
+
logger.error(f"Error sending error response: {send_error}")
|
167 |
+
finally:
|
168 |
+
if 'queue' in locals():
|
169 |
+
queue.task_done()
|
170 |
+
|
171 |
+
async def process_chat_queue(queue: asyncio.Queue, ws: web.WebSocketResponse):
|
172 |
+
"""High priority queue for chat operations"""
|
173 |
+
while True:
|
174 |
+
data = await queue.get()
|
175 |
+
try:
|
176 |
+
api = ws.app['api']
|
177 |
+
if data['action'] == 'join_chat':
|
178 |
+
result = await api.handle_join_chat(data, ws)
|
179 |
+
elif data['action'] == 'chat_message':
|
180 |
+
result = await api.handle_chat_message(data, ws)
|
181 |
+
elif data['action'] == 'leave_chat':
|
182 |
+
result = await api.handle_leave_chat(data, ws)
|
183 |
+
await ws.send_json(result)
|
184 |
+
except Exception as e:
|
185 |
+
logger.error(f"Error processing chat request: {e}")
|
186 |
+
try:
|
187 |
+
await ws.send_json({
|
188 |
+
'action': data['action'],
|
189 |
+
'requestId': data.get('requestId'),
|
190 |
+
'success': False,
|
191 |
+
'error': f'Chat error: {str(e)}'
|
192 |
+
})
|
193 |
+
except Exception as send_error:
|
194 |
+
logger.error(f"Error sending error response: {send_error}")
|
195 |
+
finally:
|
196 |
+
queue.task_done()
|
197 |
+
|
198 |
+
async def process_video_queue(queue: asyncio.Queue, ws: web.WebSocketResponse):
|
199 |
+
"""Process multiple video generation requests in parallel"""
|
200 |
+
active_tasks = set()
|
201 |
+
MAX_CONCURRENT = len(VIDEO_ROUND_ROBIN_ENDPOINT_URLS) # Match client's max concurrent generations
|
202 |
+
|
203 |
+
async def process_single_request(data):
|
204 |
+
try:
|
205 |
+
api = ws.app['api']
|
206 |
+
title = data.get('title', '')
|
207 |
+
description = data.get('description', '')
|
208 |
+
video_prompt_prefix = data.get('video_prompt_prefix', '')
|
209 |
+
options = data.get('options', {})
|
210 |
+
|
211 |
+
# Get the user role from the websocket
|
212 |
+
user_role = getattr(ws, 'user_role', 'anon')
|
213 |
+
|
214 |
+
# Pass the user role to generate_video
|
215 |
+
video_data = await api.generate_video(title, description, video_prompt_prefix, options, user_role)
|
216 |
+
|
217 |
+
result = {
|
218 |
+
'action': 'generate_video',
|
219 |
+
'requestId': data.get('requestId'),
|
220 |
+
'success': True,
|
221 |
+
'video': video_data,
|
222 |
+
}
|
223 |
+
|
224 |
+
await ws.send_json(result)
|
225 |
+
|
226 |
+
except Exception as e:
|
227 |
+
logger.error(f"Error processing video request: {e}")
|
228 |
+
try:
|
229 |
+
await ws.send_json({
|
230 |
+
'action': 'generate_video',
|
231 |
+
'requestId': data.get('requestId'),
|
232 |
+
'success': False,
|
233 |
+
'error': f'Video generation error: {str(e)}'
|
234 |
+
})
|
235 |
+
except Exception as send_error:
|
236 |
+
logger.error(f"Error sending error response: {send_error}")
|
237 |
+
finally:
|
238 |
+
active_tasks.discard(asyncio.current_task())
|
239 |
+
|
240 |
+
while True:
|
241 |
+
# Clean up completed tasks
|
242 |
+
active_tasks = {task for task in active_tasks if not task.done()}
|
243 |
+
|
244 |
+
# Start new tasks if we have capacity
|
245 |
+
while len(active_tasks) < MAX_CONCURRENT:
|
246 |
+
try:
|
247 |
+
# Use try_get to avoid blocking if queue is empty
|
248 |
+
data = await asyncio.wait_for(queue.get(), timeout=0.1)
|
249 |
+
|
250 |
+
# Create and start new task
|
251 |
+
task = asyncio.create_task(process_single_request(data))
|
252 |
+
active_tasks.add(task)
|
253 |
+
|
254 |
+
except asyncio.TimeoutError:
|
255 |
+
# No items in queue, break inner loop
|
256 |
+
break
|
257 |
+
except Exception as e:
|
258 |
+
logger.error(f"Error creating video generation task: {e}")
|
259 |
+
break
|
260 |
+
|
261 |
+
# Wait a short time before checking queue again
|
262 |
+
await asyncio.sleep(0.1)
|
263 |
+
|
264 |
+
# Handle any completed tasks' errors
|
265 |
+
for task in list(active_tasks):
|
266 |
+
if task.done():
|
267 |
+
try:
|
268 |
+
await task
|
269 |
+
except Exception as e:
|
270 |
+
logger.error(f"Task failed with error: {e}")
|
271 |
+
active_tasks.discard(task)
|
272 |
+
|
273 |
+
async def status_handler(request: web.Request) -> web.Response:
|
274 |
+
"""Handler for API status endpoint"""
|
275 |
+
api = request.app['api']
|
276 |
+
return web.json_response({
|
277 |
+
'product': PRODUCT_NAME,
|
278 |
+
'version': '0.1.0',
|
279 |
+
'maintenance_mode': MAINTENANCE_MODE,
|
280 |
+
'available_endpoints': len(VIDEO_ROUND_ROBIN_ENDPOINT_URLS)
|
281 |
+
})
|
282 |
+
|
283 |
+
async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
|
284 |
+
# Check if maintenance mode is enabled
|
285 |
+
if MAINTENANCE_MODE:
|
286 |
+
# Return an error response indicating maintenance mode
|
287 |
+
return web.json_response({
|
288 |
+
'error': 'Server is in maintenance mode',
|
289 |
+
'maintenance': True
|
290 |
+
}, status=503) # 503 Service Unavailable
|
291 |
+
|
292 |
+
ws = web.WebSocketResponse(
|
293 |
+
max_msg_size=1024*1024*10, # 10MB max message size
|
294 |
+
timeout=30.0 # we want to keep things tight and short
|
295 |
+
)
|
296 |
+
|
297 |
+
ws.app = request.app
|
298 |
+
await ws.prepare(request)
|
299 |
+
api = request.app['api']
|
300 |
+
|
301 |
+
# Get the Hugging Face token from query parameters
|
302 |
+
hf_token = request.query.get('hf_token', '')
|
303 |
+
|
304 |
+
# Validate the token and determine the user role
|
305 |
+
user_role = await api.validate_user_token(hf_token)
|
306 |
+
logger.info(f"User connected with role: {user_role}")
|
307 |
+
|
308 |
+
# Store the user role in the websocket
|
309 |
+
ws.user_role = user_role
|
310 |
+
|
311 |
+
# Create separate queues for different request types
|
312 |
+
chat_queue = asyncio.Queue()
|
313 |
+
video_queue = asyncio.Queue()
|
314 |
+
search_queue = asyncio.Queue()
|
315 |
+
|
316 |
+
# Start background tasks for handling different request types
|
317 |
+
background_tasks = [
|
318 |
+
asyncio.create_task(process_chat_queue(chat_queue, ws)),
|
319 |
+
asyncio.create_task(process_video_queue(video_queue, ws)),
|
320 |
+
asyncio.create_task(process_search_queue(search_queue, ws, api))
|
321 |
+
]
|
322 |
+
|
323 |
+
try:
|
324 |
+
async for msg in ws:
|
325 |
+
if msg.type == WSMsgType.TEXT:
|
326 |
+
try:
|
327 |
+
data = json.loads(msg.data)
|
328 |
+
action = data.get('action')
|
329 |
+
|
330 |
+
# Route requests to appropriate queues
|
331 |
+
if action in ['join_chat', 'leave_chat', 'chat_message']:
|
332 |
+
await chat_queue.put(data)
|
333 |
+
elif action in ['generate_video']:
|
334 |
+
await video_queue.put(data)
|
335 |
+
elif action == 'search':
|
336 |
+
await search_queue.put(data)
|
337 |
+
else:
|
338 |
+
await process_generic_request(data, ws, api)
|
339 |
+
|
340 |
+
except Exception as e:
|
341 |
+
logger.error(f"Error processing WebSocket message: {str(e)}")
|
342 |
+
await ws.send_json({
|
343 |
+
'action': data.get('action') if 'data' in locals() else 'unknown',
|
344 |
+
'success': False,
|
345 |
+
'error': f'Error processing message: {str(e)}'
|
346 |
+
})
|
347 |
+
|
348 |
+
elif msg.type in (WSMsgType.ERROR, WSMsgType.CLOSE):
|
349 |
+
break
|
350 |
+
|
351 |
+
finally:
|
352 |
+
# Cleanup
|
353 |
+
for task in background_tasks:
|
354 |
+
task.cancel()
|
355 |
+
try:
|
356 |
+
await asyncio.gather(*background_tasks, return_exceptions=True)
|
357 |
+
except asyncio.CancelledError:
|
358 |
+
pass
|
359 |
+
|
360 |
+
return ws
|
361 |
+
|
362 |
+
async def init_app() -> web.Application:
|
363 |
+
app = web.Application(
|
364 |
+
client_max_size=1024**2*10 # 10MB max size
|
365 |
+
)
|
366 |
+
|
367 |
+
# Create API instance
|
368 |
+
api = VideoGenerationAPI()
|
369 |
+
app['api'] = api
|
370 |
+
|
371 |
+
# Add cleanup logic
|
372 |
+
async def cleanup_api(app):
|
373 |
+
# Add any necessary cleanup for the API
|
374 |
+
pass
|
375 |
+
|
376 |
+
app.on_shutdown.append(cleanup_api)
|
377 |
+
|
378 |
+
# Add routes
|
379 |
+
app.router.add_get('/ws', websocket_handler)
|
380 |
+
app.router.add_get('/api/status', status_handler)
|
381 |
+
|
382 |
+
# Set up static file serving
|
383 |
+
# Define the path to the public directory
|
384 |
+
public_path = pathlib.Path(__file__).parent / 'build' / 'web'
|
385 |
+
if not public_path.exists():
|
386 |
+
public_path.mkdir(parents=True, exist_ok=True)
|
387 |
+
|
388 |
+
# Set up static file serving with proper security considerations
|
389 |
+
async def static_file_handler(request):
|
390 |
+
# Get the path from the request (removing leading /)
|
391 |
+
path_parts = request.path.lstrip('/').split('/')
|
392 |
+
|
393 |
+
# Convert to safe path to prevent path traversal attacks
|
394 |
+
safe_path = public_path.joinpath(*path_parts)
|
395 |
+
|
396 |
+
# Make sure the path is within the public directory (prevent directory traversal)
|
397 |
+
try:
|
398 |
+
safe_path = safe_path.resolve()
|
399 |
+
if not str(safe_path).startswith(str(public_path.resolve())):
|
400 |
+
return web.HTTPForbidden(text="Access denied")
|
401 |
+
except (ValueError, FileNotFoundError):
|
402 |
+
return web.HTTPNotFound()
|
403 |
+
|
404 |
+
# If path is a directory, look for index.html
|
405 |
+
if safe_path.is_dir():
|
406 |
+
safe_path = safe_path / 'index.html'
|
407 |
+
|
408 |
+
# Check if the file exists
|
409 |
+
if not safe_path.exists() or not safe_path.is_file():
|
410 |
+
# If not found, serve index.html (for SPA routing)
|
411 |
+
safe_path = public_path / 'index.html'
|
412 |
+
if not safe_path.exists():
|
413 |
+
return web.HTTPNotFound()
|
414 |
+
|
415 |
+
# Determine content type based on file extension
|
416 |
+
content_type = 'text/plain'
|
417 |
+
ext = safe_path.suffix.lower()
|
418 |
+
if ext == '.html':
|
419 |
+
content_type = 'text/html'
|
420 |
+
elif ext == '.js':
|
421 |
+
content_type = 'application/javascript'
|
422 |
+
elif ext == '.css':
|
423 |
+
content_type = 'text/css'
|
424 |
+
elif ext in ('.jpg', '.jpeg'):
|
425 |
+
content_type = 'image/jpeg'
|
426 |
+
elif ext == '.png':
|
427 |
+
content_type = 'image/png'
|
428 |
+
elif ext == '.gif':
|
429 |
+
content_type = 'image/gif'
|
430 |
+
elif ext == '.svg':
|
431 |
+
content_type = 'image/svg+xml'
|
432 |
+
elif ext == '.json':
|
433 |
+
content_type = 'application/json'
|
434 |
+
|
435 |
+
# Return the file with appropriate headers
|
436 |
+
return web.FileResponse(safe_path, headers={'Content-Type': content_type})
|
437 |
+
|
438 |
+
# Add catch-all route for static files (lower priority than API routes)
|
439 |
+
app.router.add_get('/{path:.*}', static_file_handler)
|
440 |
+
|
441 |
+
return app
|
442 |
+
|
443 |
+
if __name__ == '__main__':
|
444 |
+
app = asyncio.run(init_app())
|
445 |
+
web.run_app(app, host='0.0.0.0', port=8080)
|
api_config.py
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
PRODUCT_NAME = os.environ.get('PRODUCT_NAME', 'AiTube')
|
4 |
+
|
5 |
+
TEXT_MODEL = os.environ.get('HF_TEXT_MODEL',
|
6 |
+
#'HuggingFaceH4/zephyr-7b-beta'
|
7 |
+
'HuggingFaceTB/SmolLM2-1.7B-Instruct'
|
8 |
+
)
|
9 |
+
|
10 |
+
IMAGE_MODEL = os.environ.get('HF_IMAGE_MODEL', '')
|
11 |
+
|
12 |
+
# Environment variable to control maintenance mode
|
13 |
+
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', 'false').lower() in ('true', 'yes', '1', 't')
|
14 |
+
|
15 |
+
ADMIN_ACCOUNTS = [
|
16 |
+
"jbilcke-hf"
|
17 |
+
]
|
18 |
+
|
19 |
+
RAW_VIDEO_ROUND_ROBIN_ENDPOINT_URLS = [
|
20 |
+
os.environ.get('VIDEO_ROUND_ROBIN_SERVER_1', ''),
|
21 |
+
os.environ.get('VIDEO_ROUND_ROBIN_SERVER_2', ''),
|
22 |
+
os.environ.get('VIDEO_ROUND_ROBIN_SERVER_3', ''),
|
23 |
+
os.environ.get('VIDEO_ROUND_ROBIN_SERVER_4', ''),
|
24 |
+
]
|
25 |
+
|
26 |
+
# Filter out empty strings from the endpoint list
|
27 |
+
VIDEO_ROUND_ROBIN_ENDPOINT_URLS = [url for url in RAW_VIDEO_ROUND_ROBIN_ENDPOINT_URLS if url]
|
28 |
+
|
29 |
+
HF_TOKEN = os.environ.get('HF_TOKEN')
|
30 |
+
|
31 |
+
# use the same secret token as you used to secure your BASE_SPACE_NAME spaces
|
32 |
+
SECRET_TOKEN = os.environ.get('SECRET_TOKEN')
|
33 |
+
|
34 |
+
# altenative words we could use: "saturated, highlight, overexposed, highlighted, overlit, shaking, too bright, worst quality, inconsistent motion, blurry, jittery, distorted, cropped, watermarked, watermark, logo, subtitle, subtitles, lowres"
|
35 |
+
NEGATIVE_PROMPT = "low quality, worst quality, deformed, distorted, disfigured, blurry, text, watermark"
|
36 |
+
|
37 |
+
POSITIVE_PROMPT_SUFFIX = "high quality, cinematic, 4K, intricate details"
|
38 |
+
|
39 |
+
GUIDANCE_SCALE = 1.0
|
40 |
+
|
41 |
+
# anonymous users are people browing AiTube2 without being connected
|
42 |
+
# this category suffers from regular abuse so we need to enforce strict limitations
|
43 |
+
CONFIG_FOR_ANONYMOUS_USERS = {
|
44 |
+
|
45 |
+
# anons can only watch 2 minutes per video
|
46 |
+
"max_rendering_time_per_client_per_video_in_sec": 2 * 60,
|
47 |
+
|
48 |
+
"max_buffer_size": 2,
|
49 |
+
"max_concurrent_generations": 2,
|
50 |
+
|
51 |
+
"min_num_inference_steps": 2,
|
52 |
+
"default_num_inference_steps": 2,
|
53 |
+
"max_num_inference_steps": 4,
|
54 |
+
|
55 |
+
"min_num_frames": 9, # 8 + 1
|
56 |
+
"default_max_num_frames": 65, # 8*8 + 1
|
57 |
+
"max_num_frames": 65, # 8*8 + 1
|
58 |
+
|
59 |
+
"min_clip_duration_seconds": 1,
|
60 |
+
"default_clip_duration_seconds": 2,
|
61 |
+
"max_clip_duration_seconds": 2,
|
62 |
+
|
63 |
+
"min_clip_playback_speed": 0.5,
|
64 |
+
"default_clip_playback_speed": 0.5,
|
65 |
+
"max_clip_playback_speed": 0.5,
|
66 |
+
|
67 |
+
"min_clip_framerate": 8,
|
68 |
+
"default_clip_framerate": 16,
|
69 |
+
"max_clip_framerate": 16,
|
70 |
+
|
71 |
+
"min_clip_width": 544,
|
72 |
+
"default_clip_width": 544,
|
73 |
+
"max_clip_width": 544,
|
74 |
+
|
75 |
+
"min_clip_height": 320,
|
76 |
+
"default_clip_height": 320,
|
77 |
+
"max_clip_height": 320,
|
78 |
+
}
|
79 |
+
|
80 |
+
# Hugging Face users enjoy a more normal and calibrated experience
|
81 |
+
CONFIG_FOR_STANDARD_HF_USERS = {
|
82 |
+
"max_rendering_time_per_client_per_video_in_sec": 15 * 60,
|
83 |
+
|
84 |
+
"max_buffer_size": 2,
|
85 |
+
"max_concurrent_generations": 2,
|
86 |
+
|
87 |
+
"min_num_inference_steps": 2,
|
88 |
+
"default_num_inference_steps": 4,
|
89 |
+
"max_num_inference_steps": 6,
|
90 |
+
|
91 |
+
"min_num_frames": 9, # 8 + 1
|
92 |
+
"default_num_frames": 65, # 8*8 + 1
|
93 |
+
"max_num_frames": 65, # 8*8 + 1
|
94 |
+
|
95 |
+
"min_clip_duration_seconds": 1,
|
96 |
+
"default_clip_duration_seconds": 2,
|
97 |
+
"max_clip_duration_seconds": 2,
|
98 |
+
|
99 |
+
"min_clip_playback_speed": 0.5,
|
100 |
+
"default_clip_playback_speed": 0.7,
|
101 |
+
"max_clip_playback_speed": 0.7,
|
102 |
+
|
103 |
+
"min_clip_framerate": 8,
|
104 |
+
"default_clip_framerate": 25,
|
105 |
+
"max_clip_framerate": 25,
|
106 |
+
|
107 |
+
"min_clip_width": 544,
|
108 |
+
"default_clip_width": 640,
|
109 |
+
"max_clip_width": 640,
|
110 |
+
|
111 |
+
"min_clip_height": 320,
|
112 |
+
"default_clip_height": 416,
|
113 |
+
"max_clip_height": 416,
|
114 |
+
}
|
115 |
+
|
116 |
+
# Hugging Face users with a Pro may enjoy an improved experience
|
117 |
+
CONFIG_FOR_PRO_HF_USERS = {
|
118 |
+
"max_rendering_time_per_client_per_video_in_sec": 20 * 60,
|
119 |
+
|
120 |
+
"max_buffer_size": 2,
|
121 |
+
"max_concurrent_generations": 2,
|
122 |
+
|
123 |
+
"min_num_inference_steps": 2,
|
124 |
+
"max_num_inference_steps": 8,
|
125 |
+
|
126 |
+
"min_num_frames": 9, # 8 + 1
|
127 |
+
"default_num_frames": 97, # (8*12) + 1
|
128 |
+
"max_num_frames": 97, # (8*12) + 1
|
129 |
+
|
130 |
+
"min_clip_duration_seconds": 1,
|
131 |
+
"default_clip_duration_seconds": 2,
|
132 |
+
"max_clip_duration_seconds": 3,
|
133 |
+
|
134 |
+
"min_clip_playback_speed": 0.5,
|
135 |
+
"default_clip_playback_speed": 0.8,
|
136 |
+
"max_clip_playback_speed": 0.8,
|
137 |
+
|
138 |
+
"min_clip_framerate": 8,
|
139 |
+
"default_clip_framerate": 25,
|
140 |
+
"max_clip_framerate": 30,
|
141 |
+
|
142 |
+
"min_clip_width": 544,
|
143 |
+
"default_clip_width": 768,
|
144 |
+
"max_clip_width": 768,
|
145 |
+
|
146 |
+
"min_clip_height": 320,
|
147 |
+
"default_clip_height": 480,
|
148 |
+
"max_clip_height": 480,
|
149 |
+
}
|
150 |
+
|
151 |
+
CONFIG_FOR_ADMIN_HF_USERS = {
|
152 |
+
"max_rendering_time_per_client_per_video_in_sec": 60 * 60,
|
153 |
+
|
154 |
+
"max_buffer_size": 2,
|
155 |
+
"max_concurrent_generations": 2,
|
156 |
+
|
157 |
+
"min_num_inference_steps": 2,
|
158 |
+
"default_num_inference_steps": 4,
|
159 |
+
"max_num_inference_steps": 8,
|
160 |
+
|
161 |
+
"min_num_frames": 9, # 8 + 1
|
162 |
+
"default_num_frames": 65, # (8 * 8) + 1
|
163 |
+
"max_num_frames": 129, # (8 * 16) + 1
|
164 |
+
|
165 |
+
"min_clip_duration_seconds": 1,
|
166 |
+
"default_clip_duration_seconds": 2,
|
167 |
+
"max_clip_duration_seconds": 4,
|
168 |
+
|
169 |
+
"min_clip_playback_speed": 0.5,
|
170 |
+
"default_clip_playback_speed": 0.8,
|
171 |
+
"max_clip_playback_speed": 0.9,
|
172 |
+
|
173 |
+
"min_clip_framerate": 8,
|
174 |
+
"default_clip_framerate": 30,
|
175 |
+
"max_clip_framerate": 60,
|
176 |
+
|
177 |
+
"min_clip_width": 544,
|
178 |
+
"default_clip_width": 768,
|
179 |
+
"max_clip_width": 1216,
|
180 |
+
|
181 |
+
"min_clip_height": 320,
|
182 |
+
"default_clip_height": 480,
|
183 |
+
"max_clip_height": 704,
|
184 |
+
}
|
api_core.py
ADDED
@@ -0,0 +1,746 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
import io
|
4 |
+
import re
|
5 |
+
import base64
|
6 |
+
import uuid
|
7 |
+
from typing import Dict, Any, Optional, List, Literal
|
8 |
+
from dataclasses import dataclass
|
9 |
+
from asyncio import Lock, Queue
|
10 |
+
import asyncio
|
11 |
+
import time
|
12 |
+
import datetime
|
13 |
+
from contextlib import asynccontextmanager
|
14 |
+
from collections import defaultdict
|
15 |
+
from aiohttp import web, ClientSession
|
16 |
+
from huggingface_hub import InferenceClient, HfApi
|
17 |
+
from gradio_client import Client
|
18 |
+
import random
|
19 |
+
import yaml
|
20 |
+
import json
|
21 |
+
|
22 |
+
from api_config import *
|
23 |
+
|
24 |
+
# User role type
|
25 |
+
UserRole = Literal['anon', 'normal', 'pro', 'admin']
|
26 |
+
|
27 |
+
# Configure logging
|
28 |
+
logging.basicConfig(
|
29 |
+
level=logging.INFO,
|
30 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
31 |
+
)
|
32 |
+
logger = logging.getLogger(__name__)
|
33 |
+
|
34 |
+
|
35 |
+
def generate_seed():
|
36 |
+
"""Generate a random positive 32-bit integer seed."""
|
37 |
+
return random.randint(0, 2**32 - 1)
|
38 |
+
|
39 |
+
def sanitize_yaml_response(response_text: str) -> str:
|
40 |
+
"""
|
41 |
+
Sanitize and format AI response into valid YAML.
|
42 |
+
Returns properly formatted YAML string.
|
43 |
+
"""
|
44 |
+
|
45 |
+
response_text = response_text.split("```")[0]
|
46 |
+
|
47 |
+
# Remove any markdown code block indicators and YAML document markers
|
48 |
+
clean_text = re.sub(r'```yaml|```|---|\.\.\.$', '', response_text.strip())
|
49 |
+
|
50 |
+
# Split into lines and process each line
|
51 |
+
lines = clean_text.split('\n')
|
52 |
+
sanitized_lines = []
|
53 |
+
current_field = None
|
54 |
+
|
55 |
+
for line in lines:
|
56 |
+
stripped = line.strip()
|
57 |
+
if not stripped:
|
58 |
+
continue
|
59 |
+
|
60 |
+
# Handle field starts
|
61 |
+
if stripped.startswith('title:') or stripped.startswith('description:'):
|
62 |
+
# Ensure proper YAML format with space after colon and proper quoting
|
63 |
+
field_name = stripped.split(':', 1)[0]
|
64 |
+
field_value = stripped.split(':', 1)[1].strip().strip('"\'')
|
65 |
+
|
66 |
+
# Quote the value if it contains special characters
|
67 |
+
if any(c in field_value for c in ':[]{},&*#?|-<>=!%@`'):
|
68 |
+
field_value = f'"{field_value}"'
|
69 |
+
|
70 |
+
sanitized_lines.append(f"{field_name}: {field_value}")
|
71 |
+
current_field = field_name
|
72 |
+
|
73 |
+
elif stripped.startswith('tags:'):
|
74 |
+
sanitized_lines.append('tags:')
|
75 |
+
current_field = 'tags'
|
76 |
+
|
77 |
+
elif stripped.startswith('-') and current_field == 'tags':
|
78 |
+
# Process tag values
|
79 |
+
tag = stripped[1:].strip().strip('"\'')
|
80 |
+
if tag:
|
81 |
+
# Clean and format tag
|
82 |
+
tag = re.sub(r'[^\x00-\x7F]+', '', tag) # Remove non-ASCII
|
83 |
+
tag = re.sub(r'[^a-zA-Z0-9\s-]', '', tag) # Keep only alphanumeric and hyphen
|
84 |
+
tag = tag.strip().lower().replace(' ', '-')
|
85 |
+
if tag:
|
86 |
+
sanitized_lines.append(f" - {tag}")
|
87 |
+
|
88 |
+
elif current_field in ['title', 'description']:
|
89 |
+
# Handle multi-line title/description continuation
|
90 |
+
value = stripped.strip('"\'')
|
91 |
+
if value:
|
92 |
+
# Append to previous line
|
93 |
+
prev = sanitized_lines[-1]
|
94 |
+
sanitized_lines[-1] = f"{prev} {value}"
|
95 |
+
|
96 |
+
# Ensure the YAML has all required fields
|
97 |
+
required_fields = {'title', 'description', 'tags'}
|
98 |
+
found_fields = {line.split(':')[0].strip() for line in sanitized_lines if ':' in line}
|
99 |
+
|
100 |
+
for field in required_fields - found_fields:
|
101 |
+
if field == 'tags':
|
102 |
+
sanitized_lines.extend(['tags:', ' - default'])
|
103 |
+
else:
|
104 |
+
sanitized_lines.append(f'{field}: "No {field} provided"')
|
105 |
+
|
106 |
+
return '\n'.join(sanitized_lines)
|
107 |
+
|
108 |
+
@dataclass
|
109 |
+
class Endpoint:
|
110 |
+
id: int
|
111 |
+
url: str
|
112 |
+
busy: bool = False
|
113 |
+
last_used: float = 0
|
114 |
+
|
115 |
+
class EndpointManager:
|
116 |
+
def __init__(self):
|
117 |
+
self.endpoints: List[Endpoint] = []
|
118 |
+
self.lock = Lock()
|
119 |
+
self.endpoint_queue: Queue[Endpoint] = Queue()
|
120 |
+
self.initialize_endpoints()
|
121 |
+
|
122 |
+
def initialize_endpoints(self):
|
123 |
+
"""Initialize the list of endpoints"""
|
124 |
+
for i, url in enumerate(VIDEO_ROUND_ROBIN_ENDPOINT_URLS):
|
125 |
+
endpoint = Endpoint(id=i + 1, url=url)
|
126 |
+
self.endpoints.append(endpoint)
|
127 |
+
self.endpoint_queue.put_nowait(endpoint)
|
128 |
+
|
129 |
+
@asynccontextmanager
|
130 |
+
async def get_endpoint(self, max_wait_time: int = 10):
|
131 |
+
"""Get the next available endpoint using a context manager"""
|
132 |
+
start_time = time.time()
|
133 |
+
endpoint = None
|
134 |
+
|
135 |
+
try:
|
136 |
+
while True:
|
137 |
+
if time.time() - start_time > max_wait_time:
|
138 |
+
raise TimeoutError(f"Could not acquire an endpoint within {max_wait_time} seconds")
|
139 |
+
|
140 |
+
try:
|
141 |
+
endpoint = self.endpoint_queue.get_nowait()
|
142 |
+
async with self.lock:
|
143 |
+
if not endpoint.busy:
|
144 |
+
endpoint.busy = True
|
145 |
+
endpoint.last_used = time.time()
|
146 |
+
break
|
147 |
+
else:
|
148 |
+
await self.endpoint_queue.put(endpoint)
|
149 |
+
except asyncio.QueueEmpty:
|
150 |
+
await asyncio.sleep(0.5)
|
151 |
+
continue
|
152 |
+
|
153 |
+
yield endpoint
|
154 |
+
|
155 |
+
finally:
|
156 |
+
if endpoint:
|
157 |
+
async with self.lock:
|
158 |
+
endpoint.busy = False
|
159 |
+
endpoint.last_used = time.time()
|
160 |
+
await self.endpoint_queue.put(endpoint)
|
161 |
+
|
162 |
+
class ChatRoom:
|
163 |
+
def __init__(self):
|
164 |
+
self.messages = []
|
165 |
+
self.connected_clients = set()
|
166 |
+
self.max_history = 100
|
167 |
+
|
168 |
+
def add_message(self, message):
|
169 |
+
self.messages.append(message)
|
170 |
+
if len(self.messages) > self.max_history:
|
171 |
+
self.messages.pop(0)
|
172 |
+
|
173 |
+
def get_recent_messages(self, limit=50):
|
174 |
+
return self.messages[-limit:]
|
175 |
+
|
176 |
+
class VideoGenerationAPI:
|
177 |
+
def __init__(self):
|
178 |
+
self.inference_client = InferenceClient(token=HF_TOKEN)
|
179 |
+
self.hf_api = HfApi(token=HF_TOKEN)
|
180 |
+
self.endpoint_manager = EndpointManager()
|
181 |
+
self.active_requests: Dict[str, asyncio.Future] = {}
|
182 |
+
self.chat_rooms = defaultdict(ChatRoom)
|
183 |
+
self.video_events: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
184 |
+
self.event_history_limit = 50
|
185 |
+
# Cache for user roles to avoid repeated API calls
|
186 |
+
self.user_role_cache: Dict[str, Dict[str, Any]] = {}
|
187 |
+
# Cache expiration time (10 minutes)
|
188 |
+
self.cache_expiration = 600
|
189 |
+
|
190 |
+
|
191 |
+
def _add_event(self, video_id: str, event: Dict[str, Any]):
|
192 |
+
"""Add an event to the video's history and maintain the size limit"""
|
193 |
+
events = self.video_events[video_id]
|
194 |
+
events.append(event)
|
195 |
+
if len(events) > self.event_history_limit:
|
196 |
+
events.pop(0)
|
197 |
+
|
198 |
+
async def validate_user_token(self, token: str) -> UserRole:
|
199 |
+
"""
|
200 |
+
Validates a Hugging Face token and determines the user's role.
|
201 |
+
|
202 |
+
Returns one of:
|
203 |
+
- 'anon': Anonymous user (no token or invalid token)
|
204 |
+
- 'normal': Standard Hugging Face user
|
205 |
+
- 'pro': Hugging Face Pro user
|
206 |
+
- 'admin': Admin user (username in ADMIN_ACCOUNTS)
|
207 |
+
"""
|
208 |
+
# If no token is provided, the user is anonymous
|
209 |
+
if not token:
|
210 |
+
return 'anon'
|
211 |
+
|
212 |
+
# Check if we have a cached result for this token
|
213 |
+
current_time = time.time()
|
214 |
+
if token in self.user_role_cache:
|
215 |
+
cached_data = self.user_role_cache[token]
|
216 |
+
# If the cache is still valid
|
217 |
+
if current_time - cached_data['timestamp'] < self.cache_expiration:
|
218 |
+
logger.info(f"Using cached user role: {cached_data['role']}")
|
219 |
+
return cached_data['role']
|
220 |
+
|
221 |
+
# No valid cache, need to check the token with the HF API
|
222 |
+
try:
|
223 |
+
# Use HF API to validate the token and get user info
|
224 |
+
logger.info("Validating Hugging Face token...")
|
225 |
+
|
226 |
+
# Run in executor to avoid blocking the event loop
|
227 |
+
user_info = await asyncio.get_event_loop().run_in_executor(
|
228 |
+
None,
|
229 |
+
lambda: self.hf_api.whoami(token=token)
|
230 |
+
)
|
231 |
+
|
232 |
+
logger.info(f"Token valid for user: {user_info.name}")
|
233 |
+
|
234 |
+
# Determine the user role based on the information
|
235 |
+
user_role: UserRole
|
236 |
+
|
237 |
+
# Check if the user is an admin
|
238 |
+
if user_info.name in ADMIN_ACCOUNTS:
|
239 |
+
user_role = 'admin'
|
240 |
+
# Check if the user has a pro account
|
241 |
+
elif hasattr(user_info, 'is_pro') and user_info.is_pro:
|
242 |
+
user_role = 'pro'
|
243 |
+
else:
|
244 |
+
user_role = 'normal'
|
245 |
+
|
246 |
+
# Cache the result
|
247 |
+
self.user_role_cache[token] = {
|
248 |
+
'role': user_role,
|
249 |
+
'timestamp': current_time,
|
250 |
+
'username': user_info.name
|
251 |
+
}
|
252 |
+
|
253 |
+
return user_role
|
254 |
+
|
255 |
+
except Exception as e:
|
256 |
+
logger.error(f"Failed to validate Hugging Face token: {str(e)}")
|
257 |
+
# If validation fails, the user is treated as anonymous
|
258 |
+
return 'anon'
|
259 |
+
|
260 |
+
async def download_video(self, url: str) -> bytes:
|
261 |
+
"""Download video file from URL and return bytes"""
|
262 |
+
async with ClientSession() as session:
|
263 |
+
async with session.get(url) as response:
|
264 |
+
if response.status != 200:
|
265 |
+
raise Exception(f"Failed to download video: HTTP {response.status}")
|
266 |
+
return await response.read()
|
267 |
+
|
268 |
+
async def search_video(self, query: str, search_count: int = 0, attempt_count: int = 0) -> Optional[dict]:
|
269 |
+
"""Generate a single search result using HF text generation"""
|
270 |
+
prompt = f"""# Instruction
|
271 |
+
Your response MUST be a YAML object containing a title, description, and tags, consistent with what we can find on a video sharing platform.
|
272 |
+
Format your YAML response with only those fields: "title" (single string of a short sentence), "description" (single string of a few sentences to describe the visuals), and "tags" (array of strings). Do not add any other field.
|
273 |
+
The description is a prompt for a generative AI, so please describe the visual elements of the scene in details, including: camera angle and focus, people's appearance, their age, actions, precise look, clothing, the location characteristics, lighting, action, objects, weather.
|
274 |
+
Make the result unique and different from previous search results. ONLY RETURN YAML AND WITH ENGLISH CONTENT, NOT CHINESE - DO NOT ADD ANY OTHER COMMENT!
|
275 |
+
|
276 |
+
# Context
|
277 |
+
This is attempt {attempt_count} at generating search result number {search_count}.
|
278 |
+
|
279 |
+
# Input
|
280 |
+
Describe the video for this theme: "{query}".
|
281 |
+
Don't forget to repeat singular elements about the characters, location.. in your description.
|
282 |
+
|
283 |
+
# Output
|
284 |
+
|
285 |
+
```yaml
|
286 |
+
title: \""""
|
287 |
+
|
288 |
+
try:
|
289 |
+
print(f"search_video(): calling self.inference_client.text_generation({prompt}, model={TEXT_MODEL}, max_new_tokens=300, temperature=0.65)")
|
290 |
+
response = await asyncio.get_event_loop().run_in_executor(
|
291 |
+
None,
|
292 |
+
lambda: self.inference_client.text_generation(
|
293 |
+
prompt,
|
294 |
+
model=TEXT_MODEL,
|
295 |
+
max_new_tokens=300,
|
296 |
+
temperature=0.6
|
297 |
+
)
|
298 |
+
)
|
299 |
+
|
300 |
+
#print("response: ", response)
|
301 |
+
|
302 |
+
response_text = re.sub(r'^\s*\.\s*\n', '', f"title: \"{response.strip()}")
|
303 |
+
sanitized_yaml = sanitize_yaml_response(response_text)
|
304 |
+
|
305 |
+
try:
|
306 |
+
result = yaml.safe_load(sanitized_yaml)
|
307 |
+
except yaml.YAMLError as e:
|
308 |
+
logger.error(f"YAML parsing failed: {str(e)}")
|
309 |
+
result = None
|
310 |
+
|
311 |
+
if not result or not isinstance(result, dict):
|
312 |
+
logger.error(f"Invalid result format: {result}")
|
313 |
+
return None
|
314 |
+
|
315 |
+
# Extract fields with defaults
|
316 |
+
title = str(result.get('title', '')).strip() or 'Untitled Video'
|
317 |
+
description = str(result.get('description', '')).strip() or 'No description available'
|
318 |
+
tags = result.get('tags', [])
|
319 |
+
|
320 |
+
# Ensure tags is a list of strings
|
321 |
+
if not isinstance(tags, list):
|
322 |
+
tags = []
|
323 |
+
tags = [str(t).strip() for t in tags if t and isinstance(t, (str, int, float))]
|
324 |
+
|
325 |
+
# Generate thumbnail
|
326 |
+
#print(f"calling self.generate_thumbnail({title}, {description})")
|
327 |
+
try:
|
328 |
+
#thumbnail = await self.generate_thumbnail(title, description)
|
329 |
+
raise ValueError("thumbnail generation is too buggy and slow right now")
|
330 |
+
except Exception as e:
|
331 |
+
logger.error(f"Thumbnail generation failed: {str(e)}")
|
332 |
+
thumbnail = ""
|
333 |
+
|
334 |
+
print("got response thumbnail")
|
335 |
+
# Return valid result with all required fields
|
336 |
+
return {
|
337 |
+
'id': str(uuid.uuid4()),
|
338 |
+
'title': title,
|
339 |
+
'description': description,
|
340 |
+
'thumbnailUrl': thumbnail,
|
341 |
+
'videoUrl': '',
|
342 |
+
'isLatent': True,
|
343 |
+
'useFixedSeed': "webcam" in description.lower(),
|
344 |
+
'seed': generate_seed(),
|
345 |
+
'views': 0,
|
346 |
+
'tags': tags
|
347 |
+
}
|
348 |
+
|
349 |
+
except Exception as e:
|
350 |
+
logger.error(f"Search video generation failed: {str(e)}")
|
351 |
+
return None
|
352 |
+
|
353 |
+
async def generate_thumbnail(self, title: str, description: str) -> str:
|
354 |
+
"""Generate thumbnail using HF image generation"""
|
355 |
+
try:
|
356 |
+
image_prompt = f"Thumbnail for video titled '{title}': {description}"
|
357 |
+
|
358 |
+
image = await asyncio.get_event_loop().run_in_executor(
|
359 |
+
None,
|
360 |
+
lambda: self.inference_client.text_to_image(
|
361 |
+
prompt=image_prompt,
|
362 |
+
model=IMAGE_MODEL,
|
363 |
+
width=1024,
|
364 |
+
height=512
|
365 |
+
)
|
366 |
+
)
|
367 |
+
|
368 |
+
buffered = io.BytesIO()
|
369 |
+
image.save(buffered, format="JPEG")
|
370 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
371 |
+
return f"data:image/jpeg;base64,{img_str}"
|
372 |
+
except Exception as e:
|
373 |
+
logger.error(f"Error generating thumbnail: {str(e)}")
|
374 |
+
return ""
|
375 |
+
|
376 |
+
async def generate_caption(self, title: str, description: str) -> str:
|
377 |
+
"""Generate detailed caption using HF text generation"""
|
378 |
+
try:
|
379 |
+
prompt = f"""Generate a detailed story for a video named: "{title}"
|
380 |
+
Visual description of the video: {description}.
|
381 |
+
Instructions: Write the story summary, including the plot, action, what should happen.
|
382 |
+
Make it around 200-300 words long.
|
383 |
+
A video can be anything from a tutorial, webcam, trailer, movie, live stream etc."""
|
384 |
+
|
385 |
+
response = await asyncio.get_event_loop().run_in_executor(
|
386 |
+
None,
|
387 |
+
lambda: self.inference_client.text_generation(
|
388 |
+
prompt,
|
389 |
+
model=TEXT_MODEL,
|
390 |
+
max_new_tokens=180,
|
391 |
+
temperature=0.7
|
392 |
+
)
|
393 |
+
)
|
394 |
+
|
395 |
+
if "Caption: " in response:
|
396 |
+
response = response.replace("Caption: ", "")
|
397 |
+
|
398 |
+
chunks = f" {response} ".split(". ")
|
399 |
+
if len(chunks) > 1:
|
400 |
+
text = ". ".join(chunks[:-1])
|
401 |
+
else:
|
402 |
+
text = response
|
403 |
+
|
404 |
+
return text.strip()
|
405 |
+
except Exception as e:
|
406 |
+
logger.error(f"Error generating caption: {str(e)}")
|
407 |
+
return ""
|
408 |
+
|
409 |
+
|
410 |
+
def get_config_value(self, role: UserRole, field: str, options: dict = None) -> Any:
|
411 |
+
"""
|
412 |
+
Get the appropriate config value for a user role.
|
413 |
+
|
414 |
+
Args:
|
415 |
+
role: The user role ('anon', 'normal', 'pro', 'admin')
|
416 |
+
field: The config field name to retrieve
|
417 |
+
options: Optional user-provided options that may override defaults
|
418 |
+
|
419 |
+
Returns:
|
420 |
+
The config value appropriate for the user's role with respect to
|
421 |
+
min/max boundaries and user overrides.
|
422 |
+
"""
|
423 |
+
# Select the appropriate config based on user role
|
424 |
+
if role == 'admin':
|
425 |
+
config = CONFIG_FOR_ADMIN_HF_USERS
|
426 |
+
elif role == 'pro':
|
427 |
+
config = CONFIG_FOR_PRO_HF_USERS
|
428 |
+
elif role == 'normal':
|
429 |
+
config = CONFIG_FOR_STANDARD_HF_USERS
|
430 |
+
else: # Anonymous users
|
431 |
+
config = CONFIG_FOR_ANONYMOUS_USERS
|
432 |
+
|
433 |
+
# Get the default value for this field from the config
|
434 |
+
default_value = config.get(f"default_{field}", None)
|
435 |
+
|
436 |
+
# For fields that have min/max bounds
|
437 |
+
min_field = f"min_{field}"
|
438 |
+
max_field = f"max_{field}"
|
439 |
+
|
440 |
+
# Check if min/max constraints exist for this field
|
441 |
+
has_constraints = min_field in config or max_field in config
|
442 |
+
|
443 |
+
if not has_constraints:
|
444 |
+
# For fields without constraints, just return the value from config
|
445 |
+
return default_value
|
446 |
+
|
447 |
+
# Get min and max values from config (if they exist)
|
448 |
+
min_value = config.get(min_field, None)
|
449 |
+
max_value = config.get(max_field, None)
|
450 |
+
|
451 |
+
# If user provided options with this field
|
452 |
+
if options and field in options:
|
453 |
+
user_value = options[field]
|
454 |
+
|
455 |
+
# Apply constraints if they exist
|
456 |
+
if min_value is not None and user_value < min_value:
|
457 |
+
return min_value
|
458 |
+
if max_value is not None and user_value > max_value:
|
459 |
+
return max_value
|
460 |
+
|
461 |
+
# If within bounds, use the user's value
|
462 |
+
return user_value
|
463 |
+
|
464 |
+
# If no user value, return the default
|
465 |
+
return default_value
|
466 |
+
|
467 |
+
async def _generate_clip_prompt(self, video_id: str, title: str, description: str) -> str:
|
468 |
+
"""Generate a new prompt for the next clip based on event history"""
|
469 |
+
events = self.video_events.get(video_id, [])
|
470 |
+
events_json = "\n".join(json.dumps(event) for event in events)
|
471 |
+
|
472 |
+
prompt = f"""# Context and task
|
473 |
+
Please write the caption for a new clip.
|
474 |
+
|
475 |
+
# Instructions
|
476 |
+
1. Consider the video context and recent events
|
477 |
+
2. Create a natural progression from previous clips
|
478 |
+
3. Take into account user suggestions (chat messages) into the scene
|
479 |
+
4. Don't generate hateful, political, violent or sexual content
|
480 |
+
5. Keep visual consistency with previous clips (in most cases you should repeat the same exact description of the location, characters etc but only change a few elements. If this is a webcam scenario, don't touch the camera orientation or focus)
|
481 |
+
6. Return ONLY the caption text, no additional formatting or explanation
|
482 |
+
7. Write in English, about 200 words.
|
483 |
+
8. Your caption must describe visual elements of the scene in details, including: camera angle and focus, people's appearance, age, look, costumes, clothes, the location visual characteristics and geometry, lighting, action, objects, weather, textures, lighting.
|
484 |
+
|
485 |
+
# Examples
|
486 |
+
Here is a demo scenario, with fake data:
|
487 |
+
{{"time": "2024-11-29T13:36:15Z", "event": "new_stream_clip", "caption": "webcam view of a beautiful park, squirrels are playing in the lush grass, blablabla etc... (rest omitted for brevity)"}}
|
488 |
+
{{"time": "2024-11-29T13:36:20Z", "event": "new_chat_message", "username": "MonkeyLover89", "data": "hi"}}
|
489 |
+
{{"time": "2024-11-29T13:36:25Z", "event": "new_chat_message", "username": "MonkeyLover89", "data": "more squirrels plz"}}
|
490 |
+
{{"time": "2024-11-29T13:36:26Z", "event": "new_stream_clip", "caption": "webcam view of a beautiful park, a lot of squirrels are playing in the lush grass, blablabla etc... (rest omitted for brevity)"}}
|
491 |
+
|
492 |
+
# Real scenario and data
|
493 |
+
|
494 |
+
We are inside a video titled "{title}"
|
495 |
+
The video is described by: "{description}".
|
496 |
+
Here is a summary of the {len(events)} most recent events:
|
497 |
+
{events_json}
|
498 |
+
|
499 |
+
# Your response
|
500 |
+
Your caption:"""
|
501 |
+
|
502 |
+
try:
|
503 |
+
response = await asyncio.get_event_loop().run_in_executor(
|
504 |
+
None,
|
505 |
+
lambda: self.inference_client.text_generation(
|
506 |
+
prompt,
|
507 |
+
model=TEXT_MODEL,
|
508 |
+
max_new_tokens=200,
|
509 |
+
temperature=0.7
|
510 |
+
)
|
511 |
+
)
|
512 |
+
|
513 |
+
# Clean up the response
|
514 |
+
caption = response.strip()
|
515 |
+
if caption.lower().startswith("caption:"):
|
516 |
+
caption = caption[8:].strip()
|
517 |
+
|
518 |
+
return caption
|
519 |
+
|
520 |
+
except Exception as e:
|
521 |
+
logger.error(f"Error generating clip prompt: {str(e)}")
|
522 |
+
# Fallback to original description if prompt generation fails
|
523 |
+
return description
|
524 |
+
|
525 |
+
async def generate_video(self, title: str, description: str, video_prompt_prefix: str, options: dict, user_role: UserRole = 'anon') -> str:
|
526 |
+
"""Generate video using available space from pool"""
|
527 |
+
video_id = options.get('video_id', str(uuid.uuid4()))
|
528 |
+
|
529 |
+
# Generate a new prompt based on event history
|
530 |
+
#clip_caption = await self._generate_clip_prompt(video_id, title, description)
|
531 |
+
clip_caption = f"{video_prompt_prefix} - {title.strip()} - {description.strip()}"
|
532 |
+
|
533 |
+
# Add the new clip to event history
|
534 |
+
self._add_event(video_id, {
|
535 |
+
"time": datetime.datetime.utcnow().isoformat() + "Z",
|
536 |
+
"event": "new_stream_clip",
|
537 |
+
"caption": clip_caption
|
538 |
+
})
|
539 |
+
|
540 |
+
# Use the generated caption as the prompt
|
541 |
+
prompt = f"{clip_caption}, {POSITIVE_PROMPT_SUFFIX}"
|
542 |
+
|
543 |
+
# Get the config values based on user role
|
544 |
+
width = self.get_config_value(user_role, 'clip_width', options)
|
545 |
+
height = self.get_config_value(user_role, 'clip_height', options)
|
546 |
+
num_frames = self.get_config_value(user_role, 'num_frames', options)
|
547 |
+
num_inference_steps = self.get_config_value(user_role, 'num_inference_steps', options)
|
548 |
+
frame_rate = self.get_config_value(user_role, 'clip_framerate', options)
|
549 |
+
|
550 |
+
# Log the user role and config values being used
|
551 |
+
logger.info(f"Generating video for user with role: {user_role}")
|
552 |
+
logger.info(f"Using config values: width={width}, height={height}, num_frames={num_frames}, steps={num_inference_steps}, fps={frame_rate}")
|
553 |
+
|
554 |
+
json_payload = {
|
555 |
+
"inputs": {
|
556 |
+
"prompt": prompt,
|
557 |
+
},
|
558 |
+
"parameters": {
|
559 |
+
|
560 |
+
# ------------------- settings for LTX-Video -----------------------
|
561 |
+
|
562 |
+
# this param doesn't exist
|
563 |
+
#"enhance_prompt_toggle": options.get('enhance_prompt', False),
|
564 |
+
|
565 |
+
"negative_prompt": options.get('negative_prompt', NEGATIVE_PROMPT),
|
566 |
+
|
567 |
+
# note about resolution:
|
568 |
+
# we cannot use 720 since it cannot be divided by 32
|
569 |
+
"width": width,
|
570 |
+
"height": height,
|
571 |
+
|
572 |
+
# this is a hack to fool LTX-Video into believing our input image is an actual video frame with poor encoding quality
|
573 |
+
#"input_image_quality": 70,
|
574 |
+
|
575 |
+
# LTX-Video requires a frame number divisible by 8, plus one frame
|
576 |
+
# note: glitches might appear if you use more than 168 frames
|
577 |
+
"num_frames": num_frames,
|
578 |
+
|
579 |
+
# using 30 steps seems to be enough for most cases, otherwise use 50 for best quality
|
580 |
+
# I think using a large number of steps (> 30) might create some overexposure and saturation
|
581 |
+
"num_inference_steps": num_inference_steps,
|
582 |
+
|
583 |
+
# values between 3.0 and 4.0 are nice
|
584 |
+
"guidance_scale": options.get('guidance_scale', GUIDANCE_SCALE),
|
585 |
+
|
586 |
+
"seed": options.get('seed', 42),
|
587 |
+
|
588 |
+
# ----------------------------------------------------------------
|
589 |
+
|
590 |
+
# ------------------- settings for Varnish -----------------------
|
591 |
+
# This will double the number of frames.
|
592 |
+
# You can activate this if you want:
|
593 |
+
# - a slow motion effect (in that case use double_num_frames=True and fps=24, 25 or 30)
|
594 |
+
# - a HD soap / video game effect (in that case use double_num_frames=True and fps=60)
|
595 |
+
"double_num_frames": False, # <- False as we want real-time generation
|
596 |
+
|
597 |
+
# controls the number of frames per second
|
598 |
+
# use this in combination with the num_frames and double_num_frames settings to control the duration and "feel" of your video
|
599 |
+
# typical values are: 24, 25, 30, 60
|
600 |
+
"fps": frame_rate,
|
601 |
+
|
602 |
+
# upscale the video using Real-ESRGAN.
|
603 |
+
# This upscaling algorithm is relatively fast,
|
604 |
+
# but might create an uncanny "3D render" or "drawing" effect.
|
605 |
+
"super_resolution": False, # <- False as we want real-time generation
|
606 |
+
|
607 |
+
# for cosmetic purposes and get a "cinematic" feel, you can optionally add some film grain.
|
608 |
+
# it is not recommended to add film grain if your theme doesn't match (film grain is great for black & white, retro looks)
|
609 |
+
# and if you do, adding more than 12% will start to negatively impact file size (video codecs aren't great are compressing film grain)
|
610 |
+
# 0% = no grain
|
611 |
+
# 10% = a bit of grain
|
612 |
+
"grain_amount": 0, # value between 0-100
|
613 |
+
|
614 |
+
|
615 |
+
# The range of the CRF scale is 0–51, where:
|
616 |
+
# 0 is lossless (for 8 bit only, for 10 bit use -qp 0)
|
617 |
+
# 23 is the default
|
618 |
+
# 51 is worst quality possible
|
619 |
+
# A lower value generally leads to higher quality, and a subjectively sane range is 17–28.
|
620 |
+
# Consider 17 or 18 to be visually lossless or nearly so;
|
621 |
+
# it should look the same or nearly the same as the input but it isn't technically lossless.
|
622 |
+
# The range is exponential, so increasing the CRF value +6 results in roughly half the bitrate / file size, while -6 leads to roughly twice the bitrate.
|
623 |
+
#"quality": 18,
|
624 |
+
|
625 |
+
}
|
626 |
+
}
|
627 |
+
|
628 |
+
async with self.endpoint_manager.get_endpoint() as endpoint:
|
629 |
+
#logger.info(f"Using endpoint {endpoint.id} for video generation with prompt: {prompt}")
|
630 |
+
|
631 |
+
async with ClientSession() as session:
|
632 |
+
async with session.post(
|
633 |
+
endpoint.url,
|
634 |
+
headers={
|
635 |
+
"Accept": "application/json",
|
636 |
+
"Authorization": f"Bearer {HF_TOKEN}",
|
637 |
+
"Content-Type": "application/json"
|
638 |
+
},
|
639 |
+
json=json_payload
|
640 |
+
) as response:
|
641 |
+
if response.status != 200:
|
642 |
+
error_text = await response.text()
|
643 |
+
raise Exception(f"Video generation failed: HTTP {response.status} - {error_text}")
|
644 |
+
|
645 |
+
result = await response.json()
|
646 |
+
|
647 |
+
if "error" in result:
|
648 |
+
raise Exception(f"Video generation failed: {result['error']}")
|
649 |
+
|
650 |
+
video_data_uri = result.get("video")
|
651 |
+
if not video_data_uri:
|
652 |
+
raise Exception("No video data in response")
|
653 |
+
|
654 |
+
return video_data_uri
|
655 |
+
|
656 |
+
|
657 |
+
async def handle_chat_message(self, data: dict, ws: web.WebSocketResponse) -> dict:
|
658 |
+
"""Process and broadcast a chat message"""
|
659 |
+
video_id = data.get('videoId')
|
660 |
+
request_id = data.get('requestId')
|
661 |
+
|
662 |
+
if not video_id:
|
663 |
+
return {
|
664 |
+
'action': 'chat_message',
|
665 |
+
'requestId': request_id,
|
666 |
+
'success': False,
|
667 |
+
'error': 'No video ID provided'
|
668 |
+
}
|
669 |
+
|
670 |
+
# Add chat message to event history
|
671 |
+
self._add_event(video_id, {
|
672 |
+
"time": datetime.datetime.utcnow().isoformat() + "Z",
|
673 |
+
"event": "new_chat_message",
|
674 |
+
"username": data.get('username', 'Anonymous'),
|
675 |
+
"data": data.get('content', '')
|
676 |
+
})
|
677 |
+
|
678 |
+
room = self.chat_rooms[video_id]
|
679 |
+
message_data = {k: v for k, v in data.items() if k != '_ws'}
|
680 |
+
room.add_message(message_data)
|
681 |
+
|
682 |
+
for client in room.connected_clients:
|
683 |
+
if client != ws:
|
684 |
+
try:
|
685 |
+
await client.send_json({
|
686 |
+
'action': 'chat_message',
|
687 |
+
'broadcast': True,
|
688 |
+
**message_data
|
689 |
+
})
|
690 |
+
except Exception as e:
|
691 |
+
logger.error(f"Failed to broadcast to client: {e}")
|
692 |
+
room.connected_clients.remove(client)
|
693 |
+
|
694 |
+
return {
|
695 |
+
'action': 'chat_message',
|
696 |
+
'requestId': request_id,
|
697 |
+
'success': True,
|
698 |
+
'message': message_data
|
699 |
+
}
|
700 |
+
|
701 |
+
async def handle_join_chat(self, data: dict, ws: web.WebSocketResponse) -> dict:
|
702 |
+
"""Handle a request to join a chat room"""
|
703 |
+
video_id = data.get('videoId')
|
704 |
+
request_id = data.get('requestId')
|
705 |
+
|
706 |
+
if not video_id:
|
707 |
+
return {
|
708 |
+
'action': 'join_chat',
|
709 |
+
'requestId': request_id,
|
710 |
+
'success': False,
|
711 |
+
'error': 'No video ID provided'
|
712 |
+
}
|
713 |
+
|
714 |
+
room = self.chat_rooms[video_id]
|
715 |
+
room.connected_clients.add(ws)
|
716 |
+
recent_messages = room.get_recent_messages()
|
717 |
+
|
718 |
+
return {
|
719 |
+
'action': 'join_chat',
|
720 |
+
'requestId': request_id,
|
721 |
+
'success': True,
|
722 |
+
'messages': recent_messages
|
723 |
+
}
|
724 |
+
|
725 |
+
async def handle_leave_chat(self, data: dict, ws: web.WebSocketResponse) -> dict:
|
726 |
+
"""Handle a request to leave a chat room"""
|
727 |
+
video_id = data.get('videoId')
|
728 |
+
request_id = data.get('requestId')
|
729 |
+
|
730 |
+
if not video_id:
|
731 |
+
return {
|
732 |
+
'action': 'leave_chat',
|
733 |
+
'requestId': request_id,
|
734 |
+
'success': False,
|
735 |
+
'error': 'No video ID provided'
|
736 |
+
}
|
737 |
+
|
738 |
+
room = self.chat_rooms[video_id]
|
739 |
+
if ws in room.connected_clients:
|
740 |
+
room.connected_clients.remove(ws)
|
741 |
+
|
742 |
+
return {
|
743 |
+
'action': 'leave_chat',
|
744 |
+
'requestId': request_id,
|
745 |
+
'success': True
|
746 |
+
}
|
docs/for-bots/flutter-videos/fvp.md
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fvp 0.31.2 
|
2 |
+
|
3 |
+
fvp: ^0.31.2 copied to clipboard
|
4 |
+
|
5 |
+
|
6 |
+
======================================================================================================================================================================
|
7 |
+
|
8 |
+
Published 9 days ago • [mediadevkit.com](/publishers/mediadevkit.com)Dart 3 compatible
|
9 |
+
|
10 |
+
SDK[Flutter](/packages?q=sdk%3Aflutter "Packages compatible with Flutter SDK")
|
11 |
+
|
12 |
+
Platform[Android](/packages?q=platform%3Aandroid "Packages compatible with Android platform")[iOS](/packages?q=platform%3Aios "Packages compatible with iOS platform")[Linux](/packages?q=platform%3Alinux "Packages compatible with Linux platform")[macOS](/packages?q=platform%3Amacos "Packages compatible with macOS platform")[Windows](/packages?q=platform%3Awindows "Packages compatible with Windows platform")
|
13 |
+
|
14 |
+
123
|
15 |
+
|
16 |
+
→
|
17 |
+
|
18 |
+
### Metadata
|
19 |
+
|
20 |
+
video\_player plugin and backend APIs. Support all desktop/mobile platforms with hardware decoders, optimal renders. Supports most formats via FFmpeg
|
21 |
+
|
22 |
+
More...
|
23 |
+
|
24 |
+
* Readme
|
25 |
+
* [Changelog](/packages/fvp/changelog)
|
26 |
+
* [Example](/packages/fvp/example)
|
27 |
+
* [Installing](/packages/fvp/install)
|
28 |
+
* [Versions](/packages/fvp/versions)
|
29 |
+
* [Scores](/packages/fvp/score)
|
30 |
+
|
31 |
+
FVP [#](#fvp)
|
32 |
+
=============
|
33 |
+
|
34 |
+
A plugin for official [Flutter Video Player](https://pub.dev/packages/video_player) to support all desktop and mobile platforms, with hardware accelerated decoding and optimal rendering. Based on [libmdk](https://github.com/wang-bin/mdk-sdk). You can also create your own players other than official `video_player` with [backend player api](#backend-player-api)
|
35 |
+
|
36 |
+
Prebuilt example can be download from artifacts of [github actions](https://github.com/wang-bin/fvp/actions).
|
37 |
+
|
38 |
+
[More examples are here](https://github.com/wang-bin/mdk-examples/tree/master/flutter)
|
39 |
+
|
40 |
+
project is create with `flutter create -t plugin --platforms=linux,macos,windows,android,ios -i objc -a java fvp`
|
41 |
+
|
42 |
+
Features [#](#features)
|
43 |
+
-----------------------
|
44 |
+
|
45 |
+
* All platforms: Windows x64(including win7) and arm64, Linux x64 and arm64, macOS, iOS, Android(requires flutter > 3.19 because of minSdk 21).
|
46 |
+
* You can choose official implementation or this plugin's
|
47 |
+
* Optimal render api: d3d11 for windows, metal for macOS/iOS, OpenGL for Linux and Android(Impeller support)
|
48 |
+
* Hardware decoders are enabled by default
|
49 |
+
* Dolby Vision support on all platforms
|
50 |
+
* Minimal code change for existing [Video Player](https://pub.dev/packages/video_player) apps
|
51 |
+
* Support most formats via FFmpeg demuxer and software decoders if not supported by gpu. You can use your own ffmpeg 4.0~7.1(or master branch) by removing bundled ffmpeg dynamic library.
|
52 |
+
* High performance. Lower cpu, gpu and memory load than libmpv based players.
|
53 |
+
* Support audio without video
|
54 |
+
* HEVC, VP8 and VP9 transparent video
|
55 |
+
* Small footprint. Only about 10MB size increase per cpu architecture(platform dependent).
|
56 |
+
|
57 |
+
How to Use [#](#how-to-use)
|
58 |
+
---------------------------
|
59 |
+
|
60 |
+
* Add [fvp](https://pub.dev/packages/fvp) in your pubspec.yaml dependencies: `flutter pub add fvp`. Since flutter 3.27, fvp must be a direct dependency in your app's pubspec.yaml.
|
61 |
+
* **(Optional)** Add 2 lines in your video\_player examples. Without this step, this plugin will be used for video\_player unsupported platforms(windows, linux), official implementation will be used otherwise.
|
62 |
+
|
63 |
+
import 'package:fvp/fvp.dart' as fvp;
|
64 |
+
|
65 |
+
fvp.registerWith(); // in main() or anywhere before creating a player. use fvp for all platforms.
|
66 |
+
|
67 |
+
|
68 |
+
copied to clipboard
|
69 |
+
|
70 |
+
You can also select the platforms to enable fvp implementation
|
71 |
+
|
72 |
+
registerWith(options: {'platforms': ['windows', 'macos', 'linux']}); // only these platforms will use this plugin implementation
|
73 |
+
|
74 |
+
|
75 |
+
copied to clipboard
|
76 |
+
|
77 |
+
To select [other decoders](https://github.com/wang-bin/mdk-sdk/wiki/Decoders), pass options like this
|
78 |
+
|
79 |
+
fvp.registerWith(options: {
|
80 |
+
'video.decoders': ['D3D11', 'NVDEC', 'FFmpeg']
|
81 |
+
//'lowLatency': 1, // optional for network streams
|
82 |
+
}); // windows
|
83 |
+
|
84 |
+
|
85 |
+
copied to clipboard
|
86 |
+
|
87 |
+
[The document](https://pub.dev/documentation/fvp/latest/fvp/registerWith.html) lists all options for `registerWith()`
|
88 |
+
|
89 |
+
### Error Handling [#](#error-handling)
|
90 |
+
|
91 |
+
Errors are usually produced when loading a media.
|
92 |
+
|
93 |
+
_controller.addListener(() {
|
94 |
+
if (_controller.value.hasError && !_controller.value.isCompleted) {
|
95 |
+
...
|
96 |
+
|
97 |
+
|
98 |
+
copied to clipboard
|
99 |
+
|
100 |
+
### Backend Player API [#](#backend-player-api)
|
101 |
+
|
102 |
+
import 'package:fvp/mdk.dart';
|
103 |
+
|
104 |
+
|
105 |
+
copied to clipboard
|
106 |
+
|
107 |
+
The plugin implements [VideoPlayerPlatform](https://pub.dev/packages/video_player_platform_interface) via [a thin wrapper](https://github.com/wang-bin/fvp/blob/master/lib/video_player_mdk.dart) on [player.dart](https://github.com/wang-bin/fvp/blob/master/lib/src/player.dart).
|
108 |
+
|
109 |
+
Now we also expose this backend player api so you can create your own players easily, and gain more features than official [video\_player](https://pub.dev/packages/video_player), for example, play from a given position, loop in a range, decoder selection, media information detail etc. You can also reuse the Player instance without unconditionally create and dispose, changing the `Player.media` is enough. [This is an example](https://github.com/wang-bin/mdk-examples/blob/master/flutter/simple/lib/multi_textures.dart)
|
110 |
+
|
111 |
+
### VideoPlayerController Extensions [#](#videoplayercontroller-extensions)
|
112 |
+
|
113 |
+
With this extension, we can leverage mature `video_player` code without rewriting a new one via backend player api, but gain more features, for example `snapshot()`, `record()`, `fastSeekTo()`, `setExternalSubtitle()`.
|
114 |
+
|
115 |
+
import 'package:fvp/fvp.dart' as fvp;
|
116 |
+
|
117 |
+
fvp.registerWith(); // in main() or anywhere before creating a player. use fvp for all platforms.
|
118 |
+
|
119 |
+
// somewhere after controller is initialized
|
120 |
+
_controller.record('rtmp://127.0.0.1/live/test');
|
121 |
+
|
122 |
+
|
123 |
+
copied to clipboard
|
124 |
+
|
125 |
+
Upgrade Dependencies Manually [#](#upgrade-dependencies-manually)
|
126 |
+
=================================================================
|
127 |
+
|
128 |
+
Upgrading binary dependencies can bring new features and backend bug fixes. For macOS and iOS, in your project dir, run
|
129 |
+
|
130 |
+
pod cache clean mdk
|
131 |
+
find . -name Podfile.lock -delete
|
132 |
+
rm -rf {mac,i}os/Pods
|
133 |
+
|
134 |
+
|
135 |
+
copied to clipboard
|
136 |
+
|
137 |
+
For other platforms, set environment var `FVP_DEPS_LATEST=1` and rebuilt, will upgrade to the latest sdk. If fvp is installed from pub.dev, run `flutter pub cache clean` is another option.
|
138 |
+
|
139 |
+
Design [#](#design)
|
140 |
+
===================
|
141 |
+
|
142 |
+
* Playback control api in dart via ffi
|
143 |
+
* Manage video renderers in platform specific manners. Receive player ptr via `MethodChannel` to construct player instance and set a renderer target.
|
144 |
+
* Callbacks and events in C++ are notified by ReceivePort
|
145 |
+
* Function with a one time callback is async and returns a future
|
146 |
+
|
147 |
+
Enable Subtitles [#](#enable-subtitles)
|
148 |
+
=======================================
|
149 |
+
|
150 |
+
libass is required, and it's added to your app automatically for windows, macOS and android(remove ass.dll, libass.dylib and libass.so from mdk-sdk if you don't need it). For iOS, [download](https://sourceforge.net/projects/mdk-sdk/files/deps/dep.7z/download) and add `ass.framework` to your xcode project. For linux, system libass can be used, you may have to install manually via system package manager.
|
151 |
+
|
152 |
+
If required subtitle font is not found in the system(e.g. android), you can add [assets/subfont.ttf](https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf) in pubspec.yaml assets as the fallback. Optionally you can also download the font file by fvp like this
|
153 |
+
|
154 |
+
fvp.registerWith(options: {
|
155 |
+
'subtitleFontFile': 'https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf'
|
156 |
+
});
|
157 |
+
|
158 |
+
|
159 |
+
copied to clipboard
|
160 |
+
|
161 |
+
DO NOT use flutter-snap [#](#do-not-use-flutter-snap)
|
162 |
+
=====================================================
|
163 |
+
|
164 |
+
Flutter can be installed by snap, but it will add some [enviroment vars(`CPLUS_INCLUDE_PATH` and `LIBRARY_PATH`) which may break C++ compiler](https://github.com/canonical/flutter-snap/blob/main/env.sh#L15-L18). It's not recommended to use snap, althrough building for linux is [fixed](https://github.com/wang-bin/fvp/commit/567c68270ba16b95b1198ae58850707ae4ad7b22), but it's not possible for android.
|
165 |
+
|
166 |
+
Screenshots [#](#screenshots)
|
167 |
+
=============================
|
168 |
+
|
169 |
+
     
|
170 |
+
|
171 |
+
[
|
172 |
+
|
173 |
+
123
|
174 |
+
|
175 |
+
likes
|
176 |
+
|
177 |
+
150
|
178 |
+
|
179 |
+
points
|
180 |
+
|
181 |
+
4.48k
|
182 |
+
|
183 |
+
downloads
|
184 |
+
|
185 |
+
|
186 |
+
|
187 |
+
](/packages/fvp/score)
|
188 |
+
|
189 |
+
### Publisher
|
190 |
+
|
191 |
+
[mediadevkit.com](/publishers/mediadevkit.com)
|
192 |
+
|
193 |
+
### Weekly Downloads
|
194 |
+
|
195 |
+
2024.10.07 - 2025.04.21
|
196 |
+
|
197 |
+
### Metadata
|
198 |
+
|
199 |
+
video\_player plugin and backend APIs. Support all desktop/mobile platforms with hardware decoders, optimal renders. Supports most formats via FFmpeg
|
200 |
+
|
201 |
+
[Repository (GitHub)](https://github.com/wang-bin/fvp)
|
202 |
+
|
203 |
+
### Topics
|
204 |
+
|
205 |
+
[#video](/packages?q=topic%3Avideo) [#player](/packages?q=topic%3Aplayer) [#video-player](/packages?q=topic%3Avideo-player) [#audio-player](/packages?q=topic%3Aaudio-player) [#videoplayer](/packages?q=topic%3Avideoplayer)
|
206 |
+
|
207 |
+
### Documentation
|
208 |
+
|
209 |
+
[API reference](/documentation/fvp/latest/)
|
210 |
+
|
211 |
+
### License
|
212 |
+
|
213 |
+
BSD-3-Clause ([license](/packages/fvp/license))
|
214 |
+
|
215 |
+
### Dependencies
|
216 |
+
|
217 |
+
[ffi](/packages/ffi "^2.1.0"), [flutter](https://api.flutter.dev/), [http](/packages/http "^1.0.0"), [logging](/packages/logging "^1.2.0"), [path](/packages/path "^1.8.0"), [path\_provider](/packages/path_provider "^2.1.2"), [plugin\_platform\_interface](/packages/plugin_platform_interface "^2.0.0"), [video\_player](/packages/video_player "^2.6.0"), [video\_player\_platform\_interface](/packages/video_player_platform_interface "^6.2.0")
|
218 |
+
|
219 |
+
### More
|
220 |
+
|
221 |
+
[Packages that depend on fvp](/packages?q=dependency%3Afvp)
|
222 |
+
|
223 |
+
{"@context":"http\\u003a\\u002f\\u002fschema.org","@type":"SoftwareSourceCode","name":"fvp","version":"0.31.2","description":"fvp - video\\u005fplayer plugin and backend APIs. Support all desktop\\u002fmobile platforms with hardware decoders, optimal renders. Supports most formats via FFmpeg","url":"https\\u003a\\u002f\\u002fpub.dev\\u002fpackages\\u002ffvp","dateCreated":"2023-06-26T16\\u003a23\\u003a33.605901Z","dateModified":"2025-04-14T04\\u003a14\\u003a34.630262Z","programmingLanguage":"Dart","image":"https\\u003a\\u002f\\u002fpub.dev\\u002fstatic\\u002fimg\\u002fpub-dev-icon-cover-image.png","license":"https\\u003a\\u002f\\u002fpub.dev\\u002fpackages\\u002ffvp\\u002flicense"}
|
docs/for-bots/flutter-videos/fvp_usage_example__main.dart
ADDED
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Copyright 2013 The Flutter Authors. All rights reserved.
|
2 |
+
// Use of this source code is governed by a BSD-style license that can be
|
3 |
+
// found in the LICENSE file.
|
4 |
+
|
5 |
+
// This file is used to extract code samples for the README.md file.
|
6 |
+
// Run update-excerpts if you modify this file.
|
7 |
+
|
8 |
+
// ignore_for_file: library_private_types_in_public_api, public_member_api_docs
|
9 |
+
|
10 |
+
// #docregion basic-example
|
11 |
+
import 'dart:io';
|
12 |
+
|
13 |
+
import 'package:flutter/material.dart';
|
14 |
+
import 'package:flutter/services.dart';
|
15 |
+
import 'package:video_player/video_player.dart';
|
16 |
+
import 'package:fvp/fvp.dart';
|
17 |
+
import 'package:logging/logging.dart';
|
18 |
+
import 'package:intl/intl.dart';
|
19 |
+
import 'package:file_selector/file_selector.dart';
|
20 |
+
import 'package:image/image.dart' as img;
|
21 |
+
|
22 |
+
String? source;
|
23 |
+
|
24 |
+
void main(List<String> args) async {
|
25 |
+
// set logger before registerWith()
|
26 |
+
Logger.root.level = Level.ALL;
|
27 |
+
final df = DateFormat("HH:mm:ss.SSS");
|
28 |
+
Logger.root.onRecord.listen((record) {
|
29 |
+
debugPrint(
|
30 |
+
'${record.loggerName}.${record.level.name}: ${df.format(record.time)}: ${record.message}',
|
31 |
+
wrapWidth: 0x7FFFFFFFFFFFFFFF);
|
32 |
+
});
|
33 |
+
|
34 |
+
final opts = <String, Object>{};
|
35 |
+
final globalOpts = <String, Object>{};
|
36 |
+
int i = 0;
|
37 |
+
bool useFvp = true;
|
38 |
+
opts['subtitleFontFile'] =
|
39 |
+
'https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf';
|
40 |
+
for (; i < args.length; i++) {
|
41 |
+
if (args[i] == '-c:v') {
|
42 |
+
opts['video.decoders'] = [args[++i]];
|
43 |
+
} else if (args[i] == '-maxSize') {
|
44 |
+
// ${w}x${h}
|
45 |
+
final size = args[++i].split('x');
|
46 |
+
opts['maxWidth'] = int.parse(size[0]);
|
47 |
+
opts['maxHeight'] = int.parse(size[1]);
|
48 |
+
} else if (args[i] == '-fvp') {
|
49 |
+
useFvp = int.parse(args[++i]) > 0;
|
50 |
+
} else if (args[i].startsWith('-')) {
|
51 |
+
globalOpts[args[i].substring(1)] = args[++i];
|
52 |
+
} else {
|
53 |
+
break;
|
54 |
+
}
|
55 |
+
}
|
56 |
+
if (globalOpts.isNotEmpty) {
|
57 |
+
opts['global'] = globalOpts;
|
58 |
+
}
|
59 |
+
opts['lowLatency'] = 0;
|
60 |
+
|
61 |
+
if (i <= args.length - 1) source = args[args.length - 1];
|
62 |
+
source ??= await getStartFile();
|
63 |
+
|
64 |
+
if (useFvp) {
|
65 |
+
registerWith(options: opts);
|
66 |
+
}
|
67 |
+
|
68 |
+
runApp(const MaterialApp(
|
69 |
+
localizationsDelegates: [DefaultMaterialLocalizations.delegate],
|
70 |
+
title: 'Video Demo',
|
71 |
+
home: VideoApp()));
|
72 |
+
}
|
73 |
+
|
74 |
+
Future<String?> getStartFile() async {
|
75 |
+
if (Platform.isMacOS) {
|
76 |
+
WidgetsFlutterBinding.ensureInitialized();
|
77 |
+
const hostApi = MethodChannel("openFile");
|
78 |
+
return await hostApi.invokeMethod("getCurrentFile");
|
79 |
+
}
|
80 |
+
return null;
|
81 |
+
}
|
82 |
+
|
83 |
+
Future<String?> showURIPicker(BuildContext context) async {
|
84 |
+
final key = GlobalKey<FormState>();
|
85 |
+
final src = TextEditingController();
|
86 |
+
String? uri;
|
87 |
+
await showModalBottomSheet(
|
88 |
+
context: context,
|
89 |
+
builder: (context) => Container(
|
90 |
+
alignment: Alignment.center,
|
91 |
+
child: Form(
|
92 |
+
key: key,
|
93 |
+
child: Padding(
|
94 |
+
padding: const EdgeInsets.all(16.0),
|
95 |
+
child: Column(
|
96 |
+
mainAxisSize: MainAxisSize.max,
|
97 |
+
mainAxisAlignment: MainAxisAlignment.start,
|
98 |
+
crossAxisAlignment: CrossAxisAlignment.center,
|
99 |
+
children: [
|
100 |
+
TextFormField(
|
101 |
+
controller: src,
|
102 |
+
style: const TextStyle(fontSize: 14.0),
|
103 |
+
decoration: const InputDecoration(
|
104 |
+
border: UnderlineInputBorder(),
|
105 |
+
labelText: 'Video URI',
|
106 |
+
),
|
107 |
+
validator: (value) {
|
108 |
+
if (value == null || value.isEmpty) {
|
109 |
+
return 'Please enter a URI';
|
110 |
+
}
|
111 |
+
return null;
|
112 |
+
},
|
113 |
+
),
|
114 |
+
Padding(
|
115 |
+
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
116 |
+
child: ElevatedButton(
|
117 |
+
onPressed: () {
|
118 |
+
if (key.currentState!.validate()) {
|
119 |
+
uri = src.text;
|
120 |
+
Navigator.of(context).maybePop();
|
121 |
+
}
|
122 |
+
},
|
123 |
+
child: const Text('Play'),
|
124 |
+
),
|
125 |
+
),
|
126 |
+
],
|
127 |
+
),
|
128 |
+
),
|
129 |
+
),
|
130 |
+
),
|
131 |
+
);
|
132 |
+
return uri;
|
133 |
+
}
|
134 |
+
|
135 |
+
/// Stateful widget to fetch and then display video content.
|
136 |
+
class VideoApp extends StatefulWidget {
|
137 |
+
const VideoApp({super.key});
|
138 |
+
|
139 |
+
@override
|
140 |
+
_VideoAppState createState() => _VideoAppState();
|
141 |
+
}
|
142 |
+
|
143 |
+
class _VideoAppState extends State<VideoApp> {
|
144 |
+
late VideoPlayerController _controller;
|
145 |
+
|
146 |
+
@override
|
147 |
+
void initState() {
|
148 |
+
super.initState();
|
149 |
+
if (source != null) {
|
150 |
+
if (File(source!).existsSync()) {
|
151 |
+
playFile(source!);
|
152 |
+
} else {
|
153 |
+
playUri(source!);
|
154 |
+
}
|
155 |
+
} else {
|
156 |
+
playUri(
|
157 |
+
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4');
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
void playFile(String path) {
|
162 |
+
_controller.dispose();
|
163 |
+
_controller = VideoPlayerController.file(File(path));
|
164 |
+
_controller.addListener(() {
|
165 |
+
setState(() {});
|
166 |
+
});
|
167 |
+
_controller.initialize().then((_) {
|
168 |
+
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
|
169 |
+
setState(() {});
|
170 |
+
_controller.play();
|
171 |
+
});
|
172 |
+
}
|
173 |
+
|
174 |
+
void playUri(String uri) {
|
175 |
+
_controller = VideoPlayerController.network(uri);
|
176 |
+
_controller.addListener(() {
|
177 |
+
setState(() {});
|
178 |
+
});
|
179 |
+
_controller.initialize().then((_) {
|
180 |
+
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
|
181 |
+
setState(() {});
|
182 |
+
_controller.play();
|
183 |
+
});
|
184 |
+
}
|
185 |
+
|
186 |
+
@override
|
187 |
+
Widget build(BuildContext context) {
|
188 |
+
return Scaffold(
|
189 |
+
floatingActionButton: Row(
|
190 |
+
children: [
|
191 |
+
FloatingActionButton(
|
192 |
+
heroTag: 'file',
|
193 |
+
tooltip: 'Open [File]',
|
194 |
+
onPressed: () async {
|
195 |
+
const XTypeGroup typeGroup = XTypeGroup(
|
196 |
+
label: 'videos',
|
197 |
+
//extensions: <String>['*'],
|
198 |
+
);
|
199 |
+
final XFile? file =
|
200 |
+
await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
201 |
+
|
202 |
+
if (file != null) {
|
203 |
+
playFile(file.path);
|
204 |
+
}
|
205 |
+
},
|
206 |
+
child: const Icon(Icons.file_open),
|
207 |
+
),
|
208 |
+
const SizedBox(width: 16.0),
|
209 |
+
FloatingActionButton(
|
210 |
+
heroTag: 'uri',
|
211 |
+
tooltip: 'Open [Uri]',
|
212 |
+
onPressed: () {
|
213 |
+
showURIPicker(context).then((value) {
|
214 |
+
if (value != null) {
|
215 |
+
playUri(value);
|
216 |
+
}
|
217 |
+
});
|
218 |
+
},
|
219 |
+
child: const Icon(Icons.link),
|
220 |
+
),
|
221 |
+
const SizedBox(width: 16.0),
|
222 |
+
FloatingActionButton(
|
223 |
+
heroTag: 'snapshot',
|
224 |
+
tooltip: 'Snapshot',
|
225 |
+
onPressed: () {
|
226 |
+
if (_controller.value.isInitialized) {
|
227 |
+
final info = _controller.getMediaInfo()?.video?[0].codec;
|
228 |
+
if (info == null) {
|
229 |
+
debugPrint('No video codec info');
|
230 |
+
return;
|
231 |
+
}
|
232 |
+
final width = info.width;
|
233 |
+
final height = info.height;
|
234 |
+
_controller.snapshot().then((value) {
|
235 |
+
if (value != null) {
|
236 |
+
// value is rgba data, must encode to png image and save as a file
|
237 |
+
final i = img.Image.fromBytes(
|
238 |
+
width: width,
|
239 |
+
height: height,
|
240 |
+
bytes: value.buffer,
|
241 |
+
numChannels: 4,
|
242 |
+
rowStride: width * 4);
|
243 |
+
final savePath =
|
244 |
+
'${Directory.systemTemp.path}/snapshot.jpg';
|
245 |
+
img.encodeJpgFile(savePath, i, quality: 70).then((value) {
|
246 |
+
final msg = value
|
247 |
+
? 'Snapshot saved to $savePath'
|
248 |
+
: 'Failed to save snapshot';
|
249 |
+
debugPrint(msg);
|
250 |
+
// show a toast
|
251 |
+
if (context.mounted) {
|
252 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
253 |
+
SnackBar(
|
254 |
+
content: Text(msg),
|
255 |
+
duration: const Duration(seconds: 2),
|
256 |
+
),
|
257 |
+
);
|
258 |
+
}
|
259 |
+
});
|
260 |
+
}
|
261 |
+
});
|
262 |
+
}
|
263 |
+
},
|
264 |
+
child: const Icon(Icons.screenshot),
|
265 |
+
),
|
266 |
+
],
|
267 |
+
),
|
268 |
+
body: Center(
|
269 |
+
child: _controller.value.isInitialized
|
270 |
+
? AspectRatio(
|
271 |
+
aspectRatio: _controller.value.aspectRatio,
|
272 |
+
child: Stack(
|
273 |
+
alignment: Alignment.bottomCenter,
|
274 |
+
children: <Widget>[
|
275 |
+
VideoPlayer(_controller),
|
276 |
+
_ControlsOverlay(controller: _controller),
|
277 |
+
VideoProgressIndicator(_controller, allowScrubbing: true),
|
278 |
+
],
|
279 |
+
),
|
280 |
+
)
|
281 |
+
: Container(),
|
282 |
+
),
|
283 |
+
);
|
284 |
+
}
|
285 |
+
|
286 |
+
@override
|
287 |
+
void dispose() {
|
288 |
+
super.dispose();
|
289 |
+
_controller.dispose();
|
290 |
+
}
|
291 |
+
}
|
292 |
+
|
293 |
+
class _ControlsOverlay extends StatelessWidget {
|
294 |
+
const _ControlsOverlay({required this.controller});
|
295 |
+
|
296 |
+
static const List<double> _examplePlaybackRates = <double>[
|
297 |
+
0.25,
|
298 |
+
0.5,
|
299 |
+
1.0,
|
300 |
+
1.5,
|
301 |
+
2.0,
|
302 |
+
3.0,
|
303 |
+
5.0,
|
304 |
+
10.0,
|
305 |
+
];
|
306 |
+
|
307 |
+
final VideoPlayerController controller;
|
308 |
+
|
309 |
+
@override
|
310 |
+
Widget build(BuildContext context) {
|
311 |
+
return Stack(
|
312 |
+
children: <Widget>[
|
313 |
+
AnimatedSwitcher(
|
314 |
+
duration: const Duration(milliseconds: 50),
|
315 |
+
reverseDuration: const Duration(milliseconds: 200),
|
316 |
+
child: controller.value.isPlaying
|
317 |
+
? const SizedBox.shrink()
|
318 |
+
: Container(
|
319 |
+
color: Colors.black26,
|
320 |
+
child: const Center(
|
321 |
+
child: Icon(
|
322 |
+
Icons.play_arrow,
|
323 |
+
color: Colors.white,
|
324 |
+
size: 100.0,
|
325 |
+
semanticLabel: 'Play',
|
326 |
+
),
|
327 |
+
),
|
328 |
+
),
|
329 |
+
),
|
330 |
+
GestureDetector(
|
331 |
+
onTap: () {
|
332 |
+
controller.value.isPlaying ? controller.pause() : controller.play();
|
333 |
+
},
|
334 |
+
),
|
335 |
+
Align(
|
336 |
+
alignment: Alignment.topRight,
|
337 |
+
child: PopupMenuButton<double>(
|
338 |
+
initialValue: controller.value.playbackSpeed,
|
339 |
+
tooltip: 'Playback speed',
|
340 |
+
onSelected: (double speed) {
|
341 |
+
controller.setPlaybackSpeed(speed);
|
342 |
+
},
|
343 |
+
itemBuilder: (BuildContext context) {
|
344 |
+
return <PopupMenuItem<double>>[
|
345 |
+
for (final double speed in _examplePlaybackRates)
|
346 |
+
PopupMenuItem<double>(
|
347 |
+
value: speed,
|
348 |
+
child: Text('${speed}x'),
|
349 |
+
)
|
350 |
+
];
|
351 |
+
},
|
352 |
+
child: Padding(
|
353 |
+
padding: const EdgeInsets.symmetric(
|
354 |
+
// Using less vertical padding as the text is also longer
|
355 |
+
// horizontally, so it feels like it would need more spacing
|
356 |
+
// horizontally (matching the aspect ratio of the video).
|
357 |
+
vertical: 12,
|
358 |
+
horizontal: 16,
|
359 |
+
),
|
360 |
+
child: Text('${controller.value.playbackSpeed}x'),
|
361 |
+
),
|
362 |
+
),
|
363 |
+
),
|
364 |
+
],
|
365 |
+
);
|
366 |
+
}
|
367 |
+
}
|
368 |
+
|
369 |
+
// #enddocregion basic-example
|
docs/for-bots/flutter-videos/fvp_usage_example__multi_textures.dart
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import 'package:flutter/material.dart';
|
2 |
+
import 'package:fvp/mdk.dart';
|
3 |
+
|
4 |
+
void main(List<String> args) async {
|
5 |
+
runApp(const SinglePlayerMultipleVideoWidget());
|
6 |
+
}
|
7 |
+
class SinglePlayerMultipleVideoWidget extends StatefulWidget {
|
8 |
+
const SinglePlayerMultipleVideoWidget({Key? key}) : super(key: key);
|
9 |
+
|
10 |
+
@override
|
11 |
+
State<SinglePlayerMultipleVideoWidget> createState() =>
|
12 |
+
_SinglePlayerMultipleVideoWidgetState();
|
13 |
+
}
|
14 |
+
|
15 |
+
class _SinglePlayerMultipleVideoWidgetState
|
16 |
+
extends State<SinglePlayerMultipleVideoWidget> {
|
17 |
+
late final player = Player();
|
18 |
+
|
19 |
+
@override
|
20 |
+
void initState() {
|
21 |
+
super.initState();
|
22 |
+
|
23 |
+
player.media = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
24 |
+
player.loop = -1;
|
25 |
+
player.state = PlaybackState.playing;
|
26 |
+
player.updateTexture();
|
27 |
+
}
|
28 |
+
|
29 |
+
@override
|
30 |
+
void dispose() {
|
31 |
+
player.dispose();
|
32 |
+
super.dispose();
|
33 |
+
}
|
34 |
+
|
35 |
+
@override
|
36 |
+
Widget build(BuildContext context) {
|
37 |
+
return MaterialApp(
|
38 |
+
title: 'package:fvp',
|
39 |
+
home: Scaffold(
|
40 |
+
body: Center(
|
41 |
+
child: Row(
|
42 |
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
43 |
+
children: [
|
44 |
+
Expanded(
|
45 |
+
flex: 3,
|
46 |
+
child: Column(
|
47 |
+
mainAxisSize: MainAxisSize.min,
|
48 |
+
children: [
|
49 |
+
Expanded(
|
50 |
+
child: Card(
|
51 |
+
elevation: 8.0,
|
52 |
+
color: Colors.black,
|
53 |
+
clipBehavior: Clip.antiAlias,
|
54 |
+
margin: const EdgeInsets.all(32.0),
|
55 |
+
child: Column(
|
56 |
+
mainAxisSize: MainAxisSize.max,
|
57 |
+
children: [
|
58 |
+
Expanded(
|
59 |
+
child: Row(
|
60 |
+
children: [
|
61 |
+
Expanded(
|
62 |
+
child: ValueListenableBuilder<int?>(
|
63 |
+
valueListenable: player.textureId,
|
64 |
+
builder: (context, id, _) => id == null
|
65 |
+
? const SizedBox.shrink()
|
66 |
+
: Texture(textureId: id),
|
67 |
+
),
|
68 |
+
),
|
69 |
+
Expanded(
|
70 |
+
child: ValueListenableBuilder<int?>(
|
71 |
+
valueListenable: player.textureId,
|
72 |
+
builder: (context, id, _) => id == null
|
73 |
+
? const SizedBox.shrink()
|
74 |
+
: Texture(textureId: id),
|
75 |
+
),
|
76 |
+
),
|
77 |
+
],
|
78 |
+
),
|
79 |
+
),
|
80 |
+
Expanded(
|
81 |
+
child: Row(
|
82 |
+
children: [
|
83 |
+
Expanded(
|
84 |
+
child: ValueListenableBuilder<int?>(
|
85 |
+
valueListenable: player.textureId,
|
86 |
+
builder: (context, id, _) => id == null
|
87 |
+
? const SizedBox.shrink()
|
88 |
+
: Texture(textureId: id),
|
89 |
+
),
|
90 |
+
),
|
91 |
+
Expanded(
|
92 |
+
child: ValueListenableBuilder<int?>(
|
93 |
+
valueListenable: player.textureId,
|
94 |
+
builder: (context, id, _) => id == null
|
95 |
+
? const SizedBox.shrink()
|
96 |
+
: Texture(textureId: id),
|
97 |
+
),
|
98 |
+
),
|
99 |
+
],
|
100 |
+
),
|
101 |
+
),
|
102 |
+
],
|
103 |
+
),
|
104 |
+
),
|
105 |
+
),
|
106 |
+
const SizedBox(height: 32.0),
|
107 |
+
],
|
108 |
+
),
|
109 |
+
),
|
110 |
+
],
|
111 |
+
),
|
112 |
+
),
|
113 |
+
),
|
114 |
+
);
|
115 |
+
}
|
116 |
+
}
|
docs/for-bots/flutter-videos/video_player.md
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Video Player plugin for Flutter [#](#video-player-plugin-for-flutter)
|
2 |
+
=====================================================================
|
3 |
+
|
4 |
+
[](https://pub.dev/packages/video_player)
|
5 |
+
|
6 |
+
A Flutter plugin for iOS, Android and Web for playing back video on a Widget surface.
|
7 |
+
|
8 |
+
Android
|
9 |
+
|
10 |
+
iOS
|
11 |
+
|
12 |
+
macOS
|
13 |
+
|
14 |
+
Web
|
15 |
+
|
16 |
+
**Support**
|
17 |
+
|
18 |
+
SDK 16+
|
19 |
+
|
20 |
+
12.0+
|
21 |
+
|
22 |
+
10.14+
|
23 |
+
|
24 |
+
Any\*
|
25 |
+
|
26 |
+

|
27 |
+
|
28 |
+
Setup [#](#setup)
|
29 |
+
-----------------
|
30 |
+
|
31 |
+
### iOS [#](#ios)
|
32 |
+
|
33 |
+
If you need to access videos using `http` (rather than `https`) URLs, you will need to add the appropriate `NSAppTransportSecurity` permissions to your app's _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`. See [Apple's documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity) to determine the right combination of entries for your use case and supported iOS versions.
|
34 |
+
|
35 |
+
### Android [#](#android)
|
36 |
+
|
37 |
+
If you are using network-based videos, ensure that the following permission is present in your Android Manifest file, located in `<project root>/android/app/src/main/AndroidManifest.xml`:
|
38 |
+
|
39 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
40 |
+
|
41 |
+
|
42 |
+
copied to clipboard
|
43 |
+
|
44 |
+
### macOS [#](#macos)
|
45 |
+
|
46 |
+
If you are using network-based videos, you will need to [add the `com.apple.security.network.client` entitlement](https://flutter.dev/to/macos-entitlements)
|
47 |
+
|
48 |
+
### Web [#](#web)
|
49 |
+
|
50 |
+
> The Web platform does **not** support `dart:io`, so avoid using the `VideoPlayerController.file` constructor for the plugin. Using the constructor attempts to create a `VideoPlayerController.file` that will throw an `UnimplementedError`.
|
51 |
+
|
52 |
+
\* Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video\_player\_web](https://pub.dev/packages/video_player_web) for more web-specific information.
|
53 |
+
|
54 |
+
The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option in web it will be silently ignored.
|
55 |
+
|
56 |
+
Supported Formats [#](#supported-formats)
|
57 |
+
-----------------------------------------
|
58 |
+
|
59 |
+
* On iOS and macOS, the backing player is [AVPlayer](https://developer.apple.com/documentation/avfoundation/avplayer). The supported formats vary depending on the version of iOS, [AVURLAsset](https://developer.apple.com/documentation/avfoundation/avurlasset) class has [audiovisualTypes](https://developer.apple.com/documentation/avfoundation/avurlasset/1386800-audiovisualtypes?language=objc) that you can query for supported av formats.
|
60 |
+
* On Android, the backing player is [ExoPlayer](https://google.github.io/ExoPlayer/), please refer [here](https://google.github.io/ExoPlayer/supported-formats.html) for list of supported formats.
|
61 |
+
* On Web, available formats depend on your users' browsers (vendor and version). Check [package:video\_player\_web](https://pub.dev/packages/video_player_web) for more specific information.
|
62 |
+
|
63 |
+
Example [#](#example)
|
64 |
+
---------------------
|
65 |
+
|
66 |
+
import 'package:flutter/material.dart';
|
67 |
+
import 'package:video_player/video_player.dart';
|
68 |
+
|
69 |
+
void main() => runApp(const VideoApp());
|
70 |
+
|
71 |
+
/// Stateful widget to fetch and then display video content.
|
72 |
+
class VideoApp extends StatefulWidget {
|
73 |
+
const VideoApp({super.key});
|
74 |
+
|
75 |
+
@override
|
76 |
+
_VideoAppState createState() => _VideoAppState();
|
77 |
+
}
|
78 |
+
|
79 |
+
class _VideoAppState extends State<VideoApp> {
|
80 |
+
late VideoPlayerController _controller;
|
81 |
+
|
82 |
+
@override
|
83 |
+
void initState() {
|
84 |
+
super.initState();
|
85 |
+
_controller = VideoPlayerController.networkUrl(Uri.parse(
|
86 |
+
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'))
|
87 |
+
..initialize().then((_) {
|
88 |
+
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
|
89 |
+
setState(() {});
|
90 |
+
});
|
91 |
+
}
|
92 |
+
|
93 |
+
@override
|
94 |
+
Widget build(BuildContext context) {
|
95 |
+
return MaterialApp(
|
96 |
+
title: 'Video Demo',
|
97 |
+
home: Scaffold(
|
98 |
+
body: Center(
|
99 |
+
child: _controller.value.isInitialized
|
100 |
+
? AspectRatio(
|
101 |
+
aspectRatio: _controller.value.aspectRatio,
|
102 |
+
child: VideoPlayer(_controller),
|
103 |
+
)
|
104 |
+
: Container(),
|
105 |
+
),
|
106 |
+
floatingActionButton: FloatingActionButton(
|
107 |
+
onPressed: () {
|
108 |
+
setState(() {
|
109 |
+
_controller.value.isPlaying
|
110 |
+
? _controller.pause()
|
111 |
+
: _controller.play();
|
112 |
+
});
|
113 |
+
},
|
114 |
+
child: Icon(
|
115 |
+
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
|
116 |
+
),
|
117 |
+
),
|
118 |
+
),
|
119 |
+
);
|
120 |
+
}
|
121 |
+
|
122 |
+
@override
|
123 |
+
void dispose() {
|
124 |
+
_controller.dispose();
|
125 |
+
super.dispose();
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
|
130 |
+
copied to clipboard
|
131 |
+
|
132 |
+
Usage [#](#usage)
|
133 |
+
-----------------
|
134 |
+
|
135 |
+
The following section contains usage information that goes beyond what is included in the documentation in order to give a more elaborate overview of the API.
|
136 |
+
|
137 |
+
This is not complete as of now. You can contribute to this section by [opening a pull request](https://github.com/flutter/packages/pulls).
|
138 |
+
|
139 |
+
### Playback speed [#](#playback-speed)
|
140 |
+
|
141 |
+
You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating the rate of playback for your video. For example, when given a value of `2.0`, your video will play at 2x the regular playback speed and so on.
|
142 |
+
|
143 |
+
To learn about playback speed limitations, see the [`setPlaybackSpeed` method documentation](https://pub.dev/documentation/video_player/latest/video_player/VideoPlayerController/setPlaybackSpeed.html).
|
144 |
+
|
145 |
+
Furthermore, see the example app for an example playback speed implementation.
|
docs/for-bots/huggingface/hf-api.md
ADDED
The diff for this file is too large to render.
See raw diff
|
|
ios/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/dgph
|
2 |
+
*.mode1v3
|
3 |
+
*.mode2v3
|
4 |
+
*.moved-aside
|
5 |
+
*.pbxuser
|
6 |
+
*.perspectivev3
|
7 |
+
**/*sync/
|
8 |
+
.sconsign.dblite
|
9 |
+
.tags*
|
10 |
+
**/.vagrant/
|
11 |
+
**/DerivedData/
|
12 |
+
Icon?
|
13 |
+
**/Pods/
|
14 |
+
**/.symlinks/
|
15 |
+
profile
|
16 |
+
xcuserdata
|
17 |
+
**/.generated/
|
18 |
+
Flutter/App.framework
|
19 |
+
Flutter/Flutter.framework
|
20 |
+
Flutter/Flutter.podspec
|
21 |
+
Flutter/Generated.xcconfig
|
22 |
+
Flutter/ephemeral/
|
23 |
+
Flutter/app.flx
|
24 |
+
Flutter/app.zip
|
25 |
+
Flutter/flutter_assets/
|
26 |
+
Flutter/flutter_export_environment.sh
|
27 |
+
ServiceDefinitions.json
|
28 |
+
Runner/GeneratedPluginRegistrant.*
|
29 |
+
|
30 |
+
# Exceptions to above rules.
|
31 |
+
!default.mode1v3
|
32 |
+
!default.mode2v3
|
33 |
+
!default.pbxuser
|
34 |
+
!default.perspectivev3
|
ios/Flutter/AppFrameworkInfo.plist
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3 |
+
<plist version="1.0">
|
4 |
+
<dict>
|
5 |
+
<key>CFBundleDevelopmentRegion</key>
|
6 |
+
<string>en</string>
|
7 |
+
<key>CFBundleExecutable</key>
|
8 |
+
<string>App</string>
|
9 |
+
<key>CFBundleIdentifier</key>
|
10 |
+
<string>io.flutter.flutter.app</string>
|
11 |
+
<key>CFBundleInfoDictionaryVersion</key>
|
12 |
+
<string>6.0</string>
|
13 |
+
<key>CFBundleName</key>
|
14 |
+
<string>App</string>
|
15 |
+
<key>CFBundlePackageType</key>
|
16 |
+
<string>FMWK</string>
|
17 |
+
<key>CFBundleShortVersionString</key>
|
18 |
+
<string>1.0</string>
|
19 |
+
<key>CFBundleSignature</key>
|
20 |
+
<string>????</string>
|
21 |
+
<key>CFBundleVersion</key>
|
22 |
+
<string>1.0</string>
|
23 |
+
<key>MinimumOSVersion</key>
|
24 |
+
<string>12.0</string>
|
25 |
+
</dict>
|
26 |
+
</plist>
|
ios/Flutter/Debug.xcconfig
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
2 |
+
#include "Generated.xcconfig"
|
ios/Flutter/Release.xcconfig
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
2 |
+
#include "Generated.xcconfig"
|
ios/Podfile
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Uncomment this line to define a global platform for your project
|
2 |
+
# platform :ios, '12.0'
|
3 |
+
|
4 |
+
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
5 |
+
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
6 |
+
|
7 |
+
project 'Runner', {
|
8 |
+
'Debug' => :debug,
|
9 |
+
'Profile' => :release,
|
10 |
+
'Release' => :release,
|
11 |
+
}
|
12 |
+
|
13 |
+
def flutter_root
|
14 |
+
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
15 |
+
unless File.exist?(generated_xcode_build_settings_path)
|
16 |
+
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
17 |
+
end
|
18 |
+
|
19 |
+
File.foreach(generated_xcode_build_settings_path) do |line|
|
20 |
+
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
21 |
+
return matches[1].strip if matches
|
22 |
+
end
|
23 |
+
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
24 |
+
end
|
25 |
+
|
26 |
+
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
27 |
+
|
28 |
+
flutter_ios_podfile_setup
|
29 |
+
|
30 |
+
target 'Runner' do
|
31 |
+
use_frameworks!
|
32 |
+
use_modular_headers!
|
33 |
+
|
34 |
+
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
35 |
+
target 'RunnerTests' do
|
36 |
+
inherit! :search_paths
|
37 |
+
end
|
38 |
+
end
|
39 |
+
|
40 |
+
post_install do |installer|
|
41 |
+
installer.pods_project.targets.each do |target|
|
42 |
+
flutter_additional_ios_build_settings(target)
|
43 |
+
end
|
44 |
+
end
|
ios/Runner.xcodeproj/project.pbxproj
ADDED
@@ -0,0 +1,616 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// !$*UTF8*$!
|
2 |
+
{
|
3 |
+
archiveVersion = 1;
|
4 |
+
classes = {
|
5 |
+
};
|
6 |
+
objectVersion = 54;
|
7 |
+
objects = {
|
8 |
+
|
9 |
+
/* Begin PBXBuildFile section */
|
10 |
+
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
11 |
+
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
12 |
+
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
13 |
+
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
14 |
+
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
15 |
+
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
16 |
+
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
17 |
+
/* End PBXBuildFile section */
|
18 |
+
|
19 |
+
/* Begin PBXContainerItemProxy section */
|
20 |
+
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
21 |
+
isa = PBXContainerItemProxy;
|
22 |
+
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
23 |
+
proxyType = 1;
|
24 |
+
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
25 |
+
remoteInfo = Runner;
|
26 |
+
};
|
27 |
+
/* End PBXContainerItemProxy section */
|
28 |
+
|
29 |
+
/* Begin PBXCopyFilesBuildPhase section */
|
30 |
+
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
31 |
+
isa = PBXCopyFilesBuildPhase;
|
32 |
+
buildActionMask = 2147483647;
|
33 |
+
dstPath = "";
|
34 |
+
dstSubfolderSpec = 10;
|
35 |
+
files = (
|
36 |
+
);
|
37 |
+
name = "Embed Frameworks";
|
38 |
+
runOnlyForDeploymentPostprocessing = 0;
|
39 |
+
};
|
40 |
+
/* End PBXCopyFilesBuildPhase section */
|
41 |
+
|
42 |
+
/* Begin PBXFileReference section */
|
43 |
+
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
44 |
+
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
45 |
+
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
46 |
+
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
47 |
+
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
48 |
+
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
49 |
+
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
50 |
+
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
51 |
+
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
52 |
+
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
53 |
+
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
54 |
+
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
55 |
+
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
56 |
+
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
57 |
+
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
58 |
+
/* End PBXFileReference section */
|
59 |
+
|
60 |
+
/* Begin PBXFrameworksBuildPhase section */
|
61 |
+
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
62 |
+
isa = PBXFrameworksBuildPhase;
|
63 |
+
buildActionMask = 2147483647;
|
64 |
+
files = (
|
65 |
+
);
|
66 |
+
runOnlyForDeploymentPostprocessing = 0;
|
67 |
+
};
|
68 |
+
/* End PBXFrameworksBuildPhase section */
|
69 |
+
|
70 |
+
/* Begin PBXGroup section */
|
71 |
+
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
72 |
+
isa = PBXGroup;
|
73 |
+
children = (
|
74 |
+
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
75 |
+
);
|
76 |
+
path = RunnerTests;
|
77 |
+
sourceTree = "<group>";
|
78 |
+
};
|
79 |
+
9740EEB11CF90186004384FC /* Flutter */ = {
|
80 |
+
isa = PBXGroup;
|
81 |
+
children = (
|
82 |
+
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
83 |
+
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
84 |
+
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
85 |
+
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
86 |
+
);
|
87 |
+
name = Flutter;
|
88 |
+
sourceTree = "<group>";
|
89 |
+
};
|
90 |
+
97C146E51CF9000F007C117D = {
|
91 |
+
isa = PBXGroup;
|
92 |
+
children = (
|
93 |
+
9740EEB11CF90186004384FC /* Flutter */,
|
94 |
+
97C146F01CF9000F007C117D /* Runner */,
|
95 |
+
97C146EF1CF9000F007C117D /* Products */,
|
96 |
+
331C8082294A63A400263BE5 /* RunnerTests */,
|
97 |
+
);
|
98 |
+
sourceTree = "<group>";
|
99 |
+
};
|
100 |
+
97C146EF1CF9000F007C117D /* Products */ = {
|
101 |
+
isa = PBXGroup;
|
102 |
+
children = (
|
103 |
+
97C146EE1CF9000F007C117D /* Runner.app */,
|
104 |
+
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
105 |
+
);
|
106 |
+
name = Products;
|
107 |
+
sourceTree = "<group>";
|
108 |
+
};
|
109 |
+
97C146F01CF9000F007C117D /* Runner */ = {
|
110 |
+
isa = PBXGroup;
|
111 |
+
children = (
|
112 |
+
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
113 |
+
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
114 |
+
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
115 |
+
97C147021CF9000F007C117D /* Info.plist */,
|
116 |
+
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
117 |
+
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
118 |
+
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
119 |
+
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
120 |
+
);
|
121 |
+
path = Runner;
|
122 |
+
sourceTree = "<group>";
|
123 |
+
};
|
124 |
+
/* End PBXGroup section */
|
125 |
+
|
126 |
+
/* Begin PBXNativeTarget section */
|
127 |
+
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
128 |
+
isa = PBXNativeTarget;
|
129 |
+
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
130 |
+
buildPhases = (
|
131 |
+
331C807D294A63A400263BE5 /* Sources */,
|
132 |
+
331C807F294A63A400263BE5 /* Resources */,
|
133 |
+
);
|
134 |
+
buildRules = (
|
135 |
+
);
|
136 |
+
dependencies = (
|
137 |
+
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
138 |
+
);
|
139 |
+
name = RunnerTests;
|
140 |
+
productName = RunnerTests;
|
141 |
+
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
142 |
+
productType = "com.apple.product-type.bundle.unit-test";
|
143 |
+
};
|
144 |
+
97C146ED1CF9000F007C117D /* Runner */ = {
|
145 |
+
isa = PBXNativeTarget;
|
146 |
+
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
147 |
+
buildPhases = (
|
148 |
+
9740EEB61CF901F6004384FC /* Run Script */,
|
149 |
+
97C146EA1CF9000F007C117D /* Sources */,
|
150 |
+
97C146EB1CF9000F007C117D /* Frameworks */,
|
151 |
+
97C146EC1CF9000F007C117D /* Resources */,
|
152 |
+
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
153 |
+
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
154 |
+
);
|
155 |
+
buildRules = (
|
156 |
+
);
|
157 |
+
dependencies = (
|
158 |
+
);
|
159 |
+
name = Runner;
|
160 |
+
productName = Runner;
|
161 |
+
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
162 |
+
productType = "com.apple.product-type.application";
|
163 |
+
};
|
164 |
+
/* End PBXNativeTarget section */
|
165 |
+
|
166 |
+
/* Begin PBXProject section */
|
167 |
+
97C146E61CF9000F007C117D /* Project object */ = {
|
168 |
+
isa = PBXProject;
|
169 |
+
attributes = {
|
170 |
+
BuildIndependentTargetsInParallel = YES;
|
171 |
+
LastUpgradeCheck = 1510;
|
172 |
+
ORGANIZATIONNAME = "";
|
173 |
+
TargetAttributes = {
|
174 |
+
331C8080294A63A400263BE5 = {
|
175 |
+
CreatedOnToolsVersion = 14.0;
|
176 |
+
TestTargetID = 97C146ED1CF9000F007C117D;
|
177 |
+
};
|
178 |
+
97C146ED1CF9000F007C117D = {
|
179 |
+
CreatedOnToolsVersion = 7.3.1;
|
180 |
+
LastSwiftMigration = 1100;
|
181 |
+
};
|
182 |
+
};
|
183 |
+
};
|
184 |
+
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
185 |
+
compatibilityVersion = "Xcode 9.3";
|
186 |
+
developmentRegion = en;
|
187 |
+
hasScannedForEncodings = 0;
|
188 |
+
knownRegions = (
|
189 |
+
en,
|
190 |
+
Base,
|
191 |
+
);
|
192 |
+
mainGroup = 97C146E51CF9000F007C117D;
|
193 |
+
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
194 |
+
projectDirPath = "";
|
195 |
+
projectRoot = "";
|
196 |
+
targets = (
|
197 |
+
97C146ED1CF9000F007C117D /* Runner */,
|
198 |
+
331C8080294A63A400263BE5 /* RunnerTests */,
|
199 |
+
);
|
200 |
+
};
|
201 |
+
/* End PBXProject section */
|
202 |
+
|
203 |
+
/* Begin PBXResourcesBuildPhase section */
|
204 |
+
331C807F294A63A400263BE5 /* Resources */ = {
|
205 |
+
isa = PBXResourcesBuildPhase;
|
206 |
+
buildActionMask = 2147483647;
|
207 |
+
files = (
|
208 |
+
);
|
209 |
+
runOnlyForDeploymentPostprocessing = 0;
|
210 |
+
};
|
211 |
+
97C146EC1CF9000F007C117D /* Resources */ = {
|
212 |
+
isa = PBXResourcesBuildPhase;
|
213 |
+
buildActionMask = 2147483647;
|
214 |
+
files = (
|
215 |
+
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
216 |
+
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
217 |
+
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
218 |
+
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
219 |
+
);
|
220 |
+
runOnlyForDeploymentPostprocessing = 0;
|
221 |
+
};
|
222 |
+
/* End PBXResourcesBuildPhase section */
|
223 |
+
|
224 |
+
/* Begin PBXShellScriptBuildPhase section */
|
225 |
+
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
226 |
+
isa = PBXShellScriptBuildPhase;
|
227 |
+
alwaysOutOfDate = 1;
|
228 |
+
buildActionMask = 2147483647;
|
229 |
+
files = (
|
230 |
+
);
|
231 |
+
inputPaths = (
|
232 |
+
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
233 |
+
);
|
234 |
+
name = "Thin Binary";
|
235 |
+
outputPaths = (
|
236 |
+
);
|
237 |
+
runOnlyForDeploymentPostprocessing = 0;
|
238 |
+
shellPath = /bin/sh;
|
239 |
+
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
240 |
+
};
|
241 |
+
9740EEB61CF901F6004384FC /* Run Script */ = {
|
242 |
+
isa = PBXShellScriptBuildPhase;
|
243 |
+
alwaysOutOfDate = 1;
|
244 |
+
buildActionMask = 2147483647;
|
245 |
+
files = (
|
246 |
+
);
|
247 |
+
inputPaths = (
|
248 |
+
);
|
249 |
+
name = "Run Script";
|
250 |
+
outputPaths = (
|
251 |
+
);
|
252 |
+
runOnlyForDeploymentPostprocessing = 0;
|
253 |
+
shellPath = /bin/sh;
|
254 |
+
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
255 |
+
};
|
256 |
+
/* End PBXShellScriptBuildPhase section */
|
257 |
+
|
258 |
+
/* Begin PBXSourcesBuildPhase section */
|
259 |
+
331C807D294A63A400263BE5 /* Sources */ = {
|
260 |
+
isa = PBXSourcesBuildPhase;
|
261 |
+
buildActionMask = 2147483647;
|
262 |
+
files = (
|
263 |
+
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
264 |
+
);
|
265 |
+
runOnlyForDeploymentPostprocessing = 0;
|
266 |
+
};
|
267 |
+
97C146EA1CF9000F007C117D /* Sources */ = {
|
268 |
+
isa = PBXSourcesBuildPhase;
|
269 |
+
buildActionMask = 2147483647;
|
270 |
+
files = (
|
271 |
+
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
272 |
+
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
273 |
+
);
|
274 |
+
runOnlyForDeploymentPostprocessing = 0;
|
275 |
+
};
|
276 |
+
/* End PBXSourcesBuildPhase section */
|
277 |
+
|
278 |
+
/* Begin PBXTargetDependency section */
|
279 |
+
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
280 |
+
isa = PBXTargetDependency;
|
281 |
+
target = 97C146ED1CF9000F007C117D /* Runner */;
|
282 |
+
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
283 |
+
};
|
284 |
+
/* End PBXTargetDependency section */
|
285 |
+
|
286 |
+
/* Begin PBXVariantGroup section */
|
287 |
+
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
288 |
+
isa = PBXVariantGroup;
|
289 |
+
children = (
|
290 |
+
97C146FB1CF9000F007C117D /* Base */,
|
291 |
+
);
|
292 |
+
name = Main.storyboard;
|
293 |
+
sourceTree = "<group>";
|
294 |
+
};
|
295 |
+
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
296 |
+
isa = PBXVariantGroup;
|
297 |
+
children = (
|
298 |
+
97C147001CF9000F007C117D /* Base */,
|
299 |
+
);
|
300 |
+
name = LaunchScreen.storyboard;
|
301 |
+
sourceTree = "<group>";
|
302 |
+
};
|
303 |
+
/* End PBXVariantGroup section */
|
304 |
+
|
305 |
+
/* Begin XCBuildConfiguration section */
|
306 |
+
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
307 |
+
isa = XCBuildConfiguration;
|
308 |
+
buildSettings = {
|
309 |
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
310 |
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
311 |
+
CLANG_ANALYZER_NONNULL = YES;
|
312 |
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
313 |
+
CLANG_CXX_LIBRARY = "libc++";
|
314 |
+
CLANG_ENABLE_MODULES = YES;
|
315 |
+
CLANG_ENABLE_OBJC_ARC = YES;
|
316 |
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
317 |
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
318 |
+
CLANG_WARN_COMMA = YES;
|
319 |
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
320 |
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
321 |
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
322 |
+
CLANG_WARN_EMPTY_BODY = YES;
|
323 |
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
324 |
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
325 |
+
CLANG_WARN_INT_CONVERSION = YES;
|
326 |
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
327 |
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
328 |
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
329 |
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
330 |
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
331 |
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
332 |
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
333 |
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
334 |
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
335 |
+
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
336 |
+
COPY_PHASE_STRIP = NO;
|
337 |
+
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
338 |
+
ENABLE_NS_ASSERTIONS = NO;
|
339 |
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
340 |
+
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
341 |
+
GCC_C_LANGUAGE_STANDARD = gnu99;
|
342 |
+
GCC_NO_COMMON_BLOCKS = YES;
|
343 |
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
344 |
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
345 |
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
346 |
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
347 |
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
348 |
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
349 |
+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
350 |
+
MTL_ENABLE_DEBUG_INFO = NO;
|
351 |
+
SDKROOT = iphoneos;
|
352 |
+
SUPPORTED_PLATFORMS = iphoneos;
|
353 |
+
TARGETED_DEVICE_FAMILY = "1,2";
|
354 |
+
VALIDATE_PRODUCT = YES;
|
355 |
+
};
|
356 |
+
name = Profile;
|
357 |
+
};
|
358 |
+
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
359 |
+
isa = XCBuildConfiguration;
|
360 |
+
DefaultConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
361 |
+
buildSettings = {
|
362 |
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
363 |
+
CLANG_ENABLE_MODULES = YES;
|
364 |
+
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
365 |
+
ENABLE_BITCODE = NO;
|
366 |
+
INFOPLIST_FILE = Runner/Info.plist;
|
367 |
+
LD_RUNPATH_SEARCH_PATHS = (
|
368 |
+
"$(inherited)",
|
369 |
+
"@executable_path/Frameworks",
|
370 |
+
);
|
371 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2;
|
372 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
373 |
+
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
374 |
+
SWIFT_VERSION = 5.0;
|
375 |
+
VERSIONING_SYSTEM = "apple-generic";
|
376 |
+
};
|
377 |
+
name = Profile;
|
378 |
+
};
|
379 |
+
331C8088294A63A400263BE5 /* Debug */ = {
|
380 |
+
isa = XCBuildConfiguration;
|
381 |
+
buildSettings = {
|
382 |
+
BUNDLE_LOADER = "$(TEST_HOST)";
|
383 |
+
CODE_SIGN_STYLE = Automatic;
|
384 |
+
CURRENT_PROJECT_VERSION = 1;
|
385 |
+
GENERATE_INFOPLIST_FILE = YES;
|
386 |
+
MARKETING_VERSION = 1.0;
|
387 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2.RunnerTests;
|
388 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
389 |
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
390 |
+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
391 |
+
SWIFT_VERSION = 5.0;
|
392 |
+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
393 |
+
};
|
394 |
+
name = Debug;
|
395 |
+
};
|
396 |
+
331C8089294A63A400263BE5 /* Release */ = {
|
397 |
+
isa = XCBuildConfiguration;
|
398 |
+
buildSettings = {
|
399 |
+
BUNDLE_LOADER = "$(TEST_HOST)";
|
400 |
+
CODE_SIGN_STYLE = Automatic;
|
401 |
+
CURRENT_PROJECT_VERSION = 1;
|
402 |
+
GENERATE_INFOPLIST_FILE = YES;
|
403 |
+
MARKETING_VERSION = 1.0;
|
404 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2.RunnerTests;
|
405 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
406 |
+
SWIFT_VERSION = 5.0;
|
407 |
+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
408 |
+
};
|
409 |
+
name = Release;
|
410 |
+
};
|
411 |
+
331C808A294A63A400263BE5 /* Profile */ = {
|
412 |
+
isa = XCBuildConfiguration;
|
413 |
+
buildSettings = {
|
414 |
+
BUNDLE_LOADER = "$(TEST_HOST)";
|
415 |
+
CODE_SIGN_STYLE = Automatic;
|
416 |
+
CURRENT_PROJECT_VERSION = 1;
|
417 |
+
GENERATE_INFOPLIST_FILE = YES;
|
418 |
+
MARKETING_VERSION = 1.0;
|
419 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2.RunnerTests;
|
420 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
421 |
+
SWIFT_VERSION = 5.0;
|
422 |
+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
423 |
+
};
|
424 |
+
name = Profile;
|
425 |
+
};
|
426 |
+
97C147031CF9000F007C117D /* Debug */ = {
|
427 |
+
isa = XCBuildConfiguration;
|
428 |
+
buildSettings = {
|
429 |
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
430 |
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
431 |
+
CLANG_ANALYZER_NONNULL = YES;
|
432 |
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
433 |
+
CLANG_CXX_LIBRARY = "libc++";
|
434 |
+
CLANG_ENABLE_MODULES = YES;
|
435 |
+
CLANG_ENABLE_OBJC_ARC = YES;
|
436 |
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
437 |
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
438 |
+
CLANG_WARN_COMMA = YES;
|
439 |
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
440 |
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
441 |
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
442 |
+
CLANG_WARN_EMPTY_BODY = YES;
|
443 |
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
444 |
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
445 |
+
CLANG_WARN_INT_CONVERSION = YES;
|
446 |
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
447 |
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
448 |
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
449 |
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
450 |
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
451 |
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
452 |
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
453 |
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
454 |
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
455 |
+
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
456 |
+
COPY_PHASE_STRIP = NO;
|
457 |
+
DEBUG_INFORMATION_FORMAT = dwarf;
|
458 |
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
459 |
+
ENABLE_TESTABILITY = YES;
|
460 |
+
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
461 |
+
GCC_C_LANGUAGE_STANDARD = gnu99;
|
462 |
+
GCC_DYNAMIC_NO_PIC = NO;
|
463 |
+
GCC_NO_COMMON_BLOCKS = YES;
|
464 |
+
GCC_OPTIMIZATION_LEVEL = 0;
|
465 |
+
GCC_PREPROCESSOR_DEFINITIONS = (
|
466 |
+
"DEBUG=1",
|
467 |
+
"$(inherited)",
|
468 |
+
);
|
469 |
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
470 |
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
471 |
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
472 |
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
473 |
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
474 |
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
475 |
+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
476 |
+
MTL_ENABLE_DEBUG_INFO = YES;
|
477 |
+
ONLY_ACTIVE_ARCH = YES;
|
478 |
+
SDKROOT = iphoneos;
|
479 |
+
TARGETED_DEVICE_FAMILY = "1,2";
|
480 |
+
};
|
481 |
+
name = Debug;
|
482 |
+
};
|
483 |
+
97C147041CF9000F007C117D /* Release */ = {
|
484 |
+
isa = XCBuildConfiguration;
|
485 |
+
buildSettings = {
|
486 |
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
487 |
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
488 |
+
CLANG_ANALYZER_NONNULL = YES;
|
489 |
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
490 |
+
CLANG_CXX_LIBRARY = "libc++";
|
491 |
+
CLANG_ENABLE_MODULES = YES;
|
492 |
+
CLANG_ENABLE_OBJC_ARC = YES;
|
493 |
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
494 |
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
495 |
+
CLANG_WARN_COMMA = YES;
|
496 |
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
497 |
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
498 |
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
499 |
+
CLANG_WARN_EMPTY_BODY = YES;
|
500 |
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
501 |
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
502 |
+
CLANG_WARN_INT_CONVERSION = YES;
|
503 |
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
504 |
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
505 |
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
506 |
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
507 |
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
508 |
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
509 |
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
510 |
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
511 |
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
512 |
+
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
513 |
+
COPY_PHASE_STRIP = NO;
|
514 |
+
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
515 |
+
ENABLE_NS_ASSERTIONS = NO;
|
516 |
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
517 |
+
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
518 |
+
GCC_C_LANGUAGE_STANDARD = gnu99;
|
519 |
+
GCC_NO_COMMON_BLOCKS = YES;
|
520 |
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
521 |
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
522 |
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
523 |
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
524 |
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
525 |
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
526 |
+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
527 |
+
MTL_ENABLE_DEBUG_INFO = NO;
|
528 |
+
SDKROOT = iphoneos;
|
529 |
+
SUPPORTED_PLATFORMS = iphoneos;
|
530 |
+
SWIFT_COMPILATION_MODE = wholemodule;
|
531 |
+
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
532 |
+
TARGETED_DEVICE_FAMILY = "1,2";
|
533 |
+
VALIDATE_PRODUCT = YES;
|
534 |
+
};
|
535 |
+
name = Release;
|
536 |
+
};
|
537 |
+
97C147061CF9000F007C117D /* Debug */ = {
|
538 |
+
isa = XCBuildConfiguration;
|
539 |
+
DefaultConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
540 |
+
buildSettings = {
|
541 |
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
542 |
+
CLANG_ENABLE_MODULES = YES;
|
543 |
+
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
544 |
+
ENABLE_BITCODE = NO;
|
545 |
+
INFOPLIST_FILE = Runner/Info.plist;
|
546 |
+
LD_RUNPATH_SEARCH_PATHS = (
|
547 |
+
"$(inherited)",
|
548 |
+
"@executable_path/Frameworks",
|
549 |
+
);
|
550 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2;
|
551 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
552 |
+
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
553 |
+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
554 |
+
SWIFT_VERSION = 5.0;
|
555 |
+
VERSIONING_SYSTEM = "apple-generic";
|
556 |
+
};
|
557 |
+
name = Debug;
|
558 |
+
};
|
559 |
+
97C147071CF9000F007C117D /* Release */ = {
|
560 |
+
isa = XCBuildConfiguration;
|
561 |
+
DefaultConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
562 |
+
buildSettings = {
|
563 |
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
564 |
+
CLANG_ENABLE_MODULES = YES;
|
565 |
+
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
566 |
+
ENABLE_BITCODE = NO;
|
567 |
+
INFOPLIST_FILE = Runner/Info.plist;
|
568 |
+
LD_RUNPATH_SEARCH_PATHS = (
|
569 |
+
"$(inherited)",
|
570 |
+
"@executable_path/Frameworks",
|
571 |
+
);
|
572 |
+
PRODUCT_BUNDLE_IDENTIFIER = com.example.aitube2;
|
573 |
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
574 |
+
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
575 |
+
SWIFT_VERSION = 5.0;
|
576 |
+
VERSIONING_SYSTEM = "apple-generic";
|
577 |
+
};
|
578 |
+
name = Release;
|
579 |
+
};
|
580 |
+
/* End XCBuildConfiguration section */
|
581 |
+
|
582 |
+
/* Begin XCConfigurationList section */
|
583 |
+
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
584 |
+
isa = XCConfigurationList;
|
585 |
+
buildConfigurations = (
|
586 |
+
331C8088294A63A400263BE5 /* Debug */,
|
587 |
+
331C8089294A63A400263BE5 /* Release */,
|
588 |
+
331C808A294A63A400263BE5 /* Profile */,
|
589 |
+
);
|
590 |
+
defaultConfigurationIsVisible = 0;
|
591 |
+
defaultConfigurationName = Release;
|
592 |
+
};
|
593 |
+
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
594 |
+
isa = XCConfigurationList;
|
595 |
+
buildConfigurations = (
|
596 |
+
97C147031CF9000F007C117D /* Debug */,
|
597 |
+
97C147041CF9000F007C117D /* Release */,
|
598 |
+
249021D3217E4FDB00AE95B9 /* Profile */,
|
599 |
+
);
|
600 |
+
defaultConfigurationIsVisible = 0;
|
601 |
+
defaultConfigurationName = Release;
|
602 |
+
};
|
603 |
+
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
604 |
+
isa = XCConfigurationList;
|
605 |
+
buildConfigurations = (
|
606 |
+
97C147061CF9000F007C117D /* Debug */,
|
607 |
+
97C147071CF9000F007C117D /* Release */,
|
608 |
+
249021D4217E4FDB00AE95B9 /* Profile */,
|
609 |
+
);
|
610 |
+
defaultConfigurationIsVisible = 0;
|
611 |
+
defaultConfigurationName = Release;
|
612 |
+
};
|
613 |
+
/* End XCConfigurationList section */
|
614 |
+
};
|
615 |
+
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
616 |
+
}
|
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<Workspace
|
3 |
+
version = "1.0">
|
4 |
+
<FileRef
|
5 |
+
location = "self:">
|
6 |
+
</FileRef>
|
7 |
+
</Workspace>
|
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3 |
+
<plist version="1.0">
|
4 |
+
<dict>
|
5 |
+
<key>IDEDidComputeMac32BitWarning</key>
|
6 |
+
<true/>
|
7 |
+
</dict>
|
8 |
+
</plist>
|
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3 |
+
<plist version="1.0">
|
4 |
+
<dict>
|
5 |
+
<key>PreviewsEnabled</key>
|
6 |
+
<false/>
|
7 |
+
</dict>
|
8 |
+
</plist>
|
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<Scheme
|
3 |
+
LastUpgradeVersion = "1510"
|
4 |
+
version = "1.3">
|
5 |
+
<BuildAction
|
6 |
+
parallelizeBuildables = "YES"
|
7 |
+
buildImplicitDependencies = "YES">
|
8 |
+
<BuildActionEntries>
|
9 |
+
<BuildActionEntry
|
10 |
+
buildForTesting = "YES"
|
11 |
+
buildForRunning = "YES"
|
12 |
+
buildForProfiling = "YES"
|
13 |
+
buildForArchiving = "YES"
|
14 |
+
buildForAnalyzing = "YES">
|
15 |
+
<BuildableReference
|
16 |
+
BuildableIdentifier = "primary"
|
17 |
+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
18 |
+
BuildableName = "Runner.app"
|
19 |
+
BlueprintName = "Runner"
|
20 |
+
ReferencedContainer = "container:Runner.xcodeproj">
|
21 |
+
</BuildableReference>
|
22 |
+
</BuildActionEntry>
|
23 |
+
</BuildActionEntries>
|
24 |
+
</BuildAction>
|
25 |
+
<TestAction
|
26 |
+
buildConfiguration = "Debug"
|
27 |
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
28 |
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
29 |
+
shouldUseLaunchSchemeArgsEnv = "YES">
|
30 |
+
<MacroExpansion>
|
31 |
+
<BuildableReference
|
32 |
+
BuildableIdentifier = "primary"
|
33 |
+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
34 |
+
BuildableName = "Runner.app"
|
35 |
+
BlueprintName = "Runner"
|
36 |
+
ReferencedContainer = "container:Runner.xcodeproj">
|
37 |
+
</BuildableReference>
|
38 |
+
</MacroExpansion>
|
39 |
+
<Testables>
|
40 |
+
<TestableReference
|
41 |
+
skipped = "NO"
|
42 |
+
parallelizable = "YES">
|
43 |
+
<BuildableReference
|
44 |
+
BuildableIdentifier = "primary"
|
45 |
+
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
46 |
+
BuildableName = "RunnerTests.xctest"
|
47 |
+
BlueprintName = "RunnerTests"
|
48 |
+
ReferencedContainer = "container:Runner.xcodeproj">
|
49 |
+
</BuildableReference>
|
50 |
+
</TestableReference>
|
51 |
+
</Testables>
|
52 |
+
</TestAction>
|
53 |
+
<LaunchAction
|
54 |
+
buildConfiguration = "Debug"
|
55 |
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
56 |
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
57 |
+
launchStyle = "0"
|
58 |
+
useCustomWorkingDirectory = "NO"
|
59 |
+
ignoresPersistentStateOnLaunch = "NO"
|
60 |
+
debugDocumentVersioning = "YES"
|
61 |
+
debugServiceExtension = "internal"
|
62 |
+
allowLocationSimulation = "YES">
|
63 |
+
<BuildableProductRunnable
|
64 |
+
runnableDebuggingMode = "0">
|
65 |
+
<BuildableReference
|
66 |
+
BuildableIdentifier = "primary"
|
67 |
+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
68 |
+
BuildableName = "Runner.app"
|
69 |
+
BlueprintName = "Runner"
|
70 |
+
ReferencedContainer = "container:Runner.xcodeproj">
|
71 |
+
</BuildableReference>
|
72 |
+
</BuildableProductRunnable>
|
73 |
+
</LaunchAction>
|
74 |
+
<ProfileAction
|
75 |
+
buildConfiguration = "Profile"
|
76 |
+
shouldUseLaunchSchemeArgsEnv = "YES"
|
77 |
+
savedToolIdentifier = ""
|
78 |
+
useCustomWorkingDirectory = "NO"
|
79 |
+
debugDocumentVersioning = "YES">
|
80 |
+
<BuildableProductRunnable
|
81 |
+
runnableDebuggingMode = "0">
|
82 |
+
<BuildableReference
|
83 |
+
BuildableIdentifier = "primary"
|
84 |
+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
85 |
+
BuildableName = "Runner.app"
|
86 |
+
BlueprintName = "Runner"
|
87 |
+
ReferencedContainer = "container:Runner.xcodeproj">
|
88 |
+
</BuildableReference>
|
89 |
+
</BuildableProductRunnable>
|
90 |
+
</ProfileAction>
|
91 |
+
<AnalyzeAction
|
92 |
+
buildConfiguration = "Debug">
|
93 |
+
</AnalyzeAction>
|
94 |
+
<ArchiveAction
|
95 |
+
buildConfiguration = "Release"
|
96 |
+
revealArchiveInOrganizer = "YES">
|
97 |
+
</ArchiveAction>
|
98 |
+
</Scheme>
|
ios/Runner.xcworkspace/contents.xcworkspacedata
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<Workspace
|
3 |
+
version = "1.0">
|
4 |
+
<FileRef
|
5 |
+
location = "group:Runner.xcodeproj">
|
6 |
+
</FileRef>
|
7 |
+
</Workspace>
|
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3 |
+
<plist version="1.0">
|
4 |
+
<dict>
|
5 |
+
<key>IDEDidComputeMac32BitWarning</key>
|
6 |
+
<true/>
|
7 |
+
</dict>
|
8 |
+
</plist>
|
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3 |
+
<plist version="1.0">
|
4 |
+
<dict>
|
5 |
+
<key>PreviewsEnabled</key>
|
6 |
+
<false/>
|
7 |
+
</dict>
|
8 |
+
</plist>
|
ios/Runner/AppDelegate.swift
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Flutter
|
2 |
+
import UIKit
|
3 |
+
|
4 |
+
@main
|
5 |
+
@objc class AppDelegate: FlutterAppDelegate {
|
6 |
+
override func application(
|
7 |
+
_ application: UIApplication,
|
8 |
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
9 |
+
) -> Bool {
|
10 |
+
GeneratedPluginRegistrant.register(with: self)
|
11 |
+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
12 |
+
}
|
13 |
+
}
|
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"images" : [
|
3 |
+
{
|
4 |
+
"size" : "20x20",
|
5 |
+
"idiom" : "iphone",
|
6 |
+
"filename" : "Icon-App-20x20@2x.png",
|
7 |
+
"scale" : "2x"
|
8 |
+
},
|
9 |
+
{
|
10 |
+
"size" : "20x20",
|
11 |
+
"idiom" : "iphone",
|
12 |
+
"filename" : "Icon-App-20x20@3x.png",
|
13 |
+
"scale" : "3x"
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"size" : "29x29",
|
17 |
+
"idiom" : "iphone",
|
18 |
+
"filename" : "Icon-App-29x29@1x.png",
|
19 |
+
"scale" : "1x"
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"size" : "29x29",
|
23 |
+
"idiom" : "iphone",
|
24 |
+
"filename" : "Icon-App-29x29@2x.png",
|
25 |
+
"scale" : "2x"
|
26 |
+
},
|
27 |
+
{
|
28 |
+
"size" : "29x29",
|
29 |
+
"idiom" : "iphone",
|
30 |
+
"filename" : "Icon-App-29x29@3x.png",
|
31 |
+
"scale" : "3x"
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"size" : "40x40",
|
35 |
+
"idiom" : "iphone",
|
36 |
+
"filename" : "Icon-App-40x40@2x.png",
|
37 |
+
"scale" : "2x"
|
38 |
+
},
|
39 |
+
{
|
40 |
+
"size" : "40x40",
|
41 |
+
"idiom" : "iphone",
|
42 |
+
"filename" : "Icon-App-40x40@3x.png",
|
43 |
+
"scale" : "3x"
|
44 |
+
},
|
45 |
+
{
|
46 |
+
"size" : "60x60",
|
47 |
+
"idiom" : "iphone",
|
48 |
+
"filename" : "Icon-App-60x60@2x.png",
|
49 |
+
"scale" : "2x"
|
50 |
+
},
|
51 |
+
{
|
52 |
+
"size" : "60x60",
|
53 |
+
"idiom" : "iphone",
|
54 |
+
"filename" : "Icon-App-60x60@3x.png",
|
55 |
+
"scale" : "3x"
|
56 |
+
},
|
57 |
+
{
|
58 |
+
"size" : "20x20",
|
59 |
+
"idiom" : "ipad",
|
60 |
+
"filename" : "Icon-App-20x20@1x.png",
|
61 |
+
"scale" : "1x"
|
62 |
+
},
|
63 |
+
{
|
64 |
+
"size" : "20x20",
|
65 |
+
"idiom" : "ipad",
|
66 |
+
"filename" : "Icon-App-20x20@2x.png",
|
67 |
+
"scale" : "2x"
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"size" : "29x29",
|
71 |
+
"idiom" : "ipad",
|
72 |
+
"filename" : "Icon-App-29x29@1x.png",
|
73 |
+
"scale" : "1x"
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"size" : "29x29",
|
77 |
+
"idiom" : "ipad",
|
78 |
+
"filename" : "Icon-App-29x29@2x.png",
|
79 |
+
"scale" : "2x"
|
80 |
+
},
|
81 |
+
{
|
82 |
+
"size" : "40x40",
|
83 |
+
"idiom" : "ipad",
|
84 |
+
"filename" : "Icon-App-40x40@1x.png",
|
85 |
+
"scale" : "1x"
|
86 |
+
},
|
87 |
+
{
|
88 |
+
"size" : "40x40",
|
89 |
+
"idiom" : "ipad",
|
90 |
+
"filename" : "Icon-App-40x40@2x.png",
|
91 |
+
"scale" : "2x"
|
92 |
+
},
|
93 |
+
{
|
94 |
+
"size" : "76x76",
|
95 |
+
"idiom" : "ipad",
|
96 |
+
"filename" : "Icon-App-76x76@1x.png",
|
97 |
+
"scale" : "1x"
|
98 |
+
},
|
99 |
+
{
|
100 |
+
"size" : "76x76",
|
101 |
+
"idiom" : "ipad",
|
102 |
+
"filename" : "Icon-App-76x76@2x.png",
|
103 |
+
"scale" : "2x"
|
104 |
+
},
|
105 |
+
{
|
106 |
+
"size" : "83.5x83.5",
|
107 |
+
"idiom" : "ipad",
|
108 |
+
"filename" : "Icon-App-83.5x83.5@2x.png",
|
109 |
+
"scale" : "2x"
|
110 |
+
},
|
111 |
+
{
|
112 |
+
"size" : "1024x1024",
|
113 |
+
"idiom" : "ios-marketing",
|
114 |
+
"filename" : "Icon-App-1024x1024@1x.png",
|
115 |
+
"scale" : "1x"
|
116 |
+
}
|
117 |
+
],
|
118 |
+
"info" : {
|
119 |
+
"version" : 1,
|
120 |
+
"author" : "xcode"
|
121 |
+
}
|
122 |
+
}
|