├── .DS_Store
├── README.md
├── architecture.png
├── backend
├── .env.example
├── .gitignore
├── Dockerfile
├── db
│ ├── base.py
│ ├── db.py
│ ├── middleware
│ │ └── auth_middleware.py
│ ├── models
│ │ ├── user.py
│ │ └── video.py
│ └── redis_db.py
├── docker-compose.yml
├── helper
│ └── auth_helper.py
├── main.py
├── pydantic_models
│ ├── auth_models.py
│ └── upload_models.py
├── requirements.txt
├── routes
│ ├── auth.py
│ ├── upload.py
│ └── video.py
└── secret_keys.py
├── consumer
├── .env.example
├── main.py
├── requirements.txt
└── secret_keys.py
├── flutter_client
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── android
│ ├── .gitignore
│ ├── app
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── flutter_client
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── res
│ │ │ │ ├── drawable-v21
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values-night
│ │ │ │ └── styles.xml
│ │ │ │ └── values
│ │ │ │ └── styles.xml
│ │ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── build.gradle.kts
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ └── settings.gradle.kts
├── ios
│ ├── .gitignore
│ ├── Flutter
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── WorkspaceSettings.xcsettings
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ ├── Runner
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ │ └── LaunchImage.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ └── README.md
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ └── Runner-Bridging-Header.h
│ └── RunnerTests
│ │ └── RunnerTests.swift
├── lib
│ ├── cubits
│ │ ├── auth
│ │ │ ├── auth_cubit.dart
│ │ │ └── auth_state.dart
│ │ └── upload_video
│ │ │ ├── upload_video_cubit.dart
│ │ │ └── upload_video_state.dart
│ ├── main.dart
│ ├── pages
│ │ ├── auth
│ │ │ ├── confirm_signup_page.dart
│ │ │ ├── login_page.dart
│ │ │ └── signup_page.dart
│ │ └── home
│ │ │ ├── home_page.dart
│ │ │ ├── upload_page.dart
│ │ │ └── video_player_page.dart
│ ├── services
│ │ ├── auth_service.dart
│ │ ├── upload_video_service.dart
│ │ └── video_service.dart
│ └── utils
│ │ └── utils.dart
├── linux
│ ├── .gitignore
│ ├── CMakeLists.txt
│ ├── flutter
│ │ ├── CMakeLists.txt
│ │ ├── generated_plugin_registrant.cc
│ │ ├── generated_plugin_registrant.h
│ │ └── generated_plugins.cmake
│ └── runner
│ │ ├── CMakeLists.txt
│ │ ├── main.cc
│ │ ├── my_application.cc
│ │ └── my_application.h
├── macos
│ ├── .gitignore
│ ├── Flutter
│ │ ├── Flutter-Debug.xcconfig
│ │ ├── Flutter-Release.xcconfig
│ │ └── GeneratedPluginRegistrant.swift
│ ├── Podfile
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Runner
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── app_icon_1024.png
│ │ │ │ ├── app_icon_128.png
│ │ │ │ ├── app_icon_16.png
│ │ │ │ ├── app_icon_256.png
│ │ │ │ ├── app_icon_32.png
│ │ │ │ ├── app_icon_512.png
│ │ │ │ └── app_icon_64.png
│ │ ├── Base.lproj
│ │ │ └── MainMenu.xib
│ │ ├── Configs
│ │ │ ├── AppInfo.xcconfig
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ └── Warnings.xcconfig
│ │ ├── DebugProfile.entitlements
│ │ ├── Info.plist
│ │ ├── MainFlutterWindow.swift
│ │ └── Release.entitlements
│ └── RunnerTests
│ │ └── RunnerTests.swift
├── pubspec.lock
├── pubspec.yaml
├── test
│ └── widget_test.dart
├── web
│ ├── favicon.png
│ ├── icons
│ │ ├── Icon-192.png
│ │ ├── Icon-512.png
│ │ ├── Icon-maskable-192.png
│ │ └── Icon-maskable-512.png
│ ├── index.html
│ └── manifest.json
└── windows
│ ├── .gitignore
│ ├── CMakeLists.txt
│ ├── flutter
│ ├── CMakeLists.txt
│ ├── generated_plugin_registrant.cc
│ ├── generated_plugin_registrant.h
│ └── generated_plugins.cmake
│ └── runner
│ ├── CMakeLists.txt
│ ├── Runner.rc
│ ├── flutter_window.cpp
│ ├── flutter_window.h
│ ├── main.cpp
│ ├── resource.h
│ ├── resources
│ └── app_icon.ico
│ ├── runner.exe.manifest
│ ├── utils.cpp
│ ├── utils.h
│ ├── win32_window.cpp
│ └── win32_window.h
└── transcoder
├── .env.example
├── Dockerfile
├── main.py
├── requirements.txt
└── secret_keys.py
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/.DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Video streaming app similar to YT
2 |
3 | Technologies used: AWS, FastAPI, Redis, Docker, Flutter, PostgreSQL, Bloc
4 |
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/architecture.png
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | COGNITO_CLIENT_ID=
2 | COGNITO_CLIENT_SECRET=
3 | REGION_NAME=
4 | POSTGRES_DB_URL=
5 | AWS_RAW_VIDEOS_BUCKET=
6 | AWS_ACCESS_KEY_ID=
7 | AWS_SECRET_ACCESS_KEY=
8 | AWS_VIDEO_THUMBNAIL_BUCKET=
9 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | __pycache__
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | WORKDIR /app
4 |
5 | COPY requirements.txt .
6 |
7 | RUN pip install --no-cache-dir -r requirements.txt
8 |
9 | COPY . .
10 |
11 | EXPOSE 8000
12 |
13 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
--------------------------------------------------------------------------------
/backend/db/base.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.declarative import declarative_base
2 |
3 | Base = declarative_base()
4 |
--------------------------------------------------------------------------------
/backend/db/db.py:
--------------------------------------------------------------------------------
1 | from secret_keys import SecretKeys
2 | from sqlalchemy import create_engine
3 | from sqlalchemy.orm import sessionmaker
4 |
5 | secret_keys = SecretKeys()
6 |
7 | engine = create_engine(secret_keys.POSTGRES_DB_URL)
8 | SessionLocal = sessionmaker(
9 | autocommit=False,
10 | autoflush=False,
11 | bind=engine,
12 | )
13 |
14 |
15 | def get_db():
16 | db = SessionLocal()
17 |
18 | try:
19 | yield db
20 | finally:
21 | db.close()
22 |
--------------------------------------------------------------------------------
/backend/db/middleware/auth_middleware.py:
--------------------------------------------------------------------------------
1 | from fastapi import Cookie, HTTPException
2 | import boto3
3 | from secret_keys import SecretKeys
4 |
5 | cognito_client = boto3.client(
6 | "cognito-idp",
7 | region_name=SecretKeys().REGION_NAME,
8 | )
9 |
10 |
11 | def _get_user_from_cognito(access_token: str):
12 | try:
13 | user_res = cognito_client.get_user(AccessToken=access_token)
14 |
15 | return {
16 | attr["Name"]: attr["Value"] for attr in user_res.get("UserAttributes", [])
17 | }
18 | except Exception as e:
19 | raise HTTPException(500, "Error fetching user")
20 |
21 |
22 | def get_current_user(access_token: str = Cookie(None)):
23 | if not access_token:
24 | raise HTTPException(401, "User not logged in!")
25 | print(access_token)
26 | return _get_user_from_cognito(access_token)
27 |
--------------------------------------------------------------------------------
/backend/db/models/user.py:
--------------------------------------------------------------------------------
1 | from db.base import Base
2 | from sqlalchemy import Column, TEXT, Integer
3 |
4 |
5 | class User(Base):
6 | __tablename__ = "users"
7 |
8 | id = Column(Integer, primary_key=True, index=True)
9 | name = Column(TEXT, nullable=False)
10 | email = Column(TEXT, unique=True, index=True, nullable=False)
11 | cognito_sub = Column(TEXT, unique=True, nullable=False, index=True)
12 |
--------------------------------------------------------------------------------
/backend/db/models/video.py:
--------------------------------------------------------------------------------
1 | from db.base import Base
2 | from sqlalchemy import Column, TEXT, Integer, ForeignKey, Enum
3 | import enum
4 |
5 |
6 | class VisibilityStatus(enum.Enum):
7 | PRIVATE = "PRIVATE"
8 | PUBLIC = "PUBLIC"
9 | UNLISTED = "UNLISTED"
10 |
11 |
12 | class ProcessingStatus(enum.Enum):
13 | COMPLETED = "COMPLETED"
14 | FAILED = "FAILED"
15 | IN_PROGRESS = "IN_PROGRESS"
16 |
17 |
18 | class Video(Base):
19 | __tablename__ = "videos"
20 |
21 | id = Column(TEXT, primary_key=True)
22 | title = Column(TEXT)
23 | description = Column(TEXT)
24 | user_id = Column(TEXT, ForeignKey("users.cognito_sub"))
25 | video_s3_key = Column(TEXT)
26 | visibility = Column(
27 | Enum(VisibilityStatus),
28 | nullable=False,
29 | default=VisibilityStatus.PRIVATE,
30 | )
31 | is_processing = Column(
32 | Enum(ProcessingStatus),
33 | nullable=False,
34 | default=ProcessingStatus.IN_PROGRESS,
35 | )
36 |
37 | def to_dict(self):
38 | result = {}
39 | for c in self.__table__.columns:
40 | value = getattr(self, c.name)
41 | if isinstance(value, enum.Enum):
42 | value = value.value
43 | result[c.name] = value
44 | return result
45 |
--------------------------------------------------------------------------------
/backend/db/redis_db.py:
--------------------------------------------------------------------------------
1 | import redis
2 |
3 | redis_client = redis.Redis(host="redis", port=6379)
4 |
--------------------------------------------------------------------------------
/backend/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | fastapi:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | container_name: fastapi_container
7 | ports:
8 | - "8000:8000"
9 | image: fastapi_app
10 | environment:
11 | - DATABASE_URL=postgresql://postgres:test123@db:5432/mydatabase
12 | volumes:
13 | - ./:/app
14 | db:
15 | image: postgres:15
16 | container_name: postgres_db
17 | ports:
18 | - "5432:5432"
19 | environment:
20 | POSTGRES_DB: mydatabase
21 | POSTGRES_USER: postgres
22 | POSTGRES_PASSWORD: test123
23 | volumes:
24 | - postgres_data:/var/lib/postgresql/data
25 |
26 | redis:
27 | image: redis:7.4.2
28 | container_name: redis
29 | ports:
30 | - "6379:6379"
31 | volumes:
32 | - redis_data:/data
33 |
34 | volumes:
35 | postgres_data:
36 | redis_data:
--------------------------------------------------------------------------------
/backend/helper/auth_helper.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import hashlib
3 | import hmac
4 |
5 |
6 | def get_secret_hash(username: str, client_id: str, client_secret: str):
7 | message = username + client_id
8 |
9 | digest = hmac.new(
10 | client_secret.encode("utf-8"),
11 | msg=message.encode("utf-8"),
12 | digestmod=hashlib.sha256,
13 | ).digest()
14 |
15 | return base64.b64encode(digest).decode()
16 |
--------------------------------------------------------------------------------
/backend/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from db.base import Base
4 | from routes import auth, upload, video
5 | from db.db import engine
6 |
7 | app = FastAPI()
8 |
9 | origins = ["http://localhost", "http://localhost:3000"]
10 |
11 | app.add_middleware(
12 | CORSMiddleware,
13 | allow_origins=origins,
14 | allow_credentials=True,
15 | allow_methods=["*"],
16 | allow_headers=["*"],
17 | )
18 |
19 | app.include_router(auth.router, prefix="/auth")
20 | app.include_router(upload.router, prefix="/upload/video")
21 | app.include_router(video.router, prefix="/videos")
22 |
23 |
24 | @app.get("/")
25 | def root():
26 | return "Hello, World!!!"
27 |
28 |
29 | Base.metadata.create_all(engine)
30 |
--------------------------------------------------------------------------------
/backend/pydantic_models/auth_models.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class SignupRequest(BaseModel):
5 | name: str
6 | email: str
7 | password: str
8 |
9 |
10 | class LoginRequest(BaseModel):
11 | email: str
12 | password: str
13 |
14 |
15 | class ConfirmSignupRequest(BaseModel):
16 | email: str
17 | otp: str
18 |
--------------------------------------------------------------------------------
/backend/pydantic_models/upload_models.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class UploadMetadata(BaseModel):
5 | title: str
6 | description: str
7 | video_id: str
8 | video_s3_key: str
9 | visibility: str
10 |
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | boto3
4 | pydantic-settings
5 | python-dotenv
6 | sqlalchemy
7 | psycopg2-binary
8 | redis
--------------------------------------------------------------------------------
/backend/routes/auth.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Cookie, Depends, HTTPException, Response
2 | import boto3
3 | from db.db import get_db
4 | from db.middleware.auth_middleware import get_current_user
5 | from db.models.user import User
6 | from helper.auth_helper import get_secret_hash
7 | from pydantic_models.auth_models import (
8 | ConfirmSignupRequest,
9 | LoginRequest,
10 | SignupRequest,
11 | )
12 | from secret_keys import SecretKeys
13 | from sqlalchemy.orm import Session
14 |
15 | router = APIRouter()
16 | secret_keys = SecretKeys()
17 |
18 | COGNITO_CLIENT_ID = secret_keys.COGNITO_CLIENT_ID
19 | COGNITO_CLIENT_SECRET = secret_keys.COGNITO_CLIENT_SECRET
20 |
21 | cognito_client = boto3.client(
22 | "cognito-idp",
23 | region_name=secret_keys.REGION_NAME,
24 | )
25 |
26 |
27 | @router.post("/signup")
28 | def signup_user(
29 | data: SignupRequest,
30 | db: Session = Depends(get_db),
31 | ):
32 | try:
33 | secret_hash = get_secret_hash(
34 | data.email,
35 | COGNITO_CLIENT_ID,
36 | COGNITO_CLIENT_SECRET,
37 | )
38 |
39 | cognito_response = cognito_client.sign_up(
40 | ClientId=COGNITO_CLIENT_ID,
41 | Username=data.email,
42 | Password=data.password,
43 | SecretHash=secret_hash,
44 | UserAttributes=[
45 | {"Name": "email", "Value": data.email},
46 | {"Name": "name", "Value": data.name},
47 | ],
48 | )
49 |
50 | cognito_sub = cognito_response.get("UserSub")
51 |
52 | if not cognito_sub:
53 | raise HTTPException(400, "Cognito did not return a valid user sub")
54 |
55 | new_user = User(
56 | name=data.name,
57 | email=data.email,
58 | cognito_sub=cognito_sub,
59 | )
60 | db.add(new_user)
61 | db.commit()
62 | db.refresh(new_user)
63 |
64 | return {"message": "Signup successful. Please verify your email if required."}
65 | except Exception as e:
66 | raise HTTPException(400, f"Cognito sugnup exception: {e}")
67 |
68 |
69 | @router.post("/login")
70 | def login_user(data: LoginRequest, response: Response):
71 | try:
72 | secret_hash = get_secret_hash(
73 | data.email,
74 | COGNITO_CLIENT_ID,
75 | COGNITO_CLIENT_SECRET,
76 | )
77 |
78 | cognito_response = cognito_client.initiate_auth(
79 | ClientId=COGNITO_CLIENT_ID,
80 | AuthFlow="USER_PASSWORD_AUTH",
81 | AuthParameters={
82 | "USERNAME": data.email,
83 | "PASSWORD": data.password,
84 | "SECRET_HASH": secret_hash,
85 | },
86 | )
87 |
88 | auth_result = cognito_response.get("AuthenticationResult")
89 |
90 | if not auth_result:
91 | raise HTTPException(400, "Incorrect cognito response")
92 |
93 | access_token = auth_result.get("AccessToken")
94 | refresh_token = auth_result.get("RefreshToken")
95 |
96 | response.set_cookie(
97 | key="access_token",
98 | value=access_token,
99 | httponly=True,
100 | secure=True,
101 | )
102 | response.set_cookie(
103 | key="refresh_token",
104 | value=refresh_token,
105 | httponly=True,
106 | secure=True,
107 | )
108 |
109 | return {"message": "User logged in successfully!"}
110 | except Exception as e:
111 | raise HTTPException(400, f"Cognito sugnup exception: {e}")
112 |
113 |
114 | @router.post("/confirm-signup")
115 | def confirm_signup(data: ConfirmSignupRequest):
116 | try:
117 | secret_hash = get_secret_hash(
118 | data.email,
119 | COGNITO_CLIENT_ID,
120 | COGNITO_CLIENT_SECRET,
121 | )
122 |
123 | cognito_response = cognito_client.confirm_sign_up(
124 | ClientId=COGNITO_CLIENT_ID,
125 | Username=data.email,
126 | ConfirmationCode=data.otp,
127 | SecretHash=secret_hash,
128 | )
129 |
130 | return {"message": "User confirmed successfully!"}
131 | except Exception as e:
132 | raise HTTPException(400, f"Cognito sugnup exception: {e}")
133 |
134 |
135 | @router.post("/refresh")
136 | def refresh_token(
137 | refresh_token: str = Cookie(None),
138 | user_cognito_sub: str = Cookie(None),
139 | response: Response = None,
140 | ):
141 | try:
142 | if not refresh_token or not user_cognito_sub:
143 | raise HTTPException(400, "cookies cannot be null!")
144 | secret_hash = get_secret_hash(
145 | user_cognito_sub,
146 | COGNITO_CLIENT_ID,
147 | COGNITO_CLIENT_SECRET,
148 | )
149 |
150 | cognito_response = cognito_client.initiate_auth(
151 | ClientId=COGNITO_CLIENT_ID,
152 | AuthFlow="REFRESH_TOKEN_AUTH",
153 | AuthParameters={
154 | "REFRESH_TOKEN": refresh_token,
155 | "SECRET_HASH": secret_hash,
156 | },
157 | )
158 | auth_result = cognito_response.get("AuthenticationResult")
159 |
160 | if not auth_result:
161 | raise HTTPException(400, "Incorrect cognito response")
162 |
163 | access_token = auth_result.get("AccessToken")
164 |
165 | response.set_cookie(
166 | key="access_token",
167 | value=access_token,
168 | httponly=True,
169 | secure=True,
170 | )
171 |
172 | return {"message": "Access token refreshed!"}
173 | except Exception as e:
174 | raise HTTPException(400, f"Cognito sugnup exception: {e}")
175 |
176 |
177 | @router.get("/me")
178 | def protected_route(user=Depends(get_current_user)):
179 | return {"message": "You are authenticated!", "user": user}
180 |
--------------------------------------------------------------------------------
/backend/routes/upload.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | from fastapi import APIRouter, Depends, HTTPException
3 | from db.db import get_db
4 | from db.models.video import Video
5 | from pydantic_models.upload_models import UploadMetadata
6 | from sqlalchemy.orm import Session
7 | from db.middleware.auth_middleware import get_current_user
8 | from secret_keys import SecretKeys
9 | import uuid
10 |
11 | router = APIRouter()
12 | secret_keys = SecretKeys()
13 |
14 | s3_client = boto3.client(
15 | "s3",
16 | region_name=secret_keys.REGION_NAME,
17 | )
18 |
19 |
20 | @router.get("/url")
21 | def get_presigned_url(user=Depends(get_current_user)):
22 | try:
23 | video_id = f"videos/{user['sub']}/{uuid.uuid4()}.mp4"
24 |
25 | response = s3_client.generate_presigned_url(
26 | "put_object",
27 | Params={
28 | "Bucket": secret_keys.AWS_RAW_VIDEOS_BUCKET,
29 | "Key": video_id,
30 | "ContentType": "video/mp4",
31 | },
32 | )
33 | print(response)
34 |
35 | return {
36 | "url": response,
37 | "video_id": video_id,
38 | }
39 | except Exception as e:
40 | raise HTTPException(500, str(e))
41 |
42 |
43 | @router.get("/url/thumbnail")
44 | def get_presigned_url_thumbnail(thumbnail_id: str, user=Depends(get_current_user)):
45 | try:
46 | thumbnail_id = thumbnail_id.replace("videos/", "thumbnails/").replace(
47 | ".mp4", ""
48 | )
49 | response = s3_client.generate_presigned_url(
50 | "put_object",
51 | Params={
52 | "Bucket": secret_keys.AWS_VIDEO_THUMBNAIL_BUCKET,
53 | "Key": thumbnail_id,
54 | "ContentType": "image/jpg",
55 | "ACL": "public-read",
56 | },
57 | )
58 | print(response)
59 |
60 | return {
61 | "url": response,
62 | "thumbnail_id": thumbnail_id,
63 | }
64 | except Exception as e:
65 | raise HTTPException(500, str(e))
66 |
67 |
68 | @router.post("/metadata")
69 | def upload_metadata(
70 | metadata: UploadMetadata,
71 | user=Depends(get_current_user),
72 | db: Session = Depends(get_db),
73 | ):
74 | new_video = Video(
75 | id=metadata.video_id,
76 | title=metadata.title,
77 | description=metadata.description,
78 | video_s3_key=metadata.video_s3_key,
79 | visibility=metadata.visibility,
80 | user_id=user["sub"],
81 | )
82 |
83 | db.add(new_video)
84 | db.commit()
85 | db.refresh(new_video)
86 |
87 | return new_video
88 |
--------------------------------------------------------------------------------
/backend/routes/video.py:
--------------------------------------------------------------------------------
1 | import json
2 | from fastapi import APIRouter, Depends, HTTPException
3 | from db.db import get_db
4 | from db.middleware.auth_middleware import get_current_user
5 | from db.models.video import ProcessingStatus, Video, VisibilityStatus
6 | from sqlalchemy.orm import Session
7 | from sqlalchemy import or_
8 | from db.redis_db import redis_client
9 |
10 | router = APIRouter()
11 |
12 |
13 | @router.get("/all")
14 | def get_all_videos(
15 | db: Session = Depends(get_db),
16 | user=Depends(
17 | get_current_user,
18 | ),
19 | ):
20 | all_videos = (
21 | db.query(Video)
22 | .filter(
23 | Video.is_processing == ProcessingStatus.COMPLETED,
24 | Video.visibility == VisibilityStatus.PUBLIC,
25 | )
26 | .all()
27 | )
28 |
29 | return all_videos
30 |
31 |
32 | @router.get("/")
33 | def get_video_info(
34 | video_id: str,
35 | db: Session = Depends(get_db),
36 | user=Depends(
37 | get_current_user,
38 | ),
39 | ):
40 | cache_key = f"video:{video_id}"
41 | cached_data = redis_client.get(cache_key)
42 |
43 | if cached_data:
44 | return json.loads(cached_data)
45 |
46 | video = (
47 | db.query(Video)
48 | .filter(
49 | Video.id == video_id,
50 | Video.is_processing == ProcessingStatus.COMPLETED,
51 | or_(
52 | Video.visibility == VisibilityStatus.PUBLIC,
53 | Video.visibility == VisibilityStatus.UNLISTED,
54 | ),
55 | )
56 | .first()
57 | )
58 |
59 | redis_client.setex(cache_key, 3600, json.dumps(video.to_dict()))
60 |
61 | return video
62 |
63 |
64 | @router.put("/")
65 | def update_video_by_id(id: str, db: Session = Depends(get_db)):
66 | video = db.query(Video).filter(Video.id == id).first()
67 |
68 | if not video:
69 | raise HTTPException(404, "Video not found!")
70 |
71 | video.is_processing = ProcessingStatus.COMPLETED
72 | db.commit()
73 | db.refresh(video)
74 |
75 | return video
76 |
--------------------------------------------------------------------------------
/backend/secret_keys.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings
2 | from dotenv import load_dotenv
3 |
4 | load_dotenv()
5 |
6 |
7 | class SecretKeys(BaseSettings):
8 | COGNITO_CLIENT_ID: str = ""
9 | COGNITO_CLIENT_SECRET: str = ""
10 | REGION_NAME: str = ""
11 | POSTGRES_DB_URL: str = ""
12 | AWS_RAW_VIDEOS_BUCKET: str = ""
13 | AWS_VIDEO_THUMBNAIL_BUCKET: str = ""
14 |
--------------------------------------------------------------------------------
/consumer/.env.example:
--------------------------------------------------------------------------------
1 | REGION_NAME=
2 | AWS_SQS_VIDEO_PROCESSING=
--------------------------------------------------------------------------------
/consumer/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | from secret_keys import SecretKeys
4 |
5 | secret_keys = SecretKeys()
6 | sqs_client = boto3.client(
7 | "sqs",
8 | region_name=secret_keys.REGION_NAME,
9 | )
10 |
11 |
12 | ecs_client = boto3.client(
13 | "ecs",
14 | region_name=secret_keys.REGION_NAME,
15 | )
16 |
17 |
18 | def poll_sqs():
19 | while True:
20 | response = sqs_client.receive_message(
21 | QueueUrl=secret_keys.AWS_SQS_VIDEO_PROCESSING,
22 | MaxNumberOfMessages=1,
23 | WaitTimeSeconds=10,
24 | )
25 |
26 | for message in response.get("Messages", []):
27 | message_body = json.loads(message.get("Body"))
28 |
29 | if (
30 | "Service" in message_body
31 | and "Event" in message_body
32 | and message_body.get("Event") == "s3:TestEvent"
33 | ):
34 | sqs_client.delete_message(
35 | QueueUrl=secret_keys.AWS_SQS_VIDEO_PROCESSING,
36 | ReceiptHandle=message["ReceiptHandle"],
37 | )
38 | continue
39 |
40 | if "Records" in message_body:
41 | s3_record = message_body["Records"][0]["s3"]
42 | bucket_name = s3_record["bucket"]["name"]
43 | s3_key = s3_record["object"]["key"]
44 |
45 | response = ecs_client.run_task(
46 | cluster="arn:aws:ecs:ap-south-1:605134446036:cluster/Rivaan-TranscoderCluster",
47 | launchType="FARGATE",
48 | taskDefinition="arn:aws:ecs:ap-south-1:605134446036:task-definition/video-transcoder:2",
49 | overrides={
50 | "containerOverrides": [
51 | {
52 | "name": "video-transcoder",
53 | "environment": [
54 | {"name": "S3_BUCKET", "value": bucket_name},
55 | {"name": "S3_KEY", "value": s3_key},
56 | ],
57 | }
58 | ]
59 | },
60 | networkConfiguration={
61 | "awsvpcConfiguration": {
62 | "subnets": [
63 | "subnet-0c1cf3385363a7d66",
64 | "subnet-02bdfb9f47bd9b9a6",
65 | "subnet-0cacf7adeb5e67b96",
66 | ],
67 | "assignPublicIp": "ENABLED",
68 | "securityGroups": ["sg-0279fe5646343ea75"],
69 | }
70 | },
71 | )
72 |
73 | print(response)
74 | sqs_client.delete_message(
75 | QueueUrl=secret_keys.AWS_SQS_VIDEO_PROCESSING,
76 | ReceiptHandle=message["ReceiptHandle"],
77 | )
78 |
79 |
80 | poll_sqs()
81 |
--------------------------------------------------------------------------------
/consumer/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3
2 | pydantic_settings
3 | python-dotenv
--------------------------------------------------------------------------------
/consumer/secret_keys.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings
2 | from dotenv import load_dotenv
3 |
4 | load_dotenv()
5 |
6 |
7 | class SecretKeys(BaseSettings):
8 | REGION_NAME: str = ""
9 | AWS_SQS_VIDEO_PROCESSING: str = ""
10 |
--------------------------------------------------------------------------------
/flutter_client/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
13 | migrate_working_dir/
14 |
15 | # IntelliJ related
16 | *.iml
17 | *.ipr
18 | *.iws
19 | .idea/
20 |
21 | # The .vscode folder contains launch configuration and tasks you configure in
22 | # VS Code which you may wish to be included in version control, so this line
23 | # is commented out by default.
24 | #.vscode/
25 |
26 | # Flutter/Dart/Pub related
27 | **/doc/api/
28 | **/ios/Flutter/.last_build_id
29 | .dart_tool/
30 | .flutter-plugins
31 | .flutter-plugins-dependencies
32 | .pub-cache/
33 | .pub/
34 | /build/
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/flutter_client/.metadata:
--------------------------------------------------------------------------------
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: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
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: 35c388afb57ef061d06a39b537336c87e0e3d1b1
17 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
18 | - platform: android
19 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
20 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
21 | - platform: ios
22 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
23 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
24 | - platform: linux
25 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
26 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
27 | - platform: macos
28 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
29 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
30 | - platform: web
31 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
32 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
33 | - platform: windows
34 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
35 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
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'
46 |
--------------------------------------------------------------------------------
/flutter_client/README.md:
--------------------------------------------------------------------------------
1 | # flutter_client
2 |
3 | A new Flutter project.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
13 |
14 | For help getting started with Flutter development, view the
15 | [online documentation](https://docs.flutter.dev/), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/flutter_client/analysis_options.yaml:
--------------------------------------------------------------------------------
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
29 |
--------------------------------------------------------------------------------
/flutter_client/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 | .cxx/
9 |
10 | # Remember to never publicly share your keystore.
11 | # See https://flutter.dev/to/reference-keystore
12 | key.properties
13 | **/*.keystore
14 | **/*.jks
15 |
--------------------------------------------------------------------------------
/flutter_client/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
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.flutter_client"
10 | compileSdk = flutter.compileSdkVersion
11 | ndkVersion = flutter.ndkVersion
12 |
13 | compileOptions {
14 | sourceCompatibility = JavaVersion.VERSION_11
15 | targetCompatibility = JavaVersion.VERSION_11
16 | }
17 |
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_11.toString()
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.flutter_client"
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.getByName("debug")
38 | }
39 | }
40 | }
41 |
42 | flutter {
43 | source = "../.."
44 | }
45 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/kotlin/com/example/flutter_client/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.flutter_client
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity : FlutterActivity()
6 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/flutter_client/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/flutter_client/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
9 | rootProject.layout.buildDirectory.value(newBuildDir)
10 |
11 | subprojects {
12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
13 | project.layout.buildDirectory.value(newSubprojectBuildDir)
14 | }
15 | subprojects {
16 | project.evaluationDependsOn(":app")
17 | }
18 |
19 | tasks.register("clean") {
20 | delete(rootProject.layout.buildDirectory)
21 | }
22 |
--------------------------------------------------------------------------------
/flutter_client/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/flutter_client/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
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.10.2-all.zip
6 |
--------------------------------------------------------------------------------
/flutter_client/android/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | val flutterSdkPath = run {
3 | val properties = java.util.Properties()
4 | file("local.properties").inputStream().use { properties.load(it) }
5 | val flutterSdkPath = properties.getProperty("flutter.sdk")
6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
7 | 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.7.0" apply false
22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false
23 | }
24 |
25 | include(":app")
26 |
--------------------------------------------------------------------------------
/flutter_client/ios/.gitignore:
--------------------------------------------------------------------------------
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
35 |
--------------------------------------------------------------------------------
/flutter_client/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/flutter_client/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/flutter_client/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/flutter_client/ios/Podfile:
--------------------------------------------------------------------------------
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 |
33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
34 | target 'RunnerTests' do
35 | inherit! :search_paths
36 | end
37 | end
38 |
39 | post_install do |installer|
40 | installer.pods_project.targets.each do |target|
41 | flutter_additional_ios_build_settings(target)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/flutter_client/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - better_player (0.0.1):
3 | - Cache (~> 6.0.0)
4 | - Flutter
5 | - GCDWebServer
6 | - HLSCachingReverseProxyServer
7 | - PINCache
8 | - Cache (6.0.0)
9 | - Flutter (1.0.0)
10 | - flutter_secure_storage (6.0.0):
11 | - Flutter
12 | - GCDWebServer (3.5.4):
13 | - GCDWebServer/Core (= 3.5.4)
14 | - GCDWebServer/Core (3.5.4)
15 | - HLSCachingReverseProxyServer (0.1.0):
16 | - GCDWebServer (~> 3.5)
17 | - PINCache (>= 3.0.1-beta.3)
18 | - image_picker_ios (0.0.1):
19 | - Flutter
20 | - package_info_plus (0.4.5):
21 | - Flutter
22 | - path_provider_foundation (0.0.1):
23 | - Flutter
24 | - FlutterMacOS
25 | - PINCache (3.0.4):
26 | - PINCache/Arc-exception-safe (= 3.0.4)
27 | - PINCache/Core (= 3.0.4)
28 | - PINCache/Arc-exception-safe (3.0.4):
29 | - PINCache/Core
30 | - PINCache/Core (3.0.4):
31 | - PINOperation (~> 1.2.3)
32 | - PINOperation (1.2.3)
33 | - wakelock_plus (0.0.1):
34 | - Flutter
35 |
36 | DEPENDENCIES:
37 | - better_player (from `.symlinks/plugins/better_player/ios`)
38 | - Flutter (from `Flutter`)
39 | - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
40 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
41 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
42 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
43 | - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
44 |
45 | SPEC REPOS:
46 | trunk:
47 | - Cache
48 | - GCDWebServer
49 | - HLSCachingReverseProxyServer
50 | - PINCache
51 | - PINOperation
52 |
53 | EXTERNAL SOURCES:
54 | better_player:
55 | :path: ".symlinks/plugins/better_player/ios"
56 | Flutter:
57 | :path: Flutter
58 | flutter_secure_storage:
59 | :path: ".symlinks/plugins/flutter_secure_storage/ios"
60 | image_picker_ios:
61 | :path: ".symlinks/plugins/image_picker_ios/ios"
62 | package_info_plus:
63 | :path: ".symlinks/plugins/package_info_plus/ios"
64 | path_provider_foundation:
65 | :path: ".symlinks/plugins/path_provider_foundation/darwin"
66 | wakelock_plus:
67 | :path: ".symlinks/plugins/wakelock_plus/ios"
68 |
69 | SPEC CHECKSUMS:
70 | better_player: 472a1f3471b8991bde82327c91498b0f7934245d
71 | Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d
72 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
73 | flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
74 | GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
75 | HLSCachingReverseProxyServer: 59935e1e0244ad7f3375d75b5ef46e8eb26ab181
76 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
77 | package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
78 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
79 | PINCache: d9a87a0ff397acffe9e2f0db972ac14680441158
80 | PINOperation: fb563bcc9c32c26d6c78aaff967d405aa2ee74a7
81 | wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
82 |
83 | PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
84 |
85 | COCOAPODS: 1.16.2
86 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
64 |
66 |
72 |
73 |
74 |
75 |
81 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
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 | }
14 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
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 | }
123 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivaanRanawat/video_streaming_app_tutorial/d1cf550d3f9144ebd7798e0cc8b659d5affd81b9/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Flutter Client
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | flutter_client
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | CADisableMinimumFrameDurationOnPhone
45 |
46 | UIApplicationSupportsIndirectInputEvents
47 |
48 | NSPhotoLibraryUsageDescription
49 | Access to gallery for thumbnail
50 | NSCameraUsageDescription
51 | Access to camera for thumbnail or for video
52 | NSMicrophoneUsageDescription
53 | Access to mic for video
54 | NSAppTransportSecurity
55 |
56 | NSAllowsArbitraryLoads
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/flutter_client/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/flutter_client/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/flutter_client/lib/cubits/auth/auth_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/services/auth_service.dart';
4 |
5 | part 'auth_state.dart';
6 |
7 | class AuthCubit extends Cubit {
8 | AuthCubit() : super(AuthInitial());
9 | final AuthService authService = AuthService();
10 |
11 | void signUpUser({
12 | required String name,
13 | required String email,
14 | required String password,
15 | }) async {
16 | emit(AuthLoading());
17 | try {
18 | final res = await authService.signUpUser(
19 | name: name,
20 | password: password,
21 | email: email,
22 | );
23 | emit(AuthSignupSuccess(res));
24 | } catch (e) {
25 | emit(AuthError(e.toString()));
26 | }
27 | }
28 |
29 | void confirmSignUpUser({required String email, required String otp}) async {
30 | emit(AuthLoading());
31 | try {
32 | final res = await authService.confirmSignUpUser(email: email, otp: otp);
33 | emit(AuthConfirmSignupSuccess(res));
34 | } catch (e) {
35 | emit(AuthError(e.toString()));
36 | }
37 | }
38 |
39 | void loginUser({required String email, required String password}) async {
40 | emit(AuthLoading());
41 | try {
42 | final res = await authService.loginUser(password: password, email: email);
43 | emit(AuthLoginSuccess(res));
44 | } catch (e) {
45 | emit(AuthError(e.toString()));
46 | }
47 | }
48 |
49 | void isAuthenticated() async {
50 | emit(AuthLoading());
51 | try {
52 | final res = await authService.isAuthenticated();
53 | if (res) {
54 | emit(AuthLoginSuccess('Logged in!'));
55 | } else {
56 | emit(AuthInitial());
57 | }
58 | } catch (e) {
59 | emit(AuthError(e.toString()));
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/flutter_client/lib/cubits/auth/auth_state.dart:
--------------------------------------------------------------------------------
1 | part of 'auth_cubit.dart';
2 |
3 | @immutable
4 | sealed class AuthState {}
5 |
6 | final class AuthInitial extends AuthState {}
7 |
8 | final class AuthLoading extends AuthState {}
9 |
10 | final class AuthSignupSuccess extends AuthState {
11 | final String message;
12 |
13 | AuthSignupSuccess(this.message);
14 | }
15 |
16 | final class AuthLoginSuccess extends AuthState {
17 | final String message;
18 |
19 | AuthLoginSuccess(this.message);
20 | }
21 |
22 | final class AuthConfirmSignupSuccess extends AuthState {
23 | final String message;
24 |
25 | AuthConfirmSignupSuccess(this.message);
26 | }
27 |
28 | final class AuthError extends AuthState {
29 | final String error;
30 | AuthError(this.error);
31 | }
32 |
--------------------------------------------------------------------------------
/flutter_client/lib/cubits/upload_video/upload_video_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/cubits/auth/auth_cubit.dart';
4 | import 'package:flutter_client/services/upload_video_service.dart';
5 | import 'package:path/path.dart' show dirname;
6 | import 'dart:io';
7 |
8 | import 'package:path_provider/path_provider.dart';
9 | part 'upload_video_state.dart';
10 |
11 | class UploadVideoCubit extends Cubit {
12 | UploadVideoCubit() : super(UploadVideoInitial());
13 | final uploadVideoService = UploadVideoService();
14 |
15 | Future uploadVideo({
16 | required File videoFile,
17 | required File thumbnailFile,
18 | required String title,
19 | required String description,
20 | required String visibility,
21 | }) async {
22 | emit(UploadVideoLoading());
23 | try {
24 | final videoData = await uploadVideoService.getPresignedUrlForVideo();
25 | final thumbnailData = await uploadVideoService
26 | .getPresignedUrlForThumbnail(videoData['video_id']);
27 |
28 | final appDir = await getApplicationDocumentsDirectory();
29 | if (!appDir.existsSync()) {
30 | appDir.createSync(recursive: true);
31 | }
32 |
33 | final newThumbnailPath =
34 | "${appDir.path}/${thumbnailData['thumbnail_id']}";
35 | final newVideoPath = "${appDir.path}/${videoData['video_id']}";
36 |
37 | final thumbnailDir = Directory(dirname(newThumbnailPath));
38 | final videoDir = Directory(dirname(newVideoPath));
39 |
40 | if (!thumbnailDir.existsSync()) {
41 | thumbnailDir.createSync(recursive: true);
42 | }
43 |
44 | if (!videoDir.existsSync()) {
45 | videoDir.createSync(recursive: true);
46 | }
47 |
48 | File newThumbnailFile = await thumbnailFile.copy(newThumbnailPath);
49 | File newVideoFile = await videoFile.copy(newVideoPath);
50 |
51 | final isThumbnailUploaded = await uploadVideoService.uploadFileToS3(
52 | presignedUrl: thumbnailData['url'],
53 | file: newThumbnailFile,
54 | isVideo: false,
55 | );
56 |
57 | final isVideoUploaded = await uploadVideoService.uploadFileToS3(
58 | presignedUrl: videoData['url'],
59 | file: newVideoFile,
60 | isVideo: true,
61 | );
62 |
63 | if (isThumbnailUploaded && isVideoUploaded) {
64 | final isMetadataUploaded = await uploadVideoService.uploadMetadata(
65 | title: title,
66 | description: description,
67 | visibility: visibility,
68 | s3Key: videoData['video_id'],
69 | );
70 |
71 | if (isMetadataUploaded) {
72 | emit(UploadVideoSuccess());
73 | } else {
74 | emit(UploadVideoError('Metadata not uploaded to backend!'));
75 | }
76 | } else {
77 | emit(UploadVideoError('Files not uploaded to S3!'));
78 | }
79 |
80 | try {
81 | if (newThumbnailFile.existsSync()) {
82 | await newThumbnailFile.delete();
83 | }
84 | if (newVideoFile.existsSync()) {
85 | await newVideoFile.delete();
86 | }
87 | } catch (e) {
88 | print('Error cleaning up temp files: $e');
89 | }
90 | } catch (e) {
91 | print('Upload error: $e');
92 | emit(UploadVideoError(e.toString()));
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/flutter_client/lib/cubits/upload_video/upload_video_state.dart:
--------------------------------------------------------------------------------
1 | part of 'upload_video_cubit.dart';
2 |
3 | @immutable
4 | sealed class UploadVideoState {
5 | const UploadVideoState();
6 | }
7 |
8 | final class UploadVideoInitial extends UploadVideoState {}
9 |
10 | final class UploadVideoLoading extends UploadVideoState {}
11 |
12 | final class UploadVideoSuccess extends UploadVideoState {}
13 |
14 | final class UploadVideoError extends UploadVideoState {
15 | final String error;
16 | const UploadVideoError(this.error);
17 | }
18 |
--------------------------------------------------------------------------------
/flutter_client/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/cubits/auth/auth_cubit.dart';
4 | import 'package:flutter_client/cubits/upload_video/upload_video_cubit.dart';
5 | import 'package:flutter_client/pages/auth/signup_page.dart';
6 | import 'package:flutter_client/pages/home/home_page.dart';
7 |
8 | void main() {
9 | runApp(
10 | MultiBlocProvider(
11 | providers: [
12 | BlocProvider(create: (context) => AuthCubit()),
13 | BlocProvider(create: (context) => UploadVideoCubit()),
14 | ],
15 | child: const MyApp(),
16 | ),
17 | );
18 | }
19 |
20 | class MyApp extends StatefulWidget {
21 | const MyApp({super.key});
22 |
23 | @override
24 | State createState() => _MyAppState();
25 | }
26 |
27 | class _MyAppState extends State {
28 | @override
29 | void initState() {
30 | super.initState();
31 | context.read().isAuthenticated();
32 | }
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | return MaterialApp(
37 | title: 'YT Clone',
38 | theme: ThemeData(
39 | elevatedButtonTheme: ElevatedButtonThemeData(
40 | style: ElevatedButton.styleFrom(
41 | backgroundColor: Colors.black,
42 | minimumSize: Size(double.infinity, 60),
43 | shape: RoundedRectangleBorder(
44 | borderRadius: BorderRadius.circular(10),
45 | ),
46 | ),
47 | ),
48 | inputDecorationTheme: InputDecorationTheme(
49 | contentPadding: const EdgeInsets.all(27),
50 | enabledBorder: OutlineInputBorder(
51 | borderSide: BorderSide(color: Colors.grey.shade300, width: 3),
52 | borderRadius: BorderRadius.all(Radius.circular(5)),
53 | ),
54 | focusedBorder: OutlineInputBorder(
55 | borderSide: BorderSide(width: 3),
56 | borderRadius: BorderRadius.all(Radius.circular(5)),
57 | ),
58 | errorBorder: OutlineInputBorder(
59 | borderSide: BorderSide(color: Colors.red, width: 3),
60 | borderRadius: BorderRadius.all(Radius.circular(5)),
61 | ),
62 | border: OutlineInputBorder(
63 | borderSide: BorderSide(width: 3),
64 | borderRadius: BorderRadius.all(Radius.circular(5)),
65 | ),
66 | ),
67 | ),
68 | home: BlocBuilder(
69 | builder: (context, state) {
70 | if (state is AuthInitial) {
71 | return SignupPage();
72 | } else if (state is AuthLoginSuccess) {
73 | return HomePage();
74 | } else if (state is AuthError) {
75 | return SignupPage();
76 | }
77 |
78 | return const SizedBox();
79 | },
80 | ),
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/auth/confirm_signup_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/cubits/auth/auth_cubit.dart';
4 | import 'package:flutter_client/pages/auth/login_page.dart';
5 | import 'package:flutter_client/services/auth_service.dart';
6 | import 'package:flutter_client/utils/utils.dart';
7 |
8 | class ConfirmSignupPage extends StatefulWidget {
9 | final String email;
10 | static route(String email) =>
11 | MaterialPageRoute(builder: (context) => ConfirmSignupPage(email: email));
12 | const ConfirmSignupPage({super.key, required this.email});
13 |
14 | @override
15 | State createState() => _ConfirmSignupPageState();
16 | }
17 |
18 | class _ConfirmSignupPageState extends State {
19 | final otpController = TextEditingController();
20 | late TextEditingController emailController;
21 | final formKey = GlobalKey();
22 | final AuthService authService = AuthService();
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | emailController = TextEditingController(text: widget.email);
28 | }
29 |
30 | @override
31 | void dispose() {
32 | otpController.dispose();
33 | emailController.dispose();
34 | super.dispose();
35 | }
36 |
37 | void confirmSignUp() async {
38 | if (formKey.currentState!.validate()) {
39 | context.read().confirmSignUpUser(
40 | email: emailController.text.trim(),
41 | otp: otpController.text.trim(),
42 | );
43 | }
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return Scaffold(
49 | body: BlocConsumer(
50 | listener: (context, state) {
51 | if (state is AuthConfirmSignupSuccess) {
52 | showSnackBar(state.message, context);
53 | Navigator.push(context, LoginPage.route());
54 | } else if (state is AuthError) {
55 | showSnackBar(state.error, context);
56 | }
57 | },
58 | builder: (context, state) {
59 | if (state is AuthLoading) {
60 | return Center(child: CircularProgressIndicator.adaptive());
61 | }
62 |
63 | return Padding(
64 | padding: const EdgeInsets.all(15.0),
65 | child: Form(
66 | key: formKey,
67 | child: Column(
68 | mainAxisAlignment: MainAxisAlignment.center,
69 | children: [
70 | Text(
71 | 'Confirm Sign Up',
72 | style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
73 | ),
74 | const SizedBox(height: 30),
75 | TextFormField(
76 | controller: emailController,
77 | decoration: InputDecoration(hintText: 'Email'),
78 | validator: (value) {
79 | if (value != null && value.trim().isEmpty) {
80 | return "Field cannot be empty!";
81 | }
82 |
83 | return null;
84 | },
85 | ),
86 | const SizedBox(height: 15),
87 | TextFormField(
88 | controller: otpController,
89 | decoration: InputDecoration(hintText: 'OTP'),
90 | obscureText: true,
91 | validator: (value) {
92 | if (value != null && value.trim().isEmpty) {
93 | return "Field cannot be empty!";
94 | }
95 |
96 | return null;
97 | },
98 | ),
99 | const SizedBox(height: 20),
100 | ElevatedButton(
101 | onPressed: confirmSignUp,
102 | child: Text(
103 | 'CONFIRM',
104 | style: TextStyle(fontSize: 16, color: Colors.white),
105 | ),
106 | ),
107 | ],
108 | ),
109 | ),
110 | );
111 | },
112 | ),
113 | );
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/auth/login_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/cubits/auth/auth_cubit.dart';
4 | import 'package:flutter_client/pages/auth/signup_page.dart';
5 | import 'package:flutter_client/services/auth_service.dart';
6 | import 'package:flutter_client/utils/utils.dart';
7 |
8 | class LoginPage extends StatefulWidget {
9 | static route() => MaterialPageRoute(builder: (context) => LoginPage());
10 | const LoginPage({super.key});
11 |
12 | @override
13 | State createState() => _LoginPageState();
14 | }
15 |
16 | class _LoginPageState extends State {
17 | final emailController = TextEditingController();
18 | final passwordController = TextEditingController();
19 | final formKey = GlobalKey();
20 | final AuthService authService = AuthService();
21 |
22 | @override
23 | void dispose() {
24 | passwordController.dispose();
25 | emailController.dispose();
26 | super.dispose();
27 | }
28 |
29 | void login() async {
30 | if (formKey.currentState!.validate()) {
31 | context.read().loginUser(
32 | email: emailController.text.trim(),
33 | password: passwordController.text.trim(),
34 | );
35 | }
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return Scaffold(
41 | body: BlocConsumer(
42 | listener: (context, state) {
43 | if (state is AuthLoginSuccess) {
44 | showSnackBar(state.message, context);
45 | // TODO: Navigate to home page
46 | } else if (state is AuthError) {
47 | showSnackBar(state.error, context);
48 | }
49 | },
50 | builder: (context, state) {
51 | if (state is AuthLoading) {
52 | return Center(child: CircularProgressIndicator.adaptive());
53 | }
54 | return Padding(
55 | padding: const EdgeInsets.all(15.0),
56 | child: Form(
57 | key: formKey,
58 | child: Column(
59 | mainAxisAlignment: MainAxisAlignment.center,
60 | children: [
61 | Text(
62 | 'Sign in.',
63 | style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
64 | ),
65 | const SizedBox(height: 30),
66 | TextFormField(
67 | controller: emailController,
68 | decoration: InputDecoration(hintText: 'Email'),
69 | validator: (value) {
70 | if (value != null && value.trim().isEmpty) {
71 | return "Field cannot be empty!";
72 | }
73 |
74 | return null;
75 | },
76 | ),
77 | const SizedBox(height: 15),
78 | TextFormField(
79 | controller: passwordController,
80 | decoration: InputDecoration(hintText: 'Password'),
81 | obscureText: true,
82 | validator: (value) {
83 | if (value != null && value.trim().isEmpty) {
84 | return "Field cannot be empty!";
85 | }
86 |
87 | return null;
88 | },
89 | ),
90 | const SizedBox(height: 20),
91 | ElevatedButton(
92 | onPressed: login,
93 | child: Text(
94 | 'SIGN IN',
95 | style: TextStyle(fontSize: 16, color: Colors.white),
96 | ),
97 | ),
98 | const SizedBox(height: 20),
99 | GestureDetector(
100 | onTap: () {
101 | Navigator.of(context).push(SignupPage.route());
102 | },
103 | child: RichText(
104 | text: TextSpan(
105 | text: 'Don\'t have an account? ',
106 | style: Theme.of(context).textTheme.titleMedium,
107 | children: [
108 | TextSpan(
109 | text: 'Sign up',
110 | style: Theme.of(context).textTheme.titleMedium
111 | ?.copyWith(fontWeight: FontWeight.bold),
112 | ),
113 | ],
114 | ),
115 | ),
116 | ),
117 | ],
118 | ),
119 | ),
120 | );
121 | },
122 | ),
123 | );
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/auth/signup_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_client/cubits/auth/auth_cubit.dart';
4 | import 'package:flutter_client/pages/auth/confirm_signup_page.dart';
5 | import 'package:flutter_client/pages/auth/login_page.dart';
6 | import 'package:flutter_client/services/auth_service.dart';
7 | import 'package:flutter_client/utils/utils.dart';
8 |
9 | class SignupPage extends StatefulWidget {
10 | static route() => MaterialPageRoute(builder: (context) => SignupPage());
11 | const SignupPage({super.key});
12 |
13 | @override
14 | State createState() => _SignupPageState();
15 | }
16 |
17 | class _SignupPageState extends State {
18 | final nameController = TextEditingController();
19 | final emailController = TextEditingController();
20 | final passwordController = TextEditingController();
21 | final formKey = GlobalKey();
22 |
23 | @override
24 | void dispose() {
25 | nameController.dispose();
26 | passwordController.dispose();
27 | emailController.dispose();
28 | super.dispose();
29 | }
30 |
31 | void signUp() async {
32 | if (formKey.currentState!.validate()) {
33 | context.read().signUpUser(
34 | name: nameController.text.trim(),
35 | email: emailController.text.trim(),
36 | password: passwordController.text.trim(),
37 | );
38 | }
39 | }
40 |
41 | @override
42 | Widget build(BuildContext context) {
43 | return Scaffold(
44 | body: BlocConsumer(
45 | listener: (context, state) {
46 | if (state is AuthSignupSuccess) {
47 | showSnackBar(state.message, context);
48 | Navigator.push(
49 | context,
50 | ConfirmSignupPage.route(emailController.text.trim()),
51 | );
52 | } else if (state is AuthError) {
53 | showSnackBar(state.error, context);
54 | }
55 | },
56 | builder: (context, state) {
57 | if (state is AuthLoading) {
58 | return Center(child: CircularProgressIndicator.adaptive());
59 | }
60 | return Padding(
61 | padding: const EdgeInsets.all(15.0),
62 | child: Form(
63 | key: formKey,
64 | child: Column(
65 | mainAxisAlignment: MainAxisAlignment.center,
66 | children: [
67 | Text(
68 | 'Sign Up',
69 | style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
70 | ),
71 | const SizedBox(height: 30),
72 | TextFormField(
73 | controller: nameController,
74 | decoration: InputDecoration(hintText: 'Name'),
75 | validator: (value) {
76 | if (value != null && value.trim().isEmpty) {
77 | return "Field cannot be empty!";
78 | }
79 |
80 | return null;
81 | },
82 | ),
83 | const SizedBox(height: 15),
84 | TextFormField(
85 | controller: emailController,
86 | decoration: InputDecoration(hintText: 'Email'),
87 | validator: (value) {
88 | if (value != null && value.trim().isEmpty) {
89 | return "Field cannot be empty!";
90 | }
91 |
92 | return null;
93 | },
94 | ),
95 | const SizedBox(height: 15),
96 | TextFormField(
97 | controller: passwordController,
98 | decoration: InputDecoration(hintText: 'Password'),
99 | obscureText: true,
100 | validator: (value) {
101 | if (value != null && value.trim().isEmpty) {
102 | return "Field cannot be empty!";
103 | }
104 |
105 | return null;
106 | },
107 | ),
108 | const SizedBox(height: 20),
109 | ElevatedButton(
110 | onPressed: signUp,
111 | child: Text(
112 | 'SIGN UP',
113 | style: TextStyle(fontSize: 16, color: Colors.white),
114 | ),
115 | ),
116 | const SizedBox(height: 20),
117 | GestureDetector(
118 | onTap: () {
119 | Navigator.of(context).push(LoginPage.route());
120 | },
121 | child: RichText(
122 | text: TextSpan(
123 | text: 'Already have an account? ',
124 | style: Theme.of(context).textTheme.titleMedium,
125 | children: [
126 | TextSpan(
127 | text: 'Sign In',
128 | style: Theme.of(context).textTheme.titleMedium
129 | ?.copyWith(fontWeight: FontWeight.bold),
130 | ),
131 | ],
132 | ),
133 | ),
134 | ),
135 | ],
136 | ),
137 | ),
138 | );
139 | },
140 | ),
141 | );
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/home/home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_client/pages/home/upload_page.dart';
3 | import 'package:flutter_client/pages/home/video_player_page.dart';
4 | import 'package:flutter_client/services/video_service.dart';
5 |
6 | class HomePage extends StatefulWidget {
7 | const HomePage({super.key});
8 |
9 | @override
10 | State createState() => _HomePageState();
11 | }
12 |
13 | class _HomePageState extends State {
14 | final videosFuture = VideoService().getVideos();
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Scaffold(
19 | appBar: AppBar(
20 | title: Text('Video Stream'),
21 | actions: [
22 | IconButton(
23 | onPressed: () {
24 | Navigator.push(context, UploadPage.route());
25 | },
26 | icon: Icon(Icons.add),
27 | ),
28 | ],
29 | ),
30 | body: FutureBuilder(
31 | future: videosFuture,
32 | builder: (context, snapshot) {
33 | if (snapshot.connectionState == ConnectionState.waiting) {
34 | return Center(child: CircularProgressIndicator.adaptive());
35 | }
36 | if (snapshot.hasError) {
37 | return Center(child: Text(snapshot.error.toString()));
38 | }
39 | final videos = snapshot.data!;
40 |
41 | return ListView.builder(
42 | itemCount: videos.length,
43 | itemBuilder: (context, index) {
44 | final video = videos[index];
45 | final thumbnail =
46 | "https://d1unjxa15f7twa.cloudfront.net/${video['video_s3_key'].replaceAll('.mp4', "").replaceAll("videos/", "thumbnails/")}";
47 |
48 | return GestureDetector(
49 | onTap: () {
50 | Navigator.push(context, VideoPlayerPage.route(video));
51 | },
52 | child: Padding(
53 | padding: const EdgeInsets.all(15.0),
54 | child: Column(
55 | crossAxisAlignment: CrossAxisAlignment.start,
56 | children: [
57 | ClipRRect(
58 | borderRadius: BorderRadius.circular(15),
59 | child: AspectRatio(
60 | aspectRatio: 16 / 9,
61 | child: Image.network(
62 | thumbnail,
63 | fit: BoxFit.cover,
64 | headers: {'Content-Type': 'image/jpg'},
65 | ),
66 | ),
67 | ),
68 | Padding(
69 | padding: const EdgeInsets.symmetric(horizontal: 10),
70 | child: Text(
71 | video['title'],
72 | style: TextStyle(
73 | fontSize: 20,
74 | fontWeight: FontWeight.bold,
75 | ),
76 | ),
77 | ),
78 | ],
79 | ),
80 | ),
81 | );
82 | },
83 | );
84 | },
85 | ),
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/home/upload_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:dotted_border/dotted_border.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_bloc/flutter_bloc.dart';
6 | import 'package:flutter_client/cubits/upload_video/upload_video_cubit.dart';
7 | import 'package:flutter_client/utils/utils.dart';
8 |
9 | class UploadPage extends StatefulWidget {
10 | static route() => MaterialPageRoute(builder: (context) => UploadPage());
11 | const UploadPage({super.key});
12 |
13 | @override
14 | State createState() => _UploadPageState();
15 | }
16 |
17 | class _UploadPageState extends State {
18 | final descriptionController = TextEditingController();
19 | final titleController = TextEditingController();
20 | String visibility = 'PRIVATE';
21 | File? imageFile;
22 | File? videoFile;
23 |
24 | @override
25 | void dispose() {
26 | descriptionController.dispose();
27 | titleController.dispose();
28 | super.dispose();
29 | }
30 |
31 | void selectImage() async {
32 | final _imageFile = await pickImage();
33 |
34 | setState(() {
35 | imageFile = _imageFile;
36 | });
37 | }
38 |
39 | void selectVideo() async {
40 | final _videoFile = await pickVideo();
41 |
42 | setState(() {
43 | videoFile = _videoFile;
44 | });
45 | }
46 |
47 | void uploadVideo() async {
48 | if (titleController.text.trim().isNotEmpty &&
49 | descriptionController.text.trim().isNotEmpty &&
50 | videoFile != null &&
51 | imageFile != null) {
52 | await context.read().uploadVideo(
53 | videoFile: videoFile!,
54 | thumbnailFile: imageFile!,
55 | title: titleController.text.trim(),
56 | description: descriptionController.text.trim(),
57 | visibility: visibility,
58 | );
59 | }
60 | }
61 |
62 | @override
63 | Widget build(BuildContext context) {
64 | return Scaffold(
65 | appBar: AppBar(title: Text('Upload Page')),
66 | body: BlocConsumer(
67 | listener: (context, state) {
68 | if (state is UploadVideoSuccess) {
69 | showSnackBar('Video uploaded successfully!', context);
70 | Navigator.pop(context);
71 | } else if (state is UploadVideoError) {
72 | showSnackBar(state.error, context);
73 | }
74 | },
75 | builder: (context, state) {
76 | if (state is UploadVideoLoading) {
77 | return Center(child: CircularProgressIndicator.adaptive());
78 | }
79 | return SingleChildScrollView(
80 | child: Padding(
81 | padding: const EdgeInsets.all(20.0),
82 | child: Column(
83 | children: [
84 | GestureDetector(
85 | onTap: selectImage,
86 | child:
87 | imageFile != null
88 | ? SizedBox(
89 | height: 150,
90 | width: double.infinity,
91 | child: Image.file(imageFile!, fit: BoxFit.cover),
92 | )
93 | : DottedBorder(
94 | dashPattern: [10, 4],
95 | borderType: BorderType.RRect,
96 | strokeCap: StrokeCap.round,
97 | radius: Radius.circular(10),
98 | child: SizedBox(
99 | height: 150,
100 | width: double.infinity,
101 | child: Column(
102 | mainAxisAlignment: MainAxisAlignment.center,
103 | children: [
104 | Icon(Icons.folder_open, size: 40),
105 | Text(
106 | 'Select the thumbnail for your video',
107 | style: TextStyle(fontSize: 15),
108 | ),
109 | ],
110 | ),
111 | ),
112 | ),
113 | ),
114 | const SizedBox(height: 15),
115 | GestureDetector(
116 | onTap: selectVideo,
117 | child:
118 | videoFile != null
119 | ? Text(videoFile!.path)
120 | : DottedBorder(
121 | dashPattern: [10, 4],
122 | borderType: BorderType.RRect,
123 | strokeCap: StrokeCap.round,
124 | radius: Radius.circular(10),
125 | child: SizedBox(
126 | height: 150,
127 | width: double.infinity,
128 | child: Column(
129 | mainAxisAlignment: MainAxisAlignment.center,
130 | children: [
131 | Icon(Icons.video_file_outlined, size: 40),
132 | Text(
133 | 'Select your video file',
134 | style: TextStyle(fontSize: 15),
135 | ),
136 | ],
137 | ),
138 | ),
139 | ),
140 | ),
141 | const SizedBox(height: 15),
142 | TextField(
143 | controller: titleController,
144 | decoration: InputDecoration(hintText: 'Title'),
145 | ),
146 | const SizedBox(height: 15),
147 | TextField(
148 | controller: descriptionController,
149 | decoration: InputDecoration(hintText: 'Description'),
150 | maxLines: null,
151 | ),
152 | const SizedBox(height: 15),
153 | Container(
154 | width: double.infinity,
155 | decoration: BoxDecoration(
156 | border: Border.all(color: Colors.grey.shade300, width: 3),
157 | borderRadius: BorderRadius.circular(5),
158 | ),
159 | child: DropdownButton(
160 | value: visibility,
161 | padding: EdgeInsets.all(15),
162 | underline: SizedBox(),
163 | items:
164 | ['PUBLIC', 'PRIVATE', 'UNLISTED']
165 | .map(
166 | (elem) => DropdownMenuItem(
167 | value: elem,
168 | child: Text(elem),
169 | ),
170 | )
171 | .toList(),
172 | onChanged: (val) {
173 | setState(() {
174 | visibility = val!;
175 | });
176 | },
177 | ),
178 | ),
179 | SizedBox(height: 15),
180 | ElevatedButton(
181 | onPressed: uploadVideo,
182 | child: Text(
183 | 'UPLOAD',
184 | style: TextStyle(fontSize: 16, color: Colors.white),
185 | ),
186 | ),
187 | ],
188 | ),
189 | ),
190 | );
191 | },
192 | ),
193 | );
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/flutter_client/lib/pages/home/video_player_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:better_player/better_player.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class VideoPlayerPage extends StatefulWidget {
5 | static route(Map video) =>
6 | MaterialPageRoute(builder: (context) => VideoPlayerPage(video: video));
7 | final Map video;
8 | const VideoPlayerPage({super.key, required this.video});
9 |
10 | @override
11 | State createState() => _VideoPlayerPageState();
12 | }
13 |
14 | class _VideoPlayerPageState extends State {
15 | late BetterPlayerController betterPlayerController;
16 |
17 | @override
18 | void initState() {
19 | super.initState();
20 | betterPlayerController = BetterPlayerController(
21 | BetterPlayerConfiguration(
22 | aspectRatio: 16 / 9,
23 | fit: BoxFit.contain,
24 | autoPlay: true,
25 | controlsConfiguration: BetterPlayerControlsConfiguration(
26 | enableFullscreen: true,
27 | enablePlayPause: true,
28 | enableProgressBar: true,
29 | enablePlaybackSpeed: true,
30 | enableQualities: true,
31 | ),
32 | ),
33 | betterPlayerDataSource: BetterPlayerDataSource.network(
34 | "https://d3iiasefxo4uf9.cloudfront.net/${widget.video['video_s3_key']}/manifest.mpd",
35 | videoFormat: BetterPlayerVideoFormat.dash,
36 | ),
37 | );
38 | }
39 |
40 | @override
41 | void dispose() {
42 | betterPlayerController.dispose();
43 | super.dispose();
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return Scaffold(
49 | body: Column(
50 | crossAxisAlignment: CrossAxisAlignment.start,
51 | children: [
52 | BetterPlayer(controller: betterPlayerController),
53 | Padding(
54 | padding: const EdgeInsets.symmetric(horizontal: 10),
55 | child: Text(
56 | widget.video['title'],
57 | style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
58 | ),
59 | ),
60 | Padding(
61 | padding: const EdgeInsets.all(10.0),
62 | child: Text(widget.video['description']),
63 | ),
64 | ],
65 | ),
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/flutter_client/lib/services/auth_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4 | import 'package:http/http.dart' as http;
5 |
6 | class AuthService {
7 | final backendUrl = "http://35.23.52.233:8000/auth";
8 | final FlutterSecureStorage secureStorage = FlutterSecureStorage();
9 |
10 | Future