Update Ionic tutorial

This commit is contained in:
juancarmore 2024-06-05 13:51:03 +02:00
parent 0833665094
commit 75c4fc0172
57 changed files with 18292 additions and 30373 deletions

View File

@ -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

View File

@ -57,6 +57,8 @@ yarn-error.log
/.angular
/.angular/cache
.sass-cache/
/.nx
/.nx/cache
/connect.lock
/coverage
/libpeerconnection.log

View 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
```

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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
}
}
}

View File

@ -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;

View File

@ -1,7 +1,7 @@
{
"name": "openvidu-ionic",
"name": "basic-ionic",
"integrations": {
"capacitor": {}
},
"type": "angular"
"type": "angular-standalone"
}

View File

@ -141,6 +141,8 @@
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:App.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -0,0 +1 @@
<audio #audioElement [id]="track().sid"></audio>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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>

View File

@ -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 */
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,3 +1,3 @@
export const environment = {
production: true,
production: true
};

View File

@ -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.

View File

@ -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>

View File

@ -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()],
});

View File

@ -41,7 +41,7 @@
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
import './zone-flags';
/***************************************************************************************************

View File

@ -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);
}

View File

@ -18,10 +18,7 @@
"importHelpers": true,
"target": "es2022",
"module": "es2020",
"lib": [
"es2018",
"dom"
],
"lib": ["es2018", "dom"],
"useDefineForClassFields": false
},
"angularCompilerOptions": {

View File

@ -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