Update Ionic tutorial
@ -3,8 +3,9 @@ root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
max_line_length = 120
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
||||
2
application-client/openvidu-ionic/.gitignore
vendored
@ -57,6 +57,8 @@ yarn-error.log
|
||||
/.angular
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/.nx
|
||||
/.nx/cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
|
||||
44
application-client/openvidu-ionic/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Basic Ionic
|
||||
|
||||
Basic client application built with Ionic and Angular. It internally uses [livekit-client-sdk-js](https://docs.livekit.io/client-sdk-js/).
|
||||
|
||||
For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-client/ionic/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node](https://nodejs.org/en/download)
|
||||
|
||||
## Run
|
||||
|
||||
1. Download repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/OpenVidu/openvidu-livekit-tutorials.git
|
||||
cd openvidu-livekit-tutorials/application-client/openvidu-ionic
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the application
|
||||
|
||||
- Browser
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
- Android
|
||||
|
||||
```bash
|
||||
npm run android
|
||||
```
|
||||
|
||||
- iOS
|
||||
|
||||
```bash
|
||||
npm run ios
|
||||
```
|
||||
@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "io.ionic.starter"
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "io.ionic.starter"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
|
||||
@ -1,19 +1,44 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
|
||||
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:exported="true" android:label="@string/title_activity_main" android:launchMode="singleTask" android:name=".MainActivity" android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<provider android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" android:name="androidx.core.content.FileProvider">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">openvidu-ionic</string>
|
||||
<string name="title_activity_main">openvidu-ionic</string>
|
||||
<string name="app_name">basic-ionic</string>
|
||||
<string name="title_activity_main">basic-ionic</string>
|
||||
<string name="package_name">io.ionic.starter</string>
|
||||
<string name="custom_url_scheme">io.ionic.starter</string>
|
||||
</resources>
|
||||
|
||||
@ -7,8 +7,8 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@ -85,9 +85,6 @@ done
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
@ -133,10 +130,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@ -197,6 +197,10 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
androidxActivityVersion = '1.7.0'
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.10.0'
|
||||
androidxFragmentVersion = '1.5.6'
|
||||
coreSplashScreenVersion = '1.0.0'
|
||||
androidxWebkitVersion = '1.6.1'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
"projects": {
|
||||
"app": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"schematics": {
|
||||
"@ionic/angular-toolkit:page": {
|
||||
"styleext": "scss",
|
||||
"standalone": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
@ -24,14 +29,9 @@
|
||||
"glob": "**/*",
|
||||
"input": "src/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
}
|
||||
],
|
||||
"styles": ["src/theme/variables.scss", "src/global.scss"],
|
||||
"styles": ["src/global.scss", "src/theme/variables.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
@ -74,10 +74,10 @@
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "app:build:production"
|
||||
"buildTarget": "app:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "app:build:development"
|
||||
"buildTarget": "app:build:development"
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
@ -88,7 +88,7 @@
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "app:build"
|
||||
"buildTarget": "app:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
@ -104,14 +104,9 @@
|
||||
"glob": "**/*",
|
||||
"input": "src/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
}
|
||||
],
|
||||
"styles": ["src/theme/variables.scss", "src/global.scss"],
|
||||
"styles": ["src/global.scss", "src/theme/variables.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
@ -124,19 +119,14 @@
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@ionic/angular-toolkit"
|
||||
]
|
||||
"schematicCollections": ["@ionic/angular-toolkit"]
|
||||
},
|
||||
"schematics": {
|
||||
"@ionic/angular-toolkit:component": {
|
||||
@ -144,6 +134,12 @@
|
||||
},
|
||||
"@ionic/angular-toolkit:page": {
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@angular-eslint/schematics:application": {
|
||||
"setParserOptionsProject": true
|
||||
},
|
||||
"@angular-eslint/schematics:library": {
|
||||
"setParserOptionsProject": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'io.ionic.starter',
|
||||
appName: 'openvidu-ionic',
|
||||
webDir: 'www',
|
||||
appName: 'basic-ionic',
|
||||
webDir: 'www'
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openvidu-ionic",
|
||||
"name": "basic-ionic",
|
||||
"integrations": {
|
||||
"capacitor": {}
|
||||
},
|
||||
"type": "angular"
|
||||
"type": "angular-standalone"
|
||||
}
|
||||
|
||||
@ -141,6 +141,8 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:App.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:App.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>openvidu-ionic</string>
|
||||
<string>basic-ionic</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -43,11 +43,11 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This Application uses your camera to make video calls.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This Application uses your microphone to make calls.</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -15,8 +15,6 @@ def capacitor_pods
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'JcesarmobileSslSkip', :path => '../../node_modules/@jcesarmobile/ssl-skip'
|
||||
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
PODS:
|
||||
- Capacitor (5.5.1):
|
||||
- CapacitorCordova
|
||||
- CapacitorApp (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorCordova (5.5.1)
|
||||
- CapacitorHaptics (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorKeyboard (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (5.0.6):
|
||||
- Capacitor
|
||||
- CordovaPlugins (5.5.0):
|
||||
- CapacitorCordova
|
||||
- JcesarmobileSslSkip (0.2.0):
|
||||
- Capacitor
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)"
|
||||
- "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
|
||||
- "JcesarmobileSslSkip (from `../../node_modules/@jcesarmobile/ssl-skip`)"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorApp:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorHaptics:
|
||||
:path: "../../node_modules/@capacitor/haptics"
|
||||
CapacitorKeyboard:
|
||||
:path: "../../node_modules/@capacitor/keyboard"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/@capacitor/status-bar"
|
||||
CordovaPlugins:
|
||||
:path: "../capacitor-cordova-ios-plugins"
|
||||
JcesarmobileSslSkip:
|
||||
:path: "../../node_modules/@jcesarmobile/ssl-skip"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: 9da0a2415e3b6098511f8b5ffdb578d91ee79f8f
|
||||
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
||||
CapacitorCordova: e128cc7688c070ca0bfa439898a5f609da8dbcfe
|
||||
CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c
|
||||
CapacitorKeyboard: b978154b024a5f65e044908e37d15b7de58b9d12
|
||||
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309
|
||||
CordovaPlugins: 11d429fd653fe424ac24a56b8060dd5faaae2860
|
||||
JcesarmobileSslSkip: c41af6d8f679d128666d64e4495863ce7b69a764
|
||||
|
||||
PODFILE CHECKSUM: fd9a4c0f2e7480092a032ed884d07b2c39637006
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
46655
application-client/openvidu-ionic/package-lock.json
generated
@ -1,75 +1,65 @@
|
||||
{
|
||||
"name": "openvidu-ionic",
|
||||
"version": "2.27.0",
|
||||
"author": "Ionic Framework",
|
||||
"homepage": "https://ionicframework.com/",
|
||||
"name": "basic-ionic",
|
||||
"version": "1.0.0",
|
||||
"author": "OpenVidu",
|
||||
"scripts": {
|
||||
"start": "npx ionic serve --port=5080 --external -- --disable-host-check",
|
||||
"build": "npx ionic build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"android": "npx ionic cap sync android && npx ionic cap run android",
|
||||
"ios": "npx ionic cap sync ios && npx ionic cap run ios",
|
||||
"sync": "npx ionic capacitor sync",
|
||||
"build:android": "npx ionic capacitor build android --no-open && cd android && ./gradlew assembleDebug && ./gradlew assembleRelease",
|
||||
"copy:android": "cp ./android/app/build/outputs/apk/debug/app-debug.apk /opt/openvidu/android/openvidu-ionic.apk"
|
||||
"start": "ionic serve --port 5080 --external -- --disable-host-check",
|
||||
"build": "ionic build",
|
||||
"sync": "ionic capacitor sync",
|
||||
"android": "ionic capacitor sync android && ionic capacitor run android",
|
||||
"ios": "ionic capacitor sync ios && ionic capacitor run ios"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.0.0",
|
||||
"@angular/common": "^16.0.0",
|
||||
"@angular/compiler": "^16.0.0",
|
||||
"@angular/core": "^16.0.0",
|
||||
"@angular/forms": "^16.0.0",
|
||||
"@angular/platform-browser": "^16.0.0",
|
||||
"@angular/platform-browser-dynamic": "^16.0.0",
|
||||
"@angular/router": "^16.0.0",
|
||||
"@awesome-cordova-plugins/android-permissions": "6.4.0",
|
||||
"@awesome-cordova-plugins/core": "6.4.0",
|
||||
"@capacitor/android": "5.5.1",
|
||||
"@capacitor/app": "5.0.6",
|
||||
"@capacitor/core": "5.5.0",
|
||||
"@capacitor/haptics": "5.0.6",
|
||||
"@capacitor/ios": "5.5.1",
|
||||
"@capacitor/keyboard": "5.0.6",
|
||||
"@capacitor/status-bar": "5.0.6",
|
||||
"@ionic/angular": "^7.0.0",
|
||||
"cordova-plugin-android-permissions": "^1.1.5",
|
||||
"ionicons": "^7.0.0",
|
||||
"livekit-client": "^2.1.0",
|
||||
"@angular/animations": "^17.0.2",
|
||||
"@angular/common": "^17.0.2",
|
||||
"@angular/compiler": "^17.0.2",
|
||||
"@angular/core": "^17.0.2",
|
||||
"@angular/forms": "^17.0.2",
|
||||
"@angular/platform-browser": "^17.0.2",
|
||||
"@angular/platform-browser-dynamic": "^17.0.2",
|
||||
"@angular/router": "^17.0.2",
|
||||
"@capacitor/android": "6.0.0",
|
||||
"@capacitor/app": "6.0.0",
|
||||
"@capacitor/core": "6.0.0",
|
||||
"@capacitor/haptics": "6.0.0",
|
||||
"@capacitor/ios": "6.0.0",
|
||||
"@capacitor/keyboard": "6.0.0",
|
||||
"@capacitor/status-bar": "6.0.0",
|
||||
"@ionic/angular": "^8.0.0",
|
||||
"ionicons": "^7.2.1",
|
||||
"livekit-client": "2.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.0"
|
||||
"zone.js": "~0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.0.0",
|
||||
"@angular-eslint/builder": "^16.0.0",
|
||||
"@angular-eslint/eslint-plugin": "^16.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.0.0",
|
||||
"@angular-eslint/schematics": "^16.0.0",
|
||||
"@angular-eslint/template-parser": "^16.0.0",
|
||||
"@angular/cli": "^16.0.0",
|
||||
"@angular/compiler-cli": "^16.0.0",
|
||||
"@angular/language-service": "^16.0.0",
|
||||
"@capacitor/cli": "5.5.0",
|
||||
"@ionic/angular-toolkit": "^9.0.0",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.3.0",
|
||||
"@typescript-eslint/parser": "5.3.0",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsdoc": "30.7.6",
|
||||
"@angular-devkit/build-angular": "^17.0.0",
|
||||
"@angular-eslint/builder": "^17.0.0",
|
||||
"@angular-eslint/eslint-plugin": "^17.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^17.0.0",
|
||||
"@angular-eslint/schematics": "^17.0.0",
|
||||
"@angular-eslint/template-parser": "^17.0.0",
|
||||
"@angular/cli": "^17.0.0",
|
||||
"@angular/compiler-cli": "^17.0.2",
|
||||
"@angular/language-service": "^17.0.2",
|
||||
"@capacitor/cli": "6.0.0",
|
||||
"@ionic/angular-toolkit": "^11.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.2.1",
|
||||
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||
"jasmine-core": "~4.6.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "~5.0.2"
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"description": "An Ionic project"
|
||||
"description": "Simple video-call application built with Ionic"
|
||||
}
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@ -1,3 +1,103 @@
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar color="dark">
|
||||
<ion-title id="header-title">Basic Ionic</ion-title>
|
||||
<ion-buttons slot="primary">
|
||||
<ion-button
|
||||
href="https://github.com/OpenVidu/openvidu-livekit-tutorials/tree/master/application-client/openvidu-ionic"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="logo-github"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
href="https://livekit-tutorials.openvidu.io/tutorials/application-client/ionic/"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="book"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@if (!room()) {
|
||||
<ion-content [fullscreen]="true" class="ion-padding" id="join">
|
||||
<div id="join-dialog">
|
||||
<h2>Join a Video Room</h2>
|
||||
<ion-list [formGroup]="roomForm">
|
||||
<ion-item>
|
||||
<ion-input
|
||||
formControlName="participantName"
|
||||
label="Participant"
|
||||
labelPlacement="floating"
|
||||
id="participant-name"
|
||||
placeholder="Participant name"
|
||||
type="text"
|
||||
required
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
formControlName="roomName"
|
||||
label="Room"
|
||||
labelPlacement="floating"
|
||||
id="room-name"
|
||||
placeholder="Room name"
|
||||
type="text"
|
||||
required
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ion-button id="join-button" (click)="joinRoom()" [disabled]="!roomForm.valid" expand="block" shape="round" color="primary">
|
||||
Join!
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
<ion-fab-button
|
||||
id="settings-button"
|
||||
[disabled]="!roomForm.valid"
|
||||
(click)="presentSettingsAlert()"
|
||||
size="small"
|
||||
color="dark"
|
||||
>
|
||||
<ion-icon name="settings"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
} @else {
|
||||
<ion-content [scrollEvents]="true" class="ion-padding" id="room">
|
||||
<div id="room-header">
|
||||
<h2 id="room-title">{{ roomForm.value.roomName }}</h2>
|
||||
<ion-button id="leave-room-button" (click)="leaveRoom()" color="danger" class="action-button">
|
||||
Leave Room
|
||||
</ion-button>
|
||||
</div>
|
||||
<div id="layout-container">
|
||||
@if (localTrack()) {
|
||||
<video-component
|
||||
[track]="localTrack()!"
|
||||
[participantIdentity]="roomForm.value.participantName!"
|
||||
[local]="true"
|
||||
></video-component>
|
||||
}
|
||||
@for (remoteTrack of remoteTracksMap().values(); track remoteTrack.trackPublication.trackSid) {
|
||||
@if (remoteTrack.trackPublication.kind === 'video') {
|
||||
<video-component
|
||||
[track]="remoteTrack.trackPublication.videoTrack!"
|
||||
[participantIdentity]="remoteTrack.participantIdentity"
|
||||
></video-component>
|
||||
} @else {
|
||||
<audio-component [track]="remoteTrack.trackPublication.audioTrack!" hidden></audio-component>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ion-content>
|
||||
}
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar color="dark">
|
||||
<ion-title id="footer-title" slot="start">Made with love by <span>OpenVidu Team</span></ion-title>
|
||||
<a slot="end" href="http://www.openvidu.io/" target="_blank">
|
||||
<img id="openvidu-logo" src="assets/images/openvidu_logo.png" alt="OpenVidu logo" />
|
||||
</a>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</ion-app>
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
#header-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#join-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#room-title {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
#footer-title {
|
||||
font-size: 1rem;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
#openvidu-logo {
|
||||
height: 35px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
@ -1,10 +1,251 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import {
|
||||
AlertController,
|
||||
IonApp,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonFab,
|
||||
IonFabButton,
|
||||
IonFooter,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonInput,
|
||||
IonItem,
|
||||
IonList,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
Platform,
|
||||
} from '@ionic/angular/standalone';
|
||||
import {
|
||||
LocalVideoTrack,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from 'livekit-client';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { VideoComponent } from './video/video.component';
|
||||
import { AudioComponent } from './audio/audio.component';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { logoGithub, book, settings } from 'ionicons/icons';
|
||||
|
||||
type TrackInfo = {
|
||||
trackPublication: RemoteTrackPublication;
|
||||
participantIdentity: string;
|
||||
};
|
||||
|
||||
// For local development launching app in web browser, leave these variables empty
|
||||
// For production or when launching app in device, configure them with correct URLs
|
||||
var APPLICATION_SERVER_URL = '';
|
||||
var LIVEKIT_URL = '';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.scss',
|
||||
standalone: true,
|
||||
imports: [
|
||||
IonApp,
|
||||
VideoComponent,
|
||||
AudioComponent,
|
||||
ReactiveFormsModule,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonFab,
|
||||
IonFabButton,
|
||||
IonIcon,
|
||||
IonContent,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonInput,
|
||||
IonFooter,
|
||||
],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() {}
|
||||
roomForm = new FormGroup({
|
||||
roomName: new FormControl('Test Room', Validators.required),
|
||||
participantName: new FormControl('Participant' + Math.floor(Math.random() * 100), Validators.required),
|
||||
});
|
||||
|
||||
room = signal<Room | undefined>(undefined);
|
||||
localTrack = signal<LocalVideoTrack | undefined>(undefined);
|
||||
remoteTracksMap = signal<Map<string, TrackInfo>>(new Map());
|
||||
|
||||
constructor(private httpClient: HttpClient, private platform: Platform, private alertController: AlertController) {
|
||||
this.configureUrls();
|
||||
addIcons({
|
||||
logoGithub,
|
||||
book,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
||||
configureUrls() {
|
||||
const deviceMode = this.platform.is('hybrid');
|
||||
|
||||
// If APPLICATION_SERVER_URL is not configured and app is not launched in device mode,
|
||||
// use default value from local development
|
||||
if (!APPLICATION_SERVER_URL) {
|
||||
if (deviceMode) {
|
||||
APPLICATION_SERVER_URL = 'https://{YOUR-LAN-IP}.openvidu-local.dev:6443/';
|
||||
} else {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
APPLICATION_SERVER_URL = 'http://localhost:6080/';
|
||||
} else {
|
||||
APPLICATION_SERVER_URL = 'https://' + window.location.hostname + ':6443/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If LIVEKIT_URL is not configured and app is not launched in device mode,
|
||||
// use default value from local development
|
||||
if (!LIVEKIT_URL) {
|
||||
if (deviceMode) {
|
||||
LIVEKIT_URL = 'wss://{YOUR-LAN-IP}.openvidu-local.dev:7443/';
|
||||
} else {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
LIVEKIT_URL = 'ws://localhost:7880/';
|
||||
} else {
|
||||
LIVEKIT_URL = 'wss://' + window.location.hostname + ':7443/';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows to change the LiveKit URL and the application server URL
|
||||
* from the application itself. This is useful for development purposes.
|
||||
*/
|
||||
async presentSettingsAlert() {
|
||||
const alert = await this.alertController.create({
|
||||
header: 'Configure URLs',
|
||||
inputs: [
|
||||
{
|
||||
name: 'serverUrl',
|
||||
type: 'text',
|
||||
value: APPLICATION_SERVER_URL,
|
||||
placeholder: 'Application Server URL',
|
||||
id: 'server-url-input',
|
||||
},
|
||||
{
|
||||
name: 'livekitUrl',
|
||||
type: 'text',
|
||||
value: LIVEKIT_URL,
|
||||
placeholder: 'LiveKit URL',
|
||||
id: 'livekit-url-input',
|
||||
},
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
id: 'cancel-btn',
|
||||
cssClass: 'secondary',
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
id: 'ok-btn',
|
||||
handler: (data) => {
|
||||
APPLICATION_SERVER_URL = data.serverUrl;
|
||||
LIVEKIT_URL = data.livekitUrl;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
async joinRoom() {
|
||||
// Initialize a new Room object
|
||||
const room = new Room();
|
||||
this.room.set(room);
|
||||
|
||||
// Specify the actions when events take place in the room
|
||||
// On every new Track received...
|
||||
room.on(
|
||||
RoomEvent.TrackSubscribed,
|
||||
(_track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
|
||||
this.remoteTracksMap.update((map) => {
|
||||
map.set(publication.trackSid, {
|
||||
trackPublication: publication,
|
||||
participantIdentity: participant.identity,
|
||||
});
|
||||
return map;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// On every new Track destroyed...
|
||||
room.on(RoomEvent.TrackUnsubscribed, (_track: RemoteTrack, publication: RemoteTrackPublication) => {
|
||||
this.remoteTracksMap.update((map) => {
|
||||
map.delete(publication.trackSid);
|
||||
return map;
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
// Get the room name and participant name from the form
|
||||
const roomName = this.roomForm.value.roomName!;
|
||||
const participantName = this.roomForm.value.participantName!;
|
||||
|
||||
// Get a token from your application server with the room name and participant name
|
||||
const token = await this.getToken(roomName, participantName);
|
||||
|
||||
// Connect to the room with the LiveKit URL and the token
|
||||
await room.connect(LIVEKIT_URL, token);
|
||||
|
||||
// Publish your camera and microphone
|
||||
await room.localParticipant.enableCameraAndMicrophone();
|
||||
this.localTrack.set(room.localParticipant.videoTrackPublications.values().next().value.videoTrack);
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
'There was an error connecting to the room:',
|
||||
error?.error?.errorMessage || error?.message || error
|
||||
);
|
||||
await this.leaveRoom();
|
||||
}
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
// Leave the room by calling 'disconnect' method over the Room object
|
||||
await this.room()?.disconnect();
|
||||
|
||||
// Reset all variables
|
||||
this.room.set(undefined);
|
||||
this.localTrack.set(undefined);
|
||||
this.remoteTracksMap.set(new Map());
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
// On window closed or component destroyed, leave the room
|
||||
await this.leaveRoom();
|
||||
}
|
||||
|
||||
/**
|
||||
* --------------------------------------------
|
||||
* GETTING A TOKEN FROM YOUR APPLICATION SERVER
|
||||
* --------------------------------------------
|
||||
* The method below request the creation of a token to
|
||||
* your application server. This prevents the need to expose
|
||||
* your LiveKit API key and secret to the client side.
|
||||
*
|
||||
* In this sample code, there is no user control at all. Anybody could
|
||||
* access your application server endpoints. In a real production
|
||||
* environment, your application server must identify the user to allow
|
||||
* access to the endpoints.
|
||||
*/
|
||||
async getToken(roomName: string, participantName: string): Promise<string> {
|
||||
const response = await lastValueFrom(
|
||||
this.httpClient.post<{ token: string }>(APPLICATION_SERVER_URL + 'token', { roomName, participantName })
|
||||
);
|
||||
return response.token;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouteReuseStrategy } from '@angular/router';
|
||||
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
|
||||
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@ -0,0 +1 @@
|
||||
<audio #audioElement [id]="track().sid"></audio>
|
||||
@ -0,0 +1,25 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnDestroy, input, viewChild } from '@angular/core';
|
||||
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
|
||||
|
||||
@Component({
|
||||
selector: 'audio-component',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './audio.component.html',
|
||||
styleUrl: './audio.component.scss',
|
||||
})
|
||||
export class AudioComponent implements AfterViewInit, OnDestroy {
|
||||
audioElement = viewChild<ElementRef<HTMLAudioElement>>('audioElement');
|
||||
|
||||
track = input.required<LocalAudioTrack | RemoteAudioTrack>();
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.audioElement()) {
|
||||
this.track().attach(this.audioElement()!.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.track().detach();
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
|
||||
|
||||
@Component({
|
||||
selector: 'ov-audio',
|
||||
template: '<audio #audioElement style="width: 100%"></audio>',
|
||||
})
|
||||
export class OpenViduAudioComponent implements AfterViewInit {
|
||||
@ViewChild('audioElement') elementRef!: ElementRef;
|
||||
|
||||
_track!: LocalAudioTrack | RemoteAudioTrack;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.updateVideoView();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set track(track: LocalAudioTrack | RemoteAudioTrack) {
|
||||
this._track = track;
|
||||
if (!!this.elementRef) {
|
||||
this._track.attach(this.elementRef.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
private updateVideoView() {
|
||||
this._track.attach(this.elementRef.nativeElement);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { LocalVideoTrack, RemoteVideoTrack } from 'livekit-client';
|
||||
|
||||
@Component({
|
||||
selector: 'ov-video',
|
||||
template: '<video #videoElement poster="../../assets/images/video-poster.png"></video>',
|
||||
})
|
||||
export class OpenViduVideoComponent implements AfterViewInit {
|
||||
@ViewChild('videoElement') elementRef!: ElementRef<HTMLVideoElement>;
|
||||
|
||||
_track!: LocalVideoTrack | RemoteVideoTrack;
|
||||
|
||||
ngAfterViewInit() {
|
||||
this._track.attach(this.elementRef.nativeElement);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set track(track: LocalVideoTrack | RemoteVideoTrack) {
|
||||
this._track = track;
|
||||
if (!!this.elementRef) {
|
||||
this._track.attach(this.elementRef.nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class HomePageRoutingModule {}
|
||||
@ -1,27 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
import { HomePageRoutingModule } from './home-routing.module';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { OpenViduVideoComponent } from '../components/ov-video.component';
|
||||
import { OpenViduAudioComponent } from '../components/ov-audio.component';
|
||||
import { AndroidPermissions } from '@awesome-cordova-plugins/android-permissions/ngx';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
HomePageRoutingModule,
|
||||
HttpClientModule,
|
||||
],
|
||||
declarations: [HomePage, OpenViduVideoComponent, OpenViduAudioComponent],
|
||||
providers: [
|
||||
AndroidPermissions,
|
||||
],
|
||||
})
|
||||
export class HomePageModule {}
|
||||
@ -1,124 +0,0 @@
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="primary">
|
||||
<ion-button
|
||||
color="dark"
|
||||
href="https://github.com/OpenVidu/openvidu-tutorials/tree/master/openvidu-ionic"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="logo-github"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<!-- Maybe in iOS the content background must be transparent as old project? -->
|
||||
<ion-content *ngIf="!room" [fullscreen]="true" class="ion-padding">
|
||||
<div id="img-div">
|
||||
<img src="assets/images/openvidu_grey_bg_transp_cropped.png" />
|
||||
</div>
|
||||
<h1 align="center" id="title">Join a video room</h1>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="user-input"
|
||||
label="Participant"
|
||||
labelPlacement="floating"
|
||||
[(ngModel)]="myParticipantName"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="room-input"
|
||||
label="Room"
|
||||
labelPlacement="floating"
|
||||
[(ngModel)]="myRoomName"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-button
|
||||
id="join-button"
|
||||
[disabled]="!myRoomName && !myParticipantName"
|
||||
(click)="joinRoom()"
|
||||
expand="block"
|
||||
shape="round"
|
||||
color="primary"
|
||||
>
|
||||
<ion-icon slot="start" name="videocam"></ion-icon>
|
||||
Join
|
||||
</ion-button>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
<ion-fab-button
|
||||
id="settings-button"
|
||||
[disabled]="!myRoomName && !myParticipantName"
|
||||
(click)="presentSettingsAlert()"
|
||||
size="small"
|
||||
color="dark"
|
||||
>
|
||||
<ion-icon name="settings"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
||||
<ion-content *ngIf="room" [scrollEvents]="true" class="transparent" id="room">
|
||||
<div id="room-header">
|
||||
<h1 id="room-name">{{ myRoomName }}</h1>
|
||||
</div>
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col size="12">
|
||||
<div *ngIf="localPublication" class="local-publication publication">
|
||||
<p class="participant-name">
|
||||
{{ getParticipantName(localPublication.trackSid) }}
|
||||
</p>
|
||||
<ov-video
|
||||
*ngIf="localPublication.videoTrack"
|
||||
[track]="localPublication.videoTrack"
|
||||
></ov-video>
|
||||
</div>
|
||||
</ion-col>
|
||||
|
||||
<ion-col
|
||||
size="6"
|
||||
*ngFor="let publication of remotePublications"
|
||||
[ngClass]="{hidden: publication.kind === 'audio'}"
|
||||
class="remote-publication publication"
|
||||
>
|
||||
<p *ngIf="publication.videoTrack" class="participant-name">
|
||||
{{ getParticipantName(publication.trackSid) }}
|
||||
</p>
|
||||
<ov-video
|
||||
*ngIf="publication.videoTrack"
|
||||
[track]="publication.videoTrack"
|
||||
></ov-video>
|
||||
<ov-audio
|
||||
*ngIf="publication.audioTrack"
|
||||
[track]="publication.audioTrack"
|
||||
></ov-audio>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
||||
<ion-fab-button
|
||||
class="action-button"
|
||||
color="light"
|
||||
id="camButton"
|
||||
(click)="swapCamera()"
|
||||
>
|
||||
<ion-icon name="camera-reverse-sharp"></ion-icon>
|
||||
</ion-fab-button>
|
||||
|
||||
<ion-fab-button (click)="toggleMicrophone()" class="action-button">
|
||||
<ion-icon name="{{ microphoneIcon }}"></ion-icon>
|
||||
</ion-fab-button>
|
||||
|
||||
<ion-fab-button (click)="toggleCamera()" class="action-button">
|
||||
<ion-icon name="{{ cameraIcon }}"></ion-icon>
|
||||
</ion-fab-button>
|
||||
|
||||
<ion-fab-button color="danger" (click)="leaveRoom()" class="action-button">
|
||||
<ion-icon name="power"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
@ -1,74 +0,0 @@
|
||||
.demo-logo {
|
||||
height: 26px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#img-div {
|
||||
max-width: 60%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
#title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#join-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
position: absolute;
|
||||
background: #f8f8f8;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.publication {
|
||||
height:200px;
|
||||
box-sizing: border-box;
|
||||
margin: 3px, 3px, 3px, 3px;
|
||||
}
|
||||
|
||||
.local-publication > p {
|
||||
background-color: #d8ffbd;
|
||||
}
|
||||
|
||||
::ng-deep video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Hide the default video controls */
|
||||
::ng-deep video::-webkit-media-controls {
|
||||
display: none; /* For Safari and Chrome */
|
||||
}
|
||||
|
||||
::ng-deep video::-webkit-media-controls-panel {
|
||||
display: none; /* For newer versions of Safari */
|
||||
}
|
||||
|
||||
::ng-deep video::-webkit-media-controls-play-button {
|
||||
display: none; /* For Safari Play button */
|
||||
}
|
||||
|
||||
::ng-deep video::-webkit-media-controls-start-playback-button {
|
||||
display: none; /* For Safari Start Playback button */
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
describe('HomePage', () => {
|
||||
let component: HomePage;
|
||||
let fixture: ComponentFixture<HomePage>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [HomePage],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomePage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,368 +0,0 @@
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import {
|
||||
LocalTrackPublication,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from 'livekit-client';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { AlertController, Platform } from '@ionic/angular';
|
||||
import { lastValueFrom, take } from 'rxjs';
|
||||
import { AndroidPermissions } from '@awesome-cordova-plugins/android-permissions/ngx';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage {
|
||||
// For local development launching app in web browser, leave these variables empty
|
||||
// For production or when launching app in device, configure them with correct URLs
|
||||
APPLICATION_SERVER_URL = '';
|
||||
LIVEKIT_URL = '';
|
||||
|
||||
private IS_DEVICE_DEV_MODE = false;
|
||||
|
||||
// OpenVidu objects
|
||||
room: Room | undefined = undefined;
|
||||
localPublication: LocalTrackPublication | undefined = undefined;
|
||||
remotePublications: RemoteTrackPublication[] = [];
|
||||
// Join form
|
||||
myRoomName!: string;
|
||||
myParticipantName!: string;
|
||||
cameraIcon = 'videocam';
|
||||
microphoneIcon = 'mic';
|
||||
private cameras: MediaDeviceInfo[] = [];
|
||||
private cameraSelected!: MediaDeviceInfo;
|
||||
ANDROID_PERMISSIONS = [
|
||||
this.androidPermissions.PERMISSION.CAMERA,
|
||||
this.androidPermissions.PERMISSION.RECORD_AUDIO,
|
||||
this.androidPermissions.PERMISSION.MODIFY_AUDIO_SETTINGS,
|
||||
];
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private alertController: AlertController,
|
||||
private platform: Platform,
|
||||
private androidPermissions: AndroidPermissions
|
||||
) {
|
||||
this.generateParticipantInfo();
|
||||
}
|
||||
|
||||
@HostListener('window:beforeunload')
|
||||
beforeunloadHandler() {
|
||||
// On window closed leave room
|
||||
this.leaveRoom();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.IS_DEVICE_DEV_MODE = this.platform.is('hybrid');
|
||||
this.generateUrls();
|
||||
|
||||
console.log('IS_DEVICE_DEV_MODE: ', this.IS_DEVICE_DEV_MODE);
|
||||
console.log('APPLICATION_SERVER_URL: ', this.APPLICATION_SERVER_URL);
|
||||
console.log('LIVEKIT_URL: ', this.LIVEKIT_URL);
|
||||
}
|
||||
|
||||
generateUrls() {
|
||||
// If APPLICATION_SERVER_URL is not configured and app is not launched in device dev mode,
|
||||
// use default value from local development
|
||||
if (!this.APPLICATION_SERVER_URL && !this.IS_DEVICE_DEV_MODE) {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
this.APPLICATION_SERVER_URL = 'http://localhost:6080/';
|
||||
} else {
|
||||
this.APPLICATION_SERVER_URL = 'https://' + window.location.hostname + ':6443/';
|
||||
}
|
||||
}
|
||||
|
||||
// If LIVEKIT_URL is not configured and app is not launched in device dev mode,
|
||||
// use default value from local development
|
||||
if (!this.LIVEKIT_URL && !this.IS_DEVICE_DEV_MODE) {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
this.LIVEKIT_URL = 'ws://localhost:7880/';
|
||||
} else {
|
||||
this.LIVEKIT_URL = 'wss://' + window.location.hostname + ':7443/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// On component destroyed leave room
|
||||
this.leaveRoom();
|
||||
}
|
||||
|
||||
async joinRoom() {
|
||||
// --- 1) Get a Room object ---
|
||||
|
||||
this.room = new Room();
|
||||
|
||||
// --- 2) Specify the actions when events take place in the room ---
|
||||
|
||||
// On every new Track received...
|
||||
this.room.on(
|
||||
RoomEvent.TrackSubscribed,
|
||||
(
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant
|
||||
) => {
|
||||
// Store the new publication in remotePublications array
|
||||
this.remotePublications.push(publication);
|
||||
}
|
||||
);
|
||||
|
||||
// On every track destroyed...
|
||||
this.room.on(
|
||||
RoomEvent.TrackUnsubscribed,
|
||||
(track, publication, participant) => {
|
||||
// Remove the publication from 'remotePublications' array
|
||||
this.deleteRemoteTrackPublication(publication);
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
// Get a token from the application backend
|
||||
const token = await this.getToken(
|
||||
this.myRoomName,
|
||||
this.myParticipantName
|
||||
);
|
||||
|
||||
// First param is the LiveKit server URL. Second param is the access token
|
||||
|
||||
await this.room.connect(this.LIVEKIT_URL, token);
|
||||
|
||||
// --- 5) Requesting and Checking Android Permissions
|
||||
if (this.platform.is('hybrid') && this.platform.is('android')) {
|
||||
await this.checkAndroidPermissions();
|
||||
}
|
||||
|
||||
const [audioPublication, videoPublication] = await Promise.all([
|
||||
this.room.localParticipant.setMicrophoneEnabled(true),
|
||||
this.room.localParticipant.setCameraEnabled(true),
|
||||
]);
|
||||
|
||||
// Set the main video in the page to display our webcam and store our localPublication
|
||||
this.localPublication = videoPublication;
|
||||
videoPublication?.track?.on('elementAttached', (track) => {
|
||||
this.refreshVideos();
|
||||
});
|
||||
|
||||
await this.initDevices();
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
'There was an error connecting to the room:',
|
||||
error.code,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
// --- 7) Leave the room by calling 'disconnect' method over the Session object ---
|
||||
|
||||
if (this.room) {
|
||||
await this.room.disconnect();
|
||||
}
|
||||
|
||||
// Empty all properties...
|
||||
this.remotePublications = [];
|
||||
this.localPublication = undefined;
|
||||
this.room = undefined;
|
||||
this.generateParticipantInfo();
|
||||
this.cameraIcon = 'videocam';
|
||||
this.microphoneIcon = 'mic';
|
||||
}
|
||||
|
||||
// Others methods...
|
||||
|
||||
private generateParticipantInfo() {
|
||||
// Random user nickname and room name
|
||||
this.myRoomName = 'RoomA';
|
||||
this.myParticipantName = 'Participant' + Math.floor(Math.random() * 100);
|
||||
}
|
||||
|
||||
private deleteRemoteTrackPublication(
|
||||
publication: RemoteTrackPublication
|
||||
): void {
|
||||
let index = this.remotePublications.findIndex(
|
||||
(p) => p.trackSid === publication.trackSid
|
||||
);
|
||||
if (index > -1) {
|
||||
this.remotePublications.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getParticipantName(trackSid: string) {
|
||||
const isLocalTrack = trackSid === this.localPublication?.trackSid;
|
||||
|
||||
if (isLocalTrack) {
|
||||
// Return local participant name
|
||||
return this.myParticipantName;
|
||||
}
|
||||
|
||||
// Find in remote participants the participant with the track and return his name
|
||||
if (!this.room) return;
|
||||
const remoteParticipant = Array.from(this.room.remoteParticipants.values()).find(
|
||||
(p) => {
|
||||
return p.getTrackPublications().some((t) => t.trackSid === trackSid);
|
||||
}
|
||||
);
|
||||
return remoteParticipant?.identity;
|
||||
}
|
||||
|
||||
async toggleCamera() {
|
||||
if (this.room) {
|
||||
const enabled = !this.room.localParticipant.isCameraEnabled;
|
||||
await this.room.localParticipant.setCameraEnabled(enabled);
|
||||
this.refreshVideos();
|
||||
this.cameraIcon = enabled ? 'videocam' : 'eye-off';
|
||||
}
|
||||
}
|
||||
|
||||
async toggleMicrophone() {
|
||||
if (this.room) {
|
||||
const enabled = !this.room.localParticipant.isMicrophoneEnabled;
|
||||
await this.room.localParticipant.setMicrophoneEnabled(enabled);
|
||||
this.microphoneIcon = enabled ? 'mic' : 'mic-off';
|
||||
}
|
||||
}
|
||||
|
||||
async swapCamera() {
|
||||
try {
|
||||
const newCamera = this.cameras.find(
|
||||
(cam) => cam.deviceId !== this.cameraSelected.deviceId
|
||||
);
|
||||
|
||||
if (newCamera && this.room) {
|
||||
await this.room.switchActiveDevice('videoinput', newCamera.deviceId);
|
||||
this.cameraSelected = newCamera;
|
||||
|
||||
this.refreshVideos();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows to change the LiveKit websocket URL and the application server URL
|
||||
* from the application itself. This is useful for development purposes.
|
||||
*/
|
||||
async presentSettingsAlert() {
|
||||
const alert = await this.alertController.create({
|
||||
header: 'Application server',
|
||||
inputs: [
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
value: this.APPLICATION_SERVER_URL,
|
||||
placeholder: 'URL',
|
||||
id: 'url-input',
|
||||
},
|
||||
{
|
||||
name: 'websocket',
|
||||
type: 'text',
|
||||
value: this.LIVEKIT_URL,
|
||||
placeholder: 'WS URL',
|
||||
id: 'ws-input',
|
||||
},
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
id: 'cancel-btn',
|
||||
cssClass: 'secondary',
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
id: 'ok-btn',
|
||||
handler: (data) => {
|
||||
this.APPLICATION_SERVER_URL = data.url;
|
||||
this.LIVEKIT_URL = data.websocket;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
private refreshVideos() {
|
||||
if (this.platform.is('hybrid') && this.platform.is('android')) {
|
||||
// Workaround for Android devices
|
||||
setTimeout(() => {
|
||||
const refreshedElement = document.getElementById(
|
||||
'refreshed-workaround'
|
||||
);
|
||||
if (refreshedElement) {
|
||||
refreshedElement.remove();
|
||||
} else {
|
||||
const p = document.createElement('p');
|
||||
p.id = 'refreshed-workaround';
|
||||
document.getElementById('room')?.appendChild(p);
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAndroidPermissions(): Promise<void> {
|
||||
await this.platform.ready();
|
||||
try {
|
||||
await this.androidPermissions.requestPermissions(
|
||||
this.ANDROID_PERMISSIONS
|
||||
);
|
||||
const promisesArray: Promise<any>[] = [];
|
||||
this.ANDROID_PERMISSIONS.forEach((permission) => {
|
||||
console.log('Checking ', permission);
|
||||
promisesArray.push(this.androidPermissions.checkPermission(permission));
|
||||
});
|
||||
const responses = await Promise.all(promisesArray);
|
||||
let allHasPermissions = true;
|
||||
responses.forEach((response, i) => {
|
||||
allHasPermissions = response.hasPermission;
|
||||
if (!allHasPermissions) {
|
||||
throw new Error('Permissions denied: ' + this.ANDROID_PERMISSIONS[i]);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error requesting or checking permissions: ', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async initDevices() {
|
||||
this.cameras = await Room.getLocalDevices('videoinput');
|
||||
this.cameraSelected = this.cameras[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* --------------------------------------------
|
||||
* GETTING A TOKEN FROM YOUR APPLICATION SERVER
|
||||
* --------------------------------------------
|
||||
* The methods below request the creation of a Token to
|
||||
* your application server. This keeps your OpenVidu deployment secure.
|
||||
*
|
||||
* In this sample code, there is no user control at all. Anybody could
|
||||
* access your application server endpoints! In a real production
|
||||
* environment, your application server must identify the user to allow
|
||||
* access to the endpoints.
|
||||
*
|
||||
*/
|
||||
async getToken(roomName: string, participantName: string): Promise<any> {
|
||||
const response = this.httpClient
|
||||
.post(
|
||||
this.APPLICATION_SERVER_URL + 'token',
|
||||
{ roomName, participantName },
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
responseType: 'text',
|
||||
}
|
||||
)
|
||||
.pipe(take(1));
|
||||
|
||||
return lastValueFrom(response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
<div [id]="'camera-' + participantIdentity()" class="video-container">
|
||||
<div class="participant-data">
|
||||
<p>{{ participantIdentity() + (local() ? " (You)" : "") }}</p>
|
||||
</div>
|
||||
<video #videoElement [id]="track().sid"></video>
|
||||
</div>
|
||||
@ -0,0 +1,27 @@
|
||||
.video-container {
|
||||
position: relative;
|
||||
background: #3b3b3b;
|
||||
aspect-ratio: 9/16;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container .participant-data {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.participant-data p {
|
||||
background: #f8f8f8;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnDestroy, input, viewChild } from '@angular/core';
|
||||
import { LocalVideoTrack, RemoteVideoTrack } from 'livekit-client';
|
||||
|
||||
@Component({
|
||||
selector: 'video-component',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './video.component.html',
|
||||
styleUrl: './video.component.scss',
|
||||
})
|
||||
export class VideoComponent implements AfterViewInit, OnDestroy {
|
||||
videoElement = viewChild<ElementRef<HTMLVideoElement>>('videoElement');
|
||||
|
||||
track = input.required<LocalVideoTrack | RemoteVideoTrack>();
|
||||
participantIdentity = input.required<string>();
|
||||
local = input(false);
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.videoElement()) {
|
||||
this.track().attach(this.videoElement()!.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.track().detach();
|
||||
}
|
||||
}
|
||||
BIN
application-client/openvidu-ionic/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 930 B |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@ -1,3 +1,3 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
production: true
|
||||
};
|
||||
|
||||
@ -3,5 +3,14 @@
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
production: false
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Basic Ionic</title>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Ionic App</title>
|
||||
<base href="/" />
|
||||
|
||||
<base href="/" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideIonicAngular } from '@ionic/angular/standalone';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { environment } from './environments/environment';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.log(err));
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [provideIonicAngular(), provideHttpClient()],
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import './zone-flags';
|
||||
|
||||
/***************************************************************************************************
|
||||
|
||||
@ -3,242 +3,83 @@
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #428cff;
|
||||
--ion-color-primary-rgb: 66,140,255;
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255,255,255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80,200,255;
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255,255,255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106,100,255;
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255,255,255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47,223,117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0,0,0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255,213,52;
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0,0,0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255,73,97;
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255,255,255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
--ion-color-dark: #f4f5f8;
|
||||
--ion-color-dark-rgb: 244,245,248;
|
||||
--ion-color-dark-contrast: #000000;
|
||||
--ion-color-dark-contrast-rgb: 0,0,0;
|
||||
--ion-color-dark-shade: #d7d8da;
|
||||
--ion-color-dark-tint: #f5f6f9;
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0,0,0;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-color-step-100);
|
||||
--ion-toolbar-background: var(--ion-color-step-150);
|
||||
--ion-toolbar-border-color: var(--ion-color-step-250);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #121212;
|
||||
--ion-background-color-rgb: 18,18,18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-toolbar-background: #1f1f1f;
|
||||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
html {
|
||||
/*
|
||||
* For more information on dynamic font scaling, visit the documentation:
|
||||
* https://ionicframework.com/docs/layout/dynamic-font-scaling
|
||||
*/
|
||||
--ion-dynamic-font: var(--ion-default-dynamic-font);
|
||||
}
|
||||
/*
|
||||
* For more information on dynamic font scaling, visit the documentation:
|
||||
* https://ionicframework.com/docs/layout/dynamic-font-scaling
|
||||
*/
|
||||
--ion-dynamic-font: var(--ion-default-dynamic-font);
|
||||
}
|
||||
|
||||
@ -18,10 +18,7 @@
|
||||
"importHelpers": true,
|
||||
"target": "es2022",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["es2018", "dom"],
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
||||
@ -23,15 +23,17 @@ cd openvidu-livekit-tutorials/application-server/python
|
||||
python -m venv venv
|
||||
```
|
||||
|
||||
- Windows
|
||||
```bash
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
- Windows
|
||||
|
||||
- Linux/macOS
|
||||
```bash
|
||||
. ./venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
|
||||
- Linux/macOS
|
||||
|
||||
```bash
|
||||
. ./venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install dependencies
|
||||
|
||||
|
||||