From 132e21de5b040bdece557a5f9e880d11223584aa Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 19 Jul 2024 18:31:05 +0100 Subject: [PATCH 01/38] [CI] Update some release-related git commands (#467) --- fastlane/Fastfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2d59dc76f..d6cde0494 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -503,8 +503,11 @@ lane :merge_main_to_develop do ensure_git_status_clean end - sh('git checkout main && git pull') - sh('git checkout develop && git pull') + sh('git checkout main') + sh('git pull origin main') + sh('git checkout develop') + sh('git pull origin develop') + sh('git log develop..main') sh('git merge main') sh('git push') end From a48a6b341bab5fd3561e61fdcab0f6ca66bea6da Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Sat, 20 Jul 2024 15:30:29 +0300 Subject: [PATCH 02/38] [Fix]Debug menu call expiration not working as expected (#468) --- DemoApp/Sources/Views/Login/DebugMenu.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DemoApp/Sources/Views/Login/DebugMenu.swift b/DemoApp/Sources/Views/Login/DebugMenu.swift index 248ff1313..bf9232bde 100644 --- a/DemoApp/Sources/Views/Login/DebugMenu.swift +++ b/DemoApp/Sources/Views/Login/DebugMenu.swift @@ -129,7 +129,7 @@ struct DebugMenu: View { currentValue: callExpiration, additionalItems: { customCallExpirationView }, label: "Call Expiration" - ) { _ in self.callExpiration = .custom(10) } + ) { self.callExpiration = $0 } makeMenu( for: [.enabled, .disabled], From 97ee9984e974b26a008ba3ba955739bb6826d9a5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 22 Jul 2024 11:52:39 +0100 Subject: [PATCH 03/38] Resolve StreamVideo.xcframework cocoapods-related paths (#469) --- StreamVideo-XCFramework.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StreamVideo-XCFramework.podspec b/StreamVideo-XCFramework.podspec index 6e053adc1..15387b725 100644 --- a/StreamVideo-XCFramework.podspec +++ b/StreamVideo-XCFramework.podspec @@ -17,10 +17,10 @@ Pod::Spec.new do |spec| spec.module_name = 'StreamVideo' spec.source = { http: "https://github.com/GetStream/stream-video-swift/releases/download/#{spec.version}/#{spec.module_name}.zip" } - spec.preserve_paths = "#{spec.module_name}.xcframework/*" + spec.vendored_frameworks = "#{spec.module_name}.xcframework", 'Frameworks/StreamWebRTC.xcframework' + spec.preserve_paths = "#{spec.module_name}.xcframework/*", 'Frameworks/*' spec.dependency('SwiftProtobuf', '~> 1.18.0') - spec.vendored_frameworks = 'Frameworks/StreamWebRTC.xcframework' spec.prepare_command = <<-CMD mkdir -p Frameworks/ From 562b9ce319cf0146a207c5227665bd34e6374a68 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 22 Jul 2024 15:58:12 +0100 Subject: [PATCH 04/38] Update git fetch-depth on release (#470) --- .github/workflows/publish-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 879681950..96e3080bf 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -18,6 +18,8 @@ jobs: with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 - name: Extract version from branch name (for release branches) if: startsWith(github.event.pull_request.head.ref, 'release/') run: | From 0cb9aaaedb948238c661fb7243aff67e86abdcdd Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 30 Jul 2024 10:53:32 +0100 Subject: [PATCH 05/38] [CI] Comment SDK size on every commit (#472) --- .github/workflows/sdk-size-metrics.yml | 38 +++++++++ .github/workflows/smoke-checks.yml | 1 + .gitignore | 3 + fastlane/Fastfile | 103 +++++++++++++++++++++++++ fastlane/sdk_size_export_options.plist | 33 ++++++++ 5 files changed, 178 insertions(+) create mode 100644 .github/workflows/sdk-size-metrics.yml create mode 100644 fastlane/sdk_size_export_options.plist diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml new file mode 100644 index 000000000..50745c712 --- /dev/null +++ b/.github/workflows/sdk-size-metrics.yml @@ -0,0 +1,38 @@ +name: SDK Size + +on: + pull_request: + + workflow_dispatch: + + push: + branches: + - develop + +env: + HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI + +jobs: + sdk_size: + name: Metrics + runs-on: macos-14 + env: + GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}' + steps: + - name: Install Bot SSH Key + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + + - uses: actions/checkout@v3.1.0 + + - uses: ./.github/actions/bootstrap + + - name: Run SDK Size Metrics + run: bundle exec fastlane show_frameworks_sizes + timeout-minutes: 30 + env: + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index 71cb8b5c8..aa6978831 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -271,6 +271,7 @@ jobs: env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.number }} GITHUB_EVENT: ${{ toJson(github.event) }} - id: get_launch_id run: echo "launch_id=${{env.LAUNCH_ID}}" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index 00577aa41..dbc271087 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ fastlane/screenshots fastlane/test_output fastlane/allurectl fastlane/xcresults +fastlane/metrics recordings *.coverage.txt vendor/bundle/ @@ -92,6 +93,8 @@ derived_data/ spm_cache/ .buildcache buildcache +App Thinning Size Report.txt +app-thinning.plist *.dmg # Stream Video Buddy diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6cde0494..9dcc07c9f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,6 +16,8 @@ video_buddy_port = 5678 app_secret = ENV.fetch('STREAM_VIDEO_SECRET', nil) derived_data_path = 'derived_data' source_packages_path = 'spm_cache' +metrics_git = 'git@github.com:GetStream/apple-internal-metrics.git' +sdk_size_path = "metrics/#{github_repo.split('/').last}-size.json" buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++' gci = ENV['GOOGLE_CLIENT_ID'] || '' reversed_gci = gci.split('.').reverse.join('.') @@ -24,6 +26,11 @@ is_localhost = !is_ci swiftformat_excluded_paths = ["**/Generated", "**/generated", "**/protobuf", "**/OpenApi"] swiftformat_source_paths = ["Sources", "DemoApp", "DemoAppUIKit", "StreamVideoTests", "StreamVideoSwiftUITests", "StreamVideoUIKitTests"] +warning_status = '🟡' # Warning if a branch is #{max_tolerance} less performant than the benchmark +fail_status = '🔴' # Failure if a branch is more than #{max_tolerance} less performant than the benchmark +success_status = '🟢' # Success if a branch is more performant or equals to the benchmark +outstanding_status = '🚀' # Outstanding performance + before_all do |lane| if is_ci setup_ci @@ -732,6 +739,7 @@ lane :sources_matrix do swiftui_sample_apps: ['Sources', 'DemoApp', xcode_project], uikit_sample_apps: ['Sources', 'DemoAppUIKit', xcode_project], documentation_tests: ['Sources', 'DocumentationTests', 'docusaurus', xcode_project], + size: ['Sources', xcode_project], ruby: ['fastlane'] } end @@ -746,6 +754,93 @@ lane :copyright do ) end +desc 'Show current frameworks size' +lane :show_frameworks_sizes do |options| + next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check) + + ['metrics/'].each { |dir| FileUtils.remove_dir(dir, force: true) } + + sh("git clone #{metrics_git} #{File.dirname(sdk_size_path)}") + develop_sizes = JSON.parse(File.read(sdk_size_path)) + branch_sizes = options[:sizes] || frameworks_sizes + + table_header = '## SDK Size' + markdown_table = "#{table_header}\n| `title` | `develop` | `branch` | `diff` | `status` |\n| - | - | - | - | - |\n" + sdk_names.each do |title| + benchmark_value = develop_sizes[title] + branch_value = branch_sizes[title.to_sym] + max_tolerance = 0.5 # Max Tolerance is 0.5MB + fine_tolerance = 0.25 # Fine Tolerance is 0.25MB + + diff = (branch_value - benchmark_value).round(2) + + status_emoji = + if diff < 0 + outstanding_status + elsif diff >= max_tolerance + fail_status + elsif diff >= fine_tolerance + warning_status + else + success_status + end + + markdown_table << "|#{title}|#{benchmark_value}MB|#{branch_value}MB|#{diff}MB|#{status_emoji}|\n" + end + + FastlaneCore::PrintTable.print_values(title: 'Benchmark', config: develop_sizes) + FastlaneCore::PrintTable.print_values(title: 'SDK Size', config: branch_sizes) + + if is_ci + if ENV['GITHUB_EVENT_NAME'].to_s == 'push' + File.write(sdk_size_path, JSON.pretty_generate(branch_sizes)) + Dir.chdir(File.dirname(sdk_size_path)) do + if sh('git status -s', log: false).to_s.empty? + UI.important('No changes in SDK sizes benchmarks.') + else + sh('git add -A') + sh("git commit -m 'Update #{sdk_size_path}'") + sh('git push') + end + end + end + + create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: markdown_table, edit_last_comment_with_text: table_header) + end + + UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status) +end + +def frameworks_sizes + root_dir = 'Build/SDKSize' + archive_dir = "#{root_dir}/DemoApp.xcarchive" + + FileUtils.rm_rf("../#{root_dir}/") + + match_me + + gym( + scheme: 'DemoAppUIKit', + archive_path: archive_dir, + export_method: 'ad-hoc', + export_options: 'fastlane/sdk_size_export_options.plist' + ) + + frameworks_path = "../#{archive_dir}/Products/Applications/DemoAppUIKit.app/Frameworks" + stream_video_size = File.size("#{frameworks_path}/StreamVideo.framework/StreamVideo") + stream_video_size_mb = (stream_video_size.to_f / 1024 / 1024).round(2) + stream_video_swiftui_size = File.size("#{frameworks_path}/StreamVideoSwiftUI.framework/StreamVideoSwiftUI") + stream_video_swiftui_size_mb = (stream_video_swiftui_size.to_f / 1024 / 1024).round(2) + stream_video_uikit_size = File.size("#{frameworks_path}/StreamVideoUIKit.framework/StreamVideoUIKit") + stream_video_uikit_size_mb = ((stream_video_uikit_size + stream_video_swiftui_size).to_f / 1024 / 1024).round(2) + + { + StreamVideo: stream_video_size_mb, + StreamVideoSwiftUI: stream_video_swiftui_size_mb, + StreamVideoUIKit: stream_video_uikit_size_mb + } +end + private_lane :create_pr do |options| options[:base_branch] ||= 'develop' sh("git checkout -b #{options[:head_branch]}") @@ -763,6 +858,14 @@ private_lane :create_pr do |options| ) end +private_lane :create_pr_comment do |options| + if is_ci && !options[:pr_num].to_s.empty? + last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-iOS-Bot\")) | last'") + edit_last_comment = last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : '' + sh("gh pr comment #{options[:pr_num]} #{edit_last_comment} -b '#{options[:text]}'") + end +end + private_lane :current_branch do ENV['BRANCH_NAME'].to_s.empty? ? git_branch : ENV.fetch('BRANCH_NAME') end diff --git a/fastlane/sdk_size_export_options.plist b/fastlane/sdk_size_export_options.plist new file mode 100644 index 000000000..6b447de07 --- /dev/null +++ b/fastlane/sdk_size_export_options.plist @@ -0,0 +1,33 @@ + + + + + compileBitcode + + destination + export + method + release-testing + provisioningProfiles + + io.getstream.iOS.VideoDemoApp + match AdHoc io.getstream.iOS.VideoDemoApp + io.getstream.iOS.DemoAppUIKit + match AdHoc io.getstream.iOS.DemoAppUIKit + io.getstream.iOS.VideoDemoApp.CallIntent + match AdHoc io.getstream.iOS.VideoDemoApp.CallIntent + io.getstream.iOS.VideoDemoApp.ScreenSharing + match AdHoc io.getstream.iOS.VideoDemoApp.ScreenSharing + + signingCertificate + Apple Distribution + signingStyle + manual + stripSwiftSymbols + + teamID + EHV7XZLAHA + thinning + iPhone15,2 + + From 7453211702a37d1b04fac85c020e050df8f32e54 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 30 Jul 2024 12:48:28 +0100 Subject: [PATCH 06/38] [CI] Automatically update SDK size badges in README.md (#473) --- README.md | 5 +++-- fastlane/Fastfile | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cfeac14eb..67e34053b 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@

- StreamVideo - StreamVideoSwiftUI + StreamVideo + StreamVideoSwiftUI + StreamVideoUIKit

![Stream Video for iOS Header image](https://github.com/GetStream/stream-video-swift/assets/12433593/e4a44ae5-a8eb-4ac7-8910-28187aa011f6) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9dcc07c9f..a8cf09556 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -59,6 +59,9 @@ lane :release do |options| # Set the framework version in SystemEnvironment+Version.swift new_content = File.read(swift_environment_path).gsub!(previous_version_number, release_version) File.open(swift_environment_path, 'w') { |f| f.puts(new_content) } + + # Update sdk sizes + Dir.chdir('fastlane') { update_img_shields_sdk_sizes } end pod_lint @@ -811,6 +814,31 @@ lane :show_frameworks_sizes do |options| UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status) end +desc 'Update img shields SDK size labels' +lane :update_img_shields_sdk_sizes do + sizes = frameworks_sizes + + # Read the file into a string + readme_path = '../README.md' + readme_content = File.read(readme_path) + + # Define the new value for the badge + stream_video_size = "#{sizes[:StreamVideo]}MB" + stream_video_swiftui_size = "#{sizes[:StreamVideoSwiftUI]}MB" + stream_video_uikit_size = "#{sizes[:StreamVideoUIKit]}MB" + + # Replace the value in the badge URL + readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideo-)(.*?)(-blue)}, "\\1#{stream_video_size}\\3") + readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideoSwiftUI-)(.*?)(-blue)}, "\\1#{stream_video_swiftui_size}\\3") + readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideoUIKit-)(.*?)(-blue)}, "\\1#{stream_video_uikit_size}\\3") + + # Write the updated content back to the file + File.write(readme_path, readme_content) + + # Notify success + UI.success('Successfully updated the SDK size labels in README.md!') +end + def frameworks_sizes root_dir = 'Build/SDKSize' archive_dir = "#{root_dir}/DemoApp.xcarchive" From 5997914b02edbe6aa7401eb3a8eda05ea5d98cd7 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 30 Jul 2024 17:30:58 +0100 Subject: [PATCH 07/38] [CI] Adjust sdk size report on release PRs (#474) --- .github/workflows/sdk-size-metrics.yml | 5 +++++ fastlane/Fastfile | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index 50745c712..e88d63c67 100644 --- a/.github/workflows/sdk-size-metrics.yml +++ b/.github/workflows/sdk-size-metrics.yml @@ -28,10 +28,15 @@ jobs: - uses: ./.github/actions/bootstrap + - name: Get branch name + id: get_branch_name + run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT + - name: Run SDK Size Metrics run: bundle exec fastlane show_frameworks_sizes timeout-minutes: 30 env: + BRANCH_NAME: ${{ steps.get_branch_name.outputs.branch }} GITHUB_PR_NUM: ${{ github.event.pull_request.number }} GITHUB_EVENT_NAME: ${{ github.event_name }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a8cf09556..20160808a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -764,13 +764,16 @@ lane :show_frameworks_sizes do |options| ['metrics/'].each { |dir| FileUtils.remove_dir(dir, force: true) } sh("git clone #{metrics_git} #{File.dirname(sdk_size_path)}") - develop_sizes = JSON.parse(File.read(sdk_size_path)) + is_release = current_branch.include?('release/') + benchmark_config = JSON.parse(File.read(sdk_size_path)) + benchmark_key = is_release ? 'release' : 'develop' + benchmark_sizes = benchmark_config[benchmark_key] branch_sizes = options[:sizes] || frameworks_sizes table_header = '## SDK Size' - markdown_table = "#{table_header}\n| `title` | `develop` | `branch` | `diff` | `status` |\n| - | - | - | - | - |\n" + markdown_table = "#{table_header}\n| `title` | `#{is_release ? 'previous release' : 'develop'}` | `#{is_release ? 'current release' : 'branch'}` | `diff` | `status` |\n| - | - | - | - | - |\n" sdk_names.each do |title| - benchmark_value = develop_sizes[title] + benchmark_value = benchmark_sizes[title] branch_value = branch_sizes[title.to_sym] max_tolerance = 0.5 # Max Tolerance is 0.5MB fine_tolerance = 0.25 # Fine Tolerance is 0.25MB @@ -791,12 +794,13 @@ lane :show_frameworks_sizes do |options| markdown_table << "|#{title}|#{benchmark_value}MB|#{branch_value}MB|#{diff}MB|#{status_emoji}|\n" end - FastlaneCore::PrintTable.print_values(title: 'Benchmark', config: develop_sizes) + FastlaneCore::PrintTable.print_values(title: 'Benchmark', config: benchmark_sizes) FastlaneCore::PrintTable.print_values(title: 'SDK Size', config: branch_sizes) if is_ci - if ENV['GITHUB_EVENT_NAME'].to_s == 'push' - File.write(sdk_size_path, JSON.pretty_generate(branch_sizes)) + if is_release || ENV['GITHUB_EVENT_NAME'].to_s == 'push' + benchmark_config[benchmark_key] = branch_sizes + File.write(sdk_size_path, JSON.pretty_generate(benchmark_config)) Dir.chdir(File.dirname(sdk_size_path)) do if sh('git status -s', log: false).to_s.empty? UI.important('No changes in SDK sizes benchmarks.') From 11f1940c8e2ceeede8778e843dde49feced11525 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Wed, 31 Jul 2024 13:10:03 +0200 Subject: [PATCH 08/38] Update the notification docs (#475) --- docusaurus/docs/iOS/06-advanced/02-push-notifications.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docusaurus/docs/iOS/06-advanced/02-push-notifications.mdx b/docusaurus/docs/iOS/06-advanced/02-push-notifications.mdx index 39b024094..2fe9fa4a9 100644 --- a/docusaurus/docs/iOS/06-advanced/02-push-notifications.mdx +++ b/docusaurus/docs/iOS/06-advanced/02-push-notifications.mdx @@ -5,6 +5,11 @@ description: Push Notifications setup The `StreamVideo` SDK supports two types of push notifications: regular and VoIP notifications. You can use one of them, or both, depending on your use-case. +Push notifications are sent in the following scenarios: +- you create a call with the `ring` value set to true. In this case, a VoIP notification that shows a ringing screen is sent. +- you create a call with the `notify` value set to true. In this case, a regular push notification is sent. +- you haven't answered a call. In this case, a missed call notification is sent (regular push notification). + ### StreamVideo setup The push notification config is provided optionally, when the SDK is initalized. By default, the config uses `apn` as a push provider, for both VoIP and regular push. The push provider name for regular push is "apn", while for VoIP, the name is "voip". From 41b35ef030579115501f4af5dd8acefe448e0419 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Wed, 31 Jul 2024 17:07:50 +0300 Subject: [PATCH 09/38] [Fix]Recording state when failed (#476) --- Sources/StreamVideo/CallState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StreamVideo/CallState.swift b/Sources/StreamVideo/CallState.swift index 9ba5f4f36..2a523034c 100644 --- a/Sources/StreamVideo/CallState.swift +++ b/Sources/StreamVideo/CallState.swift @@ -214,7 +214,7 @@ public class CallState: ObservableObject { case .typeCallHLSBroadcastingFailedEvent: break case .typeCallRecordingFailedEvent: - break + recordingState = .noRecording case .typeCallRecordingReadyEvent: break case .typeClosedCaptionEvent: From 7b863b4c00bc6fb41e48a6b10dad0b2f9ab1d60b Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 31 Jul 2024 18:20:15 +0100 Subject: [PATCH 10/38] [CI] Correctly retrieve a git branch on CI (#477) --- .github/workflows/cron-checks.yml | 11 +++++----- .github/workflows/publish-release.yml | 2 +- .github/workflows/sdk-size-metrics.yml | 8 +------ .github/workflows/smoke-checks.yml | 29 +++++-------------------- .github/workflows/start-new-release.yml | 2 +- .github/workflows/testflight.yml | 9 ++++---- fastlane/Allurefile | 5 ++--- fastlane/Fastfile | 11 ++++++++-- 8 files changed, 29 insertions(+), 48 deletions(-) diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml index e84661c09..d986e5ec5 100644 --- a/.github/workflows/cron-checks.yml +++ b/.github/workflows/cron-checks.yml @@ -12,6 +12,7 @@ concurrency: env: HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: build-test-app-and-frameworks: @@ -58,7 +59,6 @@ jobs: runs-on: ${{ matrix.os }} env: GITHUB_EVENT: ${{ toJson(github.event) }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} XCODE_VERSION: ${{ matrix.xcode }} IOS_SIMULATOR_DEVICE: "${{ matrix.device }} (${{ matrix.ios }})" # For the Allure report @@ -147,7 +147,6 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} XCODE_VERSION: ${{ matrix.xcode }} STREAM_VIDEO_SECRET: ${{ secrets.STREAM_VIDEO_SECRET }} steps: @@ -213,6 +212,10 @@ jobs: env: XCODE_VERSION: "15.0.1" steps: + - name: Connect iOS Bot + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v3.1.0 - uses: ./.github/actions/ruby-cache - name: List Xcode versions @@ -225,10 +228,6 @@ jobs: - name: Build UIKit run: bundle exec fastlane test_uikit device:"iPhone 15" build_for_testing:true timeout-minutes: 25 - - name: Install Bot SSH Key - uses: webfactory/ssh-agent@v0.7.0 - with: - ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - name: Build XCFrameworks run: bundle exec fastlane build_xcframeworks timeout-minutes: 40 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 96e3080bf..93f1ad60f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -13,7 +13,7 @@ jobs: runs-on: macos-13 if: github.event.pull_request.merged == true # only merged pull requests must trigger this job steps: - - name: Install Bot SSH Key + - name: Connect iOS Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index e88d63c67..16e1d1e1c 100644 --- a/.github/workflows/sdk-size-metrics.yml +++ b/.github/workflows/sdk-size-metrics.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}' steps: - - name: Install Bot SSH Key + - name: Connect iOS Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} @@ -28,16 +28,10 @@ jobs: - uses: ./.github/actions/bootstrap - - name: Get branch name - id: get_branch_name - run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - - name: Run SDK Size Metrics run: bundle exec fastlane show_frameworks_sizes timeout-minutes: 30 env: - BRANCH_NAME: ${{ steps.get_branch_name.outputs.branch }} GITHUB_PR_NUM: ${{ github.event.pull_request.number }} - GITHUB_EVENT_NAME: ${{ github.event_name }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index aa6978831..3dd502d91 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -29,6 +29,8 @@ concurrency: env: HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} jobs: test-llc-debug: @@ -36,8 +38,6 @@ jobs: runs-on: macos-14 if: ${{ github.event.inputs.swiftui_snapshots != 'true' && github.event.inputs.uikit_snapshots != 'true' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} STREAM_VIDEO_SECRET: ${{ secrets.STREAM_VIDEO_SECRET }} steps: - uses: actions/checkout@v4.1.1 @@ -53,13 +53,9 @@ jobs: env: XCODE_VERSION: "15.2" # the most stable pair of Xcode IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.2)" # and iOS - - name: Get branch name - id: get_branch_name - run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Run Sonar analysis run: bundle exec fastlane sonar_upload env: - BRANCH_NAME: ${{ steps.get_branch_name.outputs.branch }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - uses: actions/upload-artifact@v4 @@ -97,7 +93,6 @@ jobs: if: ${{ github.event_name != 'push' && github.event.inputs.swiftui_snapshots != 'false' }} env: GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} # to open a PR - GITHUB_PR_NUM: ${{ github.event.number }} steps: - uses: actions/checkout@v4.1.1 - uses: ./.github/actions/bootstrap @@ -129,7 +124,6 @@ jobs: if: ${{ github.event_name != 'push' && github.event.inputs.uikit_snapshots != 'false' }} env: GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} # to open a PR - GITHUB_PR_NUM: ${{ github.event.number }} steps: - uses: actions/checkout@v4.1.1 - uses: ./.github/actions/bootstrap @@ -161,8 +155,6 @@ jobs: runs-on: macos-13 env: XCODE_VERSION: "15.0.1" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} if: ${{ github.event_name != 'push' && github.event.inputs.swiftui_snapshots != 'true' && github.event.inputs.uikit_snapshots != 'true' }} steps: - uses: actions/checkout@v4.1.1 @@ -186,9 +178,11 @@ jobs: if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }} env: XCODE_VERSION: "15.0.1" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} steps: + - name: Connect iOS Bot + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v3.1.0 - uses: ./.github/actions/ruby-cache - name: List Xcode versions @@ -201,10 +195,6 @@ jobs: - name: Build UIKit run: bundle exec fastlane test_uikit device:"iPhone 15" build_for_testing:true timeout-minutes: 25 - - name: Install Bot SSH Key - uses: webfactory/ssh-agent@v0.7.0 - with: - ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - name: Build XCFrameworks run: bundle exec fastlane build_xcframeworks timeout-minutes: 40 @@ -238,9 +228,6 @@ jobs: runs-on: macos-14 needs: build-test-app-and-frameworks if: ${{ github.event_name != 'push' && github.event.inputs.swiftui_snapshots != 'true' && github.event.inputs.uikit_snapshots != 'true' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} steps: - uses: actions/checkout@v4.1.1 - uses: actions/download-artifact@v4 @@ -270,8 +257,6 @@ jobs: run: bundle exec fastlane allure_launch env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} GITHUB_EVENT: ${{ toJson(github.event) }} - id: get_launch_id run: echo "launch_id=${{env.LAUNCH_ID}}" >> $GITHUB_OUTPUT @@ -286,8 +271,6 @@ jobs: - build-test-app-and-frameworks env: LAUNCH_ID: ${{ needs.allure_testops_launch.outputs.launch_id }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_PR_NUM: ${{ github.event.number }} ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} strategy: matrix: diff --git a/.github/workflows/start-new-release.yml b/.github/workflows/start-new-release.yml index 1f434bee8..e66a1eac5 100644 --- a/.github/workflows/start-new-release.yml +++ b/.github/workflows/start-new-release.yml @@ -13,7 +13,7 @@ jobs: name: Start new release runs-on: macos-14 steps: - - name: Install Bot SSH Key + - name: Connect iOS Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 820f0b501..7b4572c1a 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -1,10 +1,9 @@ name: Test Flight Deploy DemoApp on: - # TODO: commented until `develop` branch is in place - # pull_request: - # branches: - # - 'main' + pull_request: + branches: + - 'main' release: types: [published] @@ -32,7 +31,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PR_NUM: ${{ github.event.number }} steps: - - name: Install Bot SSH Key + - name: Connect iOS Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/fastlane/Allurefile b/fastlane/Allurefile index c40298403..6f7014b45 100755 --- a/fastlane/Allurefile +++ b/fastlane/Allurefile @@ -8,11 +8,10 @@ allure_results_path = 'allure-results' desc 'Upload test results to Allure TestOps' lane :allure_upload do |options| - branch = github_run_details['head_branch'] allure_args = "-e #{allure_url} --project-id #{allure_project_id} --launch-id #{options[:launch_id]}" sh("./xcresults export test_output/DemoApp.xcresult #{allure_results_path} || true") sh("./allurectl launch reopen #{options[:launch_id]} || true") # to prevent allure from uploading results to a closed launch - sh("env BRANCH_NAME='#{branch}' ./allurectl upload #{allure_args} #{allure_results_path} || true") + sh("env BRANCH_NAME='#{current_branch}' ./allurectl upload #{allure_args} #{allure_results_path} || true") UI.success("Check out test results in Allure TestOps: #{allure_url}/launch/#{options[:launch_id]} 🎉") end @@ -54,6 +53,6 @@ def github_run_details return nil unless is_ci github_path = "#{ENV.fetch('GITHUB_API_URL', nil)}/repos/#{ENV.fetch('GITHUB_REPOSITORY', nil)}/actions/runs/#{ENV.fetch('GITHUB_RUN_ID', nil)}" - output = sh(command: "curl -s -H 'authorization: Bearer #{ENV.fetch('GITHUB_TOKEN', nil)}' -X GET -G #{github_path}", log: false) + output = sh(command: "curl -s -H 'authorization: Bearer #{ENV.fetch('GITHUB_TOKEN', nil)}' -X GET -G #{github_path}") JSON.parse(output) end diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 20160808a..2108872a3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -898,6 +898,13 @@ private_lane :create_pr_comment do |options| end end -private_lane :current_branch do - ENV['BRANCH_NAME'].to_s.empty? ? git_branch : ENV.fetch('BRANCH_NAME') +lane :current_branch do + branch = if ENV['GITHUB_PR_NUM'].to_s.empty? + git_branch + else + sh("gh pr view #{ENV.fetch('GITHUB_PR_NUM')} --json headRefName -q .headRefName").strip + end + + UI.important("Current branch: #{branch} 🕊️") + branch end From bb6d8277c2dc87d761277b6425d3adef7d33177e Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 1 Aug 2024 15:25:25 +0100 Subject: [PATCH 11/38] [CI] Automate the merge of the release branches (#478) --- .github/workflows/release-merge.yml | 31 +++++++++++ ...ublish-release.yml => release-publish.yml} | 4 ++ ...tart-new-release.yml => release-start.yml} | 4 ++ fastlane/Fastfile | 51 +++++++++++-------- 4 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/release-merge.yml rename .github/workflows/{publish-release.yml => release-publish.yml} (99%) rename .github/workflows/{start-new-release.yml => release-start.yml} (99%) diff --git a/.github/workflows/release-merge.yml b/.github/workflows/release-merge.yml new file mode 100644 index 000000000..05785d093 --- /dev/null +++ b/.github/workflows/release-merge.yml @@ -0,0 +1,31 @@ +name: "Merge release" + +on: + issue_comment: + types: [created] + + workflow_dispatch: + +jobs: + merge-comment: + name: Merge release to main + runs-on: macos-14 + if: github.event_name == 'workflow_dispatch' || (github.event.issue.pull_request && github.event.issue.state == 'open' && github.event.comment.body == '/merge release') + steps: + - name: Connect iOS Bot + uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + + - uses: ./.github/actions/ruby-cache + + - name: Merge + run: bundle exec fastlane merge_release_to_main author:"$USER_LOGIN" --verbose + env: + GITHUB_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} # A token with the "admin:org" scope to get the list of the team members on GitHub + GITHUB_PR_NUM: ${{ github.event.issue.number }} + USER_LOGIN: ${{ github.event.comment.user.login != null && github.event.comment.user.login || github.event.sender.login }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/release-publish.yml similarity index 99% rename from .github/workflows/publish-release.yml rename to .github/workflows/release-publish.yml index 93f1ad60f..80113e243 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/release-publish.yml @@ -17,16 +17,20 @@ jobs: uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + - uses: actions/checkout@v4.1.1 with: fetch-depth: 0 + - name: Extract version from branch name (for release branches) if: startsWith(github.event.pull_request.head.ref, 'release/') run: | BRANCH_NAME="${{ github.event.pull_request.head.ref }}" VERSION=${BRANCH_NAME#release/} echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + - uses: ./.github/actions/ruby-cache + - name: "Fastlane - Publish Release" if: startsWith(github.event.pull_request.head.ref, 'release/') env: diff --git a/.github/workflows/start-new-release.yml b/.github/workflows/release-start.yml similarity index 99% rename from .github/workflows/start-new-release.yml rename to .github/workflows/release-start.yml index e66a1eac5..a03592c30 100644 --- a/.github/workflows/start-new-release.yml +++ b/.github/workflows/release-start.yml @@ -17,11 +17,15 @@ jobs: uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + - uses: actions/checkout@v4.1.1 with: fetch-depth: 0 # to fetch git tags + - uses: ./.github/actions/ruby-cache + - uses: ./.github/actions/xcode-cache + - name: Create Release PR run: bundle exec fastlane release version:"${{ github.event.inputs.version }}" --verbose env: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2108872a3..6175d8bbe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -34,6 +34,7 @@ outstanding_status = '🚀' # Outstanding performance before_all do |lane| if is_ci setup_ci + sh('git config --global user.name "Stream Bot"') xcversion(version: xcode_version) unless [:publish_release, :allure_launch, :allure_upload, :pod_lint, :stop_e2e_helpers].include?(lane) elsif lane == :test_e2e stop_e2e_helpers @@ -489,37 +490,46 @@ private_lane :build_example_app do |options| ) end -lane :merge_release_to_main do +lane :merge_release_to_main do |options| ensure_git_status_clean - sh('git checkout main') - sh('git pull') - # Grep all remote release branches and ensure there's only one - release_branches = sh(command: 'git branch -a', log: false).delete(' ').split("\n").grep(%r(origin/.*release/)) - UI.user_error!("Expected 1 release branch, found #{release_branches.size}") if release_branches.size != 1 + release_branch = + if is_ci + # This API operation needs the "admin:org" scope. + ios_team = sh('gh api orgs/GetStream/teams/ios-developers/members -q ".[].login"', log: false).split + UI.user_error!("#{options[:author]} is not a member of the iOS Team") unless ios_team.include?(options[:author]) + + current_branch + else + release_branches = sh(command: 'git branch -a', log: false).delete(' ').split("\n").grep(%r(origin/.*release/)) + UI.user_error!("Expected 1 release branch, found #{release_branches.size}") if release_branches.size != 1 + + release_branches.first + end + + UI.user_error!("`#{release_branch}`` branch does not match the release branch pattern: `release/*`") unless release_branch.start_with?('release/') + + sh('git checkout origin/main') + sh('git pull origin main') # Merge release branch to main. For more info, read: https://notion.so/iOS-Branching-Strategy-37c10127dc26493e937769d44b1d6d9a - sh("git merge #{release_branches.first} --ff-only") - UI.user_error!('Not pushing changes') unless prompt(text: 'Will push changes. All looking good?', boolean: true) - sh('git push') - UI.important('Please, wait for the `Publish new release` workflow to pass on GitHub Actions: ' \ - "https://github.com/#{github_repo}/actions/workflows/publish-release.yml") + sh("git merge #{release_branch} --ff-only") + sh('git push origin main') + + comment = "[Publication of the release](https://github.com/#{github_repo}/actions/workflows/release-publish.yml) has been launched 👍" + UI.important(comment) + create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: comment) end lane :merge_main_to_develop do - if is_ci - sh('git reset --hard') - else - ensure_git_status_clean - end - + ensure_git_status_clean sh('git checkout main') sh('git pull origin main') - sh('git checkout develop') + sh('git checkout origin/develop') sh('git pull origin develop') sh('git log develop..main') sh('git merge main') - sh('git push') + sh('git push origin develop') end desc 'Compresses the XCFrameworks into zip files' @@ -594,7 +604,6 @@ private_lane :update_spm do |options| File.open('./Package.swift', 'w') { |file| file << file_data } # Update the repo - sh('git config --global user.name "Stream Bot"') sh('git add -A') sh("git commit -m 'Bump #{version}'") sh('git push') @@ -893,7 +902,7 @@ end private_lane :create_pr_comment do |options| if is_ci && !options[:pr_num].to_s.empty? last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-iOS-Bot\")) | last'") - edit_last_comment = last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : '' + edit_last_comment = options[:edit_last_comment_with_text] && last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : '' sh("gh pr comment #{options[:pr_num]} #{edit_last_comment} -b '#{options[:text]}'") end end From 05d23d525cefeb438911a9ffdba056d78fd9ea3c Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Tue, 6 Aug 2024 10:54:27 +0200 Subject: [PATCH 12/38] Removed clean reconnect from reconnect strategies (#481) --- .../protobuf/sfu/models/models.pb.swift | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift index ea62ab464..66d016a88 100644 --- a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift +++ b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift @@ -567,16 +567,12 @@ enum Stream_Video_Sfu_Models_WebsocketReconnectStrategy: SwiftProtobuf.Enum { /// and establish a new WebSocket connection. case fast // = 2 - /// SDK should drop existing pc instances and creates a fresh WebSocket connection, - /// ensuring a clean state for the reconnection. - case clean // = 3 - /// SDK should obtain new credentials from the coordinator, drops existing pc instances, set a new session_id and initializes /// a completely new WebSocket connection, ensuring a comprehensive reset. - case rejoin // = 4 + case rejoin // = 3 /// SDK should migrate to a new SFU instance - case migrate // = 5 + case migrate // = 4 case UNRECOGNIZED(Int) init() { @@ -588,9 +584,8 @@ enum Stream_Video_Sfu_Models_WebsocketReconnectStrategy: SwiftProtobuf.Enum { case 0: self = .unspecified case 1: self = .disconnect case 2: self = .fast - case 3: self = .clean - case 4: self = .rejoin - case 5: self = .migrate + case 3: self = .rejoin + case 4: self = .migrate default: self = .UNRECOGNIZED(rawValue) } } @@ -600,9 +595,8 @@ enum Stream_Video_Sfu_Models_WebsocketReconnectStrategy: SwiftProtobuf.Enum { case .unspecified: return 0 case .disconnect: return 1 case .fast: return 2 - case .clean: return 3 - case .rejoin: return 4 - case .migrate: return 5 + case .rejoin: return 3 + case .migrate: return 4 case .UNRECOGNIZED(let i): return i } } @@ -617,7 +611,6 @@ extension Stream_Video_Sfu_Models_WebsocketReconnectStrategy: CaseIterable { .unspecified, .disconnect, .fast, - .clean, .rejoin, .migrate, ] @@ -1223,9 +1216,8 @@ extension Stream_Video_Sfu_Models_WebsocketReconnectStrategy: SwiftProtobuf._Pro 0: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_UNSPECIFIED"), 1: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT"), 2: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_FAST"), - 3: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_CLEAN"), - 4: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_REJOIN"), - 5: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_MIGRATE"), + 3: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_REJOIN"), + 4: .same(proto: "WEBSOCKET_RECONNECT_STRATEGY_MIGRATE"), ] } From a4ebfd2fda13f464e98e86849ada9f9ea4afdce8 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 6 Aug 2024 14:01:57 +0100 Subject: [PATCH 13/38] [CI] Update gems (#482) --- Gemfile.lock | 79 +++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cfc99e2d3..0ec3d13ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,7 @@ GEM base64 nkf rexml - activesupport (7.1.3.3) + activesupport (7.1.3.4) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -15,8 +15,8 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) @@ -24,20 +24,20 @@ GEM ast (2.4.2) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.937.0) - aws-sdk-core (3.196.1) + aws-partitions (1.962.0) + aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.82.0) - aws-sdk-core (~> 3, >= 3.193.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.157.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) badge (0.13.0) @@ -94,11 +94,11 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.3) connection_pool (2.4.1) cork (0.3.0) colored2 (~> 3.1) - danger (9.4.3) + danger (9.5.0) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) @@ -108,7 +108,6 @@ GEM git (~> 1.13) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) - no_proxy_fix octokit (>= 4.0) terminal-table (>= 1, < 4) danger-commit_lint (0.0.7) @@ -125,7 +124,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.110.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -149,7 +148,7 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) @@ -157,7 +156,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.220.0) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -206,7 +205,7 @@ GEM fastlane-plugin-stream_actions (0.3.38) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) - ffi (1.16.3) + ffi (1.17.0) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) @@ -229,7 +228,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -250,24 +249,24 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) - jazzy (0.15.0) + jazzy (0.15.1) cocoapods (~> 1.5) mustache (~> 1.1) open4 (~> 1.3) redcarpet (~> 3.4) - rexml (~> 3.2) + rexml (>= 3.2.7, < 4.0) rouge (>= 2.0.6, < 5.0) sassc (~> 2.1) sqlite3 (~> 1.3) xcinvoke (~> 0.3.0) jmespath (1.6.2) json (2.7.2) - jwt (2.8.1) + jwt (2.8.2) base64 kramdown (2.4.0) rexml @@ -275,15 +274,15 @@ GEM kramdown (~> 2.0) liferaft (0.0.6) method_source (1.1.0) - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.6) - minitest (5.23.1) + mini_portile2 (2.8.7) + minitest (5.24.1) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) mustache (1.1.1) - mustermann (3.0.0) + mustermann (3.0.1) ruby2_keywords (~> 0.0.1) mutex_m (0.2.0) nanaimo (0.3.0) @@ -292,19 +291,17 @@ GEM netrc (0.11.0) nio4r (2.7.3) nkf (0.2.0) - no_proxy_fix (0.1.2) - nokogiri (1.16.5) + nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - octokit (8.1.0) - base64 + octokit (9.1.0) faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) optparse (0.5.0) os (1.1.4) - parallel (1.24.0) - parser (3.3.2.0) + parallel (1.25.1) + parser (3.3.4.0) ast (~> 2.4.1) racc plist (3.7.1) @@ -314,8 +311,8 @@ GEM public_suffix (4.0.7) puma (6.4.2) nio4r (~> 2.0) - racc (1.8.0) - rack (3.0.11) + racc (1.8.1) + rack (3.1.7) rack-protection (4.0.0) base64 (>= 0.1.0) rack (>= 3.0.0, < 4) @@ -334,8 +331,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.2.9) + strscan rouge (2.0.7) rubocop (1.38.0) json (~> 2.3) @@ -347,7 +344,7 @@ GEM rubocop-ast (>= 1.23.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.31.3) + rubocop-ast (1.32.0) parser (>= 3.3.1.0) rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) @@ -378,7 +375,7 @@ GEM rack-protection (= 4.0.0) rack-session (>= 2.0.0, < 3) tilt (~> 2.0) - slather (2.8.0) + slather (2.8.3) CFPropertyList (>= 2.2, < 4) activesupport clamp (~> 1.3) @@ -390,7 +387,7 @@ GEM terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - tilt (2.3.0) + tilt (2.4.0) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) From 8ebe1f07275fbe9daa0f7d90cda8dbd3e0474878 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Thu, 8 Aug 2024 11:50:28 +0200 Subject: [PATCH 14/38] Remove unused code (#483) --- .../OpenApi/generated/Extensions.swift | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/Sources/StreamVideo/OpenApi/generated/Extensions.swift b/Sources/StreamVideo/OpenApi/generated/Extensions.swift index 557638320..fd01f2647 100644 --- a/Sources/StreamVideo/OpenApi/generated/Extensions.swift +++ b/Sources/StreamVideo/OpenApi/generated/Extensions.swift @@ -120,102 +120,3 @@ extension String: CodingKey { } } - -extension KeyedEncodingContainerProtocol { - - public mutating func encodeArray(_ values: [T], forKey key: Self.Key) throws where T: Encodable { - var arrayContainer = nestedUnkeyedContainer(forKey: key) - try arrayContainer.encode(contentsOf: values) - } - - public mutating func encodeArrayIfPresent(_ values: [T]?, forKey key: Self.Key) throws where T: Encodable { - if let values = values { - try encodeArray(values, forKey: key) - } - } - - public mutating func encodeMap(_ pairs: [Self.Key: T]) throws where T: Encodable { - for (key, value) in pairs { - try encode(value, forKey: key) - } - } - - public mutating func encodeMapIfPresent(_ pairs: [Self.Key: T]?) throws where T: Encodable { - if let pairs = pairs { - try encodeMap(pairs) - } - } - - public mutating func encode(_ value: Decimal, forKey key: Self.Key) throws { - var mutableValue = value - let stringValue = NSDecimalString(&mutableValue, Locale(identifier: "en_US")) - try encode(stringValue, forKey: key) - } - - public mutating func encodeIfPresent(_ value: Decimal?, forKey key: Self.Key) throws { - if let value = value { - try encode(value, forKey: key) - } - } -} - -extension KeyedDecodingContainerProtocol { - - public func decodeArray(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T: Decodable { - var tmpArray = [T]() - - var nestedContainer = try nestedUnkeyedContainer(forKey: key) - while !nestedContainer.isAtEnd { - let arrayValue = try nestedContainer.decode(T.self) - tmpArray.append(arrayValue) - } - - return tmpArray - } - - public func decodeArrayIfPresent(_ type: T.Type, forKey key: Self.Key) throws -> [T]? where T: Decodable { - var tmpArray: [T]? - - if contains(key) { - tmpArray = try decodeArray(T.self, forKey: key) - } - - return tmpArray - } - - public func decodeMap(_ type: T.Type, excludedKeys: Set) throws -> [Self.Key: T] where T: Decodable { - var map: [Self.Key: T] = [:] - - for key in allKeys { - if !excludedKeys.contains(key) { - let value = try decode(T.self, forKey: key) - map[key] = value - } - } - - return map - } - - public func decode(_ type: Decimal.Type, forKey key: Self.Key) throws -> Decimal { - let stringValue = try decode(String.self, forKey: key) - guard let decimalValue = Decimal(string: stringValue) else { - let context = DecodingError.Context(codingPath: [key], debugDescription: "The key \(key) couldn't be converted to a Decimal value") - throw DecodingError.typeMismatch(type, context) - } - - return decimalValue - } - - public func decodeIfPresent(_ type: Decimal.Type, forKey key: Self.Key) throws -> Decimal? { - guard let stringValue = try decodeIfPresent(String.self, forKey: key) else { - return nil - } - guard let decimalValue = Decimal(string: stringValue) else { - let context = DecodingError.Context(codingPath: [key], debugDescription: "The key \(key) couldn't be converted to a Decimal value") - throw DecodingError.typeMismatch(type, context) - } - - return decimalValue - } - -} From 3d1ff034817e980a010074dad95942f2c951f3c2 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 9 Aug 2024 13:46:19 +0100 Subject: [PATCH 15/38] [CI] Rename bot on CI --- .github/workflows/cron-checks.yml | 2 +- .github/workflows/release-merge.yml | 2 +- .github/workflows/release-publish.yml | 2 +- .github/workflows/release-start.yml | 2 +- .github/workflows/sdk-size-metrics.yml | 2 +- .github/workflows/smoke-checks.yml | 2 +- .github/workflows/testflight.yml | 2 +- fastlane/Fastfile | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml index d986e5ec5..e409b46fe 100644 --- a/.github/workflows/cron-checks.yml +++ b/.github/workflows/cron-checks.yml @@ -212,7 +212,7 @@ jobs: env: XCODE_VERSION: "15.0.1" steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/release-merge.yml b/.github/workflows/release-merge.yml index 05785d093..a100effec 100644 --- a/.github/workflows/release-merge.yml +++ b/.github/workflows/release-merge.yml @@ -12,7 +12,7 @@ jobs: runs-on: macos-14 if: github.event_name == 'workflow_dispatch' || (github.event.issue.pull_request && github.event.issue.state == 'open' && github.event.comment.body == '/merge release') steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 80113e243..712c0fca7 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -13,7 +13,7 @@ jobs: runs-on: macos-13 if: github.event.pull_request.merged == true # only merged pull requests must trigger this job steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index a03592c30..6900e055c 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -13,7 +13,7 @@ jobs: name: Start new release runs-on: macos-14 steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index 16e1d1e1c..b2245193b 100644 --- a/.github/workflows/sdk-size-metrics.yml +++ b/.github/workflows/sdk-size-metrics.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}' steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index 3dd502d91..af34b7bb1 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -179,7 +179,7 @@ jobs: env: XCODE_VERSION: "15.0.1" steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 7b4572c1a..951a73a97 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -31,7 +31,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PR_NUM: ${{ github.event.number }} steps: - - name: Connect iOS Bot + - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 with: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6175d8bbe..2acbe985a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -901,7 +901,7 @@ end private_lane :create_pr_comment do |options| if is_ci && !options[:pr_num].to_s.empty? - last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-iOS-Bot\")) | last'") + last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-SDK-Bot\")) | last'") edit_last_comment = options[:edit_last_comment_with_text] && last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : '' sh("gh pr comment #{options[:pr_num]} #{edit_last_comment} -b '#{options[:text]}'") end From 4ca2bdad425fcd287fd359dfedb4be21cb221751 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 12 Aug 2024 11:13:15 +0100 Subject: [PATCH 16/38] [CI] Rename metrics repo --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2acbe985a..441bb5747 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,7 +16,7 @@ video_buddy_port = 5678 app_secret = ENV.fetch('STREAM_VIDEO_SECRET', nil) derived_data_path = 'derived_data' source_packages_path = 'spm_cache' -metrics_git = 'git@github.com:GetStream/apple-internal-metrics.git' +metrics_git = 'git@github.com:GetStream/stream-internal-metrics.git' sdk_size_path = "metrics/#{github_repo.split('/').last}-size.json" buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++' gci = ENV['GOOGLE_CLIENT_ID'] || '' From ee1f20964671785bcd04a9bf034c9fe060c220bc Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 14 Aug 2024 09:09:10 +0100 Subject: [PATCH 17/38] [CI] Share fastlane lanes across platforms (#487) --- .github/workflows/cron-checks.yml | 5 +- .gitignore | 2 +- Gemfile.lock | 4 +- fastlane/Fastfile | 165 ++++-------------------------- fastlane/Pluginfile | 2 +- 5 files changed, 26 insertions(+), 152 deletions(-) diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml index e409b46fe..3fd9ceb10 100644 --- a/.github/workflows/cron-checks.yml +++ b/.github/workflows/cron-checks.yml @@ -2,8 +2,9 @@ name: Cron Checks on: schedule: - # Runs "At 01:00 every night" - - cron: '0 1 * * *' + # Runs "At 01:00 every night except weekends" + - cron: '0 1 * * 1-5' + workflow_dispatch: concurrency: diff --git a/.gitignore b/.gitignore index dbc271087..60500fe5e 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,7 @@ fastlane/screenshots fastlane/test_output fastlane/allurectl fastlane/xcresults -fastlane/metrics +**/metrics/ recordings *.coverage.txt vendor/bundle/ diff --git a/Gemfile.lock b/Gemfile.lock index 0ec3d13ab..7e5cacab2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,7 +202,7 @@ GEM bundler fastlane pry - fastlane-plugin-stream_actions (0.3.38) + fastlane-plugin-stream_actions (0.3.57) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) ffi (1.17.0) @@ -430,7 +430,7 @@ DEPENDENCIES fastlane fastlane-plugin-create_xcframework fastlane-plugin-lizard - fastlane-plugin-stream_actions (= 0.3.38) + fastlane-plugin-stream_actions (= 0.3.57) fastlane-plugin-versioning jazzy json diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 441bb5747..8bf29877c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,8 +16,6 @@ video_buddy_port = 5678 app_secret = ENV.fetch('STREAM_VIDEO_SECRET', nil) derived_data_path = 'derived_data' source_packages_path = 'spm_cache' -metrics_git = 'git@github.com:GetStream/stream-internal-metrics.git' -sdk_size_path = "metrics/#{github_repo.split('/').last}-size.json" buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++' gci = ENV['GOOGLE_CLIENT_ID'] || '' reversed_gci = gci.split('.').reverse.join('.') @@ -26,15 +24,10 @@ is_localhost = !is_ci swiftformat_excluded_paths = ["**/Generated", "**/generated", "**/protobuf", "**/OpenApi"] swiftformat_source_paths = ["Sources", "DemoApp", "DemoAppUIKit", "StreamVideoTests", "StreamVideoSwiftUITests", "StreamVideoUIKitTests"] -warning_status = '🟡' # Warning if a branch is #{max_tolerance} less performant than the benchmark -fail_status = '🔴' # Failure if a branch is more than #{max_tolerance} less performant than the benchmark -success_status = '🟢' # Success if a branch is more performant or equals to the benchmark -outstanding_status = '🚀' # Outstanding performance - before_all do |lane| if is_ci setup_ci - sh('git config --global user.name "Stream Bot"') + setup_git_config xcversion(version: xcode_version) unless [:publish_release, :allure_launch, :allure_upload, :pod_lint, :stop_e2e_helpers].include?(lane) elsif lane == :test_e2e stop_e2e_helpers @@ -292,7 +285,7 @@ private_lane :test_ui do |options| png_files.each { |png| sh("git add #{png}") || true } sh('git restore .') - create_pr( + pr_create( title: '[CI] Snapshots', base_branch: current_branch, head_branch: "#{current_branch}-snapshots" @@ -518,7 +511,7 @@ lane :merge_release_to_main do |options| comment = "[Publication of the release](https://github.com/#{github_repo}/actions/workflows/release-publish.yml) has been launched 👍" UI.important(comment) - create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: comment) + pr_comment(text: comment) end lane :merge_main_to_develop do @@ -713,21 +706,7 @@ lane :run_swift_format do |options| end lane :install_runtime do |options| - runtimes = `xcrun simctl runtime list -j` - UI.message("👉 Runtime list:\n#{runtimes}") - simulators = JSON.parse(runtimes).select do |_, sim| - sim['platformIdentifier'].end_with?('iphonesimulator') && sim['version'] == options[:ios] && sim['state'] == 'Ready' - end - - if simulators.empty? - Dir.chdir('..') do - sh("echo 'iOS #{options[:ios]} Simulator' | ipsw download xcode --sim") if Dir['*.dmg'].first.nil? - sh("./Scripts/install_ios_runtime.sh #{Dir['*.dmg'].first}") - UI.success("iOS #{options[:ios]} Runtime successfuly installed") - end - else - UI.important("iOS #{options[:ios]} Runtime already exists") - end + install_ios_runtime(version: options[:ios], custom_script: 'Scripts/install_ios_runtime.sh') end desc 'Remove UI Snapshots' @@ -760,96 +739,26 @@ lane :copyright do update_copyright(ignore: [derived_data_path, source_packages_path, 'vendor/']) next unless is_ci - create_pr( + pr_create( title: '[CI] Update Copyright', head_branch: "ci/update-copyright-#{Time.now.to_i}" ) end -desc 'Show current frameworks size' lane :show_frameworks_sizes do |options| next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check) - ['metrics/'].each { |dir| FileUtils.remove_dir(dir, force: true) } - - sh("git clone #{metrics_git} #{File.dirname(sdk_size_path)}") - is_release = current_branch.include?('release/') - benchmark_config = JSON.parse(File.read(sdk_size_path)) - benchmark_key = is_release ? 'release' : 'develop' - benchmark_sizes = benchmark_config[benchmark_key] - branch_sizes = options[:sizes] || frameworks_sizes - - table_header = '## SDK Size' - markdown_table = "#{table_header}\n| `title` | `#{is_release ? 'previous release' : 'develop'}` | `#{is_release ? 'current release' : 'branch'}` | `diff` | `status` |\n| - | - | - | - | - |\n" - sdk_names.each do |title| - benchmark_value = benchmark_sizes[title] - branch_value = branch_sizes[title.to_sym] - max_tolerance = 0.5 # Max Tolerance is 0.5MB - fine_tolerance = 0.25 # Fine Tolerance is 0.25MB - - diff = (branch_value - benchmark_value).round(2) - - status_emoji = - if diff < 0 - outstanding_status - elsif diff >= max_tolerance - fail_status - elsif diff >= fine_tolerance - warning_status - else - success_status - end - - markdown_table << "|#{title}|#{benchmark_value}MB|#{branch_value}MB|#{diff}MB|#{status_emoji}|\n" - end - - FastlaneCore::PrintTable.print_values(title: 'Benchmark', config: benchmark_sizes) - FastlaneCore::PrintTable.print_values(title: 'SDK Size', config: branch_sizes) - - if is_ci - if is_release || ENV['GITHUB_EVENT_NAME'].to_s == 'push' - benchmark_config[benchmark_key] = branch_sizes - File.write(sdk_size_path, JSON.pretty_generate(benchmark_config)) - Dir.chdir(File.dirname(sdk_size_path)) do - if sh('git status -s', log: false).to_s.empty? - UI.important('No changes in SDK sizes benchmarks.') - else - sh('git add -A') - sh("git commit -m 'Update #{sdk_size_path}'") - sh('git push') - end - end - end - - create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: markdown_table, edit_last_comment_with_text: table_header) - end - - UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status) + sizes = options[:sizes] || frameworks_sizes + show_sdk_size(branch_sizes: sizes, github_repo: github_repo) + update_img_shields_sdk_sizes(sizes: sizes, open_pr: options[:open_pr]) if options[:update_readme] end -desc 'Update img shields SDK size labels' lane :update_img_shields_sdk_sizes do - sizes = frameworks_sizes - - # Read the file into a string - readme_path = '../README.md' - readme_content = File.read(readme_path) - - # Define the new value for the badge - stream_video_size = "#{sizes[:StreamVideo]}MB" - stream_video_swiftui_size = "#{sizes[:StreamVideoSwiftUI]}MB" - stream_video_uikit_size = "#{sizes[:StreamVideoUIKit]}MB" - - # Replace the value in the badge URL - readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideo-)(.*?)(-blue)}, "\\1#{stream_video_size}\\3") - readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideoSwiftUI-)(.*?)(-blue)}, "\\1#{stream_video_swiftui_size}\\3") - readme_content.gsub!(%r{(https://img.shields.io/badge/StreamVideoUIKit-)(.*?)(-blue)}, "\\1#{stream_video_uikit_size}\\3") - - # Write the updated content back to the file - File.write(readme_path, readme_content) - - # Notify success - UI.success('Successfully updated the SDK size labels in README.md!') + update_sdk_size_in_readme( + open_pr: options[:open_pr] || false, + readme_path: 'README.md', + sizes: options[:sizes] || frameworks_sizes + ) end def frameworks_sizes @@ -869,51 +778,15 @@ def frameworks_sizes frameworks_path = "../#{archive_dir}/Products/Applications/DemoAppUIKit.app/Frameworks" stream_video_size = File.size("#{frameworks_path}/StreamVideo.framework/StreamVideo") - stream_video_size_mb = (stream_video_size.to_f / 1024 / 1024).round(2) + stream_video_size_kb = stream_video_size.to_f / 1024 stream_video_swiftui_size = File.size("#{frameworks_path}/StreamVideoSwiftUI.framework/StreamVideoSwiftUI") - stream_video_swiftui_size_mb = (stream_video_swiftui_size.to_f / 1024 / 1024).round(2) + stream_video_swiftui_size_kb = stream_video_swiftui_size.to_f / 1024 stream_video_uikit_size = File.size("#{frameworks_path}/StreamVideoUIKit.framework/StreamVideoUIKit") - stream_video_uikit_size_mb = ((stream_video_uikit_size + stream_video_swiftui_size).to_f / 1024 / 1024).round(2) + stream_video_uikit_size_kb = (stream_video_uikit_size + stream_video_swiftui_size).to_f / 1024 { - StreamVideo: stream_video_size_mb, - StreamVideoSwiftUI: stream_video_swiftui_size_mb, - StreamVideoUIKit: stream_video_uikit_size_mb + StreamVideo: stream_video_size_kb, + StreamVideoSwiftUI: stream_video_swiftui_size_kb, + StreamVideoUIKit: stream_video_uikit_size_kb } end - -private_lane :create_pr do |options| - options[:base_branch] ||= 'develop' - sh("git checkout -b #{options[:head_branch]}") - sh('git add -A') - sh("git commit -m '#{options[:title]}'") - push_to_git_remote(tags: false) - - create_pull_request( - api_token: ENV.fetch('GITHUB_TOKEN', nil), - repo: github_repo, - title: options[:title], - head: options[:head_branch], - base: options[:base_branch], - body: 'This PR was created automatically by CI.' - ) -end - -private_lane :create_pr_comment do |options| - if is_ci && !options[:pr_num].to_s.empty? - last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-SDK-Bot\")) | last'") - edit_last_comment = options[:edit_last_comment_with_text] && last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : '' - sh("gh pr comment #{options[:pr_num]} #{edit_last_comment} -b '#{options[:text]}'") - end -end - -lane :current_branch do - branch = if ENV['GITHUB_PR_NUM'].to_s.empty? - git_branch - else - sh("gh pr view #{ENV.fetch('GITHUB_PR_NUM')} --json headRefName -q .headRefName").strip - end - - UI.important("Current branch: #{branch} 🕊️") - branch -end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 0b9f84e1a..a2e380539 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -1,3 +1,3 @@ gem 'fastlane-plugin-versioning' -gem 'fastlane-plugin-stream_actions', '0.3.38' +gem 'fastlane-plugin-stream_actions', '0.3.57' gem 'fastlane-plugin-create_xcframework' From e65f5e8119a3b8739f00758cd176880b2584ac65 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 14 Aug 2024 10:08:49 +0100 Subject: [PATCH 18/38] [CI] Measure the size of StreamWebRTC (#485) --- README.md | 1 + fastlane/Fastfile | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 67e34053b..fc74182b0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ StreamVideo StreamVideoSwiftUI StreamVideoUIKit + StreamWebRTC

![Stream Video for iOS Header image](https://github.com/GetStream/stream-video-swift/assets/12433593/e4a44ae5-a8eb-4ac7-8910-28187aa011f6) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8bf29877c..38d0d9f58 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -783,10 +783,13 @@ def frameworks_sizes stream_video_swiftui_size_kb = stream_video_swiftui_size.to_f / 1024 stream_video_uikit_size = File.size("#{frameworks_path}/StreamVideoUIKit.framework/StreamVideoUIKit") stream_video_uikit_size_kb = (stream_video_uikit_size + stream_video_swiftui_size).to_f / 1024 + stream_web_rtc_size = File.size("#{frameworks_path}/StreamWebRTC.framework/StreamWebRTC") + stream_web_rtc_size_kb = stream_web_rtc_size.to_f / 1024 { StreamVideo: stream_video_size_kb, StreamVideoSwiftUI: stream_video_swiftui_size_kb, - StreamVideoUIKit: stream_video_uikit_size_kb + StreamVideoUIKit: stream_video_uikit_size_kb, + StreamWebRTC: stream_web_rtc_size_kb } end From babef4d5edb0da94795df0c398225ff8a1f54f1a Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 14 Aug 2024 10:13:21 +0100 Subject: [PATCH 19/38] [CI] Add space to SDK size badge --- Gemfile.lock | 4 ++-- README.md | 8 ++++---- fastlane/Fastfile | 16 ++++++++-------- fastlane/Pluginfile | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7e5cacab2..b819143ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,7 +202,7 @@ GEM bundler fastlane pry - fastlane-plugin-stream_actions (0.3.57) + fastlane-plugin-stream_actions (0.3.59) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) ffi (1.17.0) @@ -430,7 +430,7 @@ DEPENDENCIES fastlane fastlane-plugin-create_xcframework fastlane-plugin-lizard - fastlane-plugin-stream_actions (= 0.3.57) + fastlane-plugin-stream_actions (= 0.3.59) fastlane-plugin-versioning jazzy json diff --git a/README.md b/README.md index fc74182b0..0fb82306e 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@

- StreamVideo - StreamVideoSwiftUI - StreamVideoUIKit - StreamWebRTC + StreamVideo + StreamVideoSwiftUI + StreamVideoUIKit + StreamWebRTC

![Stream Video for iOS Header image](https://github.com/GetStream/stream-video-swift/assets/12433593/e4a44ae5-a8eb-4ac7-8910-28187aa011f6) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 38d0d9f58..d0e9646a2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -778,18 +778,18 @@ def frameworks_sizes frameworks_path = "../#{archive_dir}/Products/Applications/DemoAppUIKit.app/Frameworks" stream_video_size = File.size("#{frameworks_path}/StreamVideo.framework/StreamVideo") - stream_video_size_kb = stream_video_size.to_f / 1024 + stream_video_size_kb = stream_video_size / 1024.0 stream_video_swiftui_size = File.size("#{frameworks_path}/StreamVideoSwiftUI.framework/StreamVideoSwiftUI") - stream_video_swiftui_size_kb = stream_video_swiftui_size.to_f / 1024 + stream_video_swiftui_size_kb = stream_video_swiftui_size / 1024.0 stream_video_uikit_size = File.size("#{frameworks_path}/StreamVideoUIKit.framework/StreamVideoUIKit") - stream_video_uikit_size_kb = (stream_video_uikit_size + stream_video_swiftui_size).to_f / 1024 + stream_video_uikit_size_kb = (stream_video_uikit_size + stream_video_swiftui_size) / 1024.0 stream_web_rtc_size = File.size("#{frameworks_path}/StreamWebRTC.framework/StreamWebRTC") - stream_web_rtc_size_kb = stream_web_rtc_size.to_f / 1024 + stream_web_rtc_size_kb = stream_web_rtc_size / 1024.0 { - StreamVideo: stream_video_size_kb, - StreamVideoSwiftUI: stream_video_swiftui_size_kb, - StreamVideoUIKit: stream_video_uikit_size_kb, - StreamWebRTC: stream_web_rtc_size_kb + StreamVideo: stream_video_size_kb.round(0), + StreamVideoSwiftUI: stream_video_swiftui_size_kb.round(0), + StreamVideoUIKit: stream_video_uikit_size_kb.round(0), + StreamWebRTC: stream_web_rtc_size_kb.round(0) } end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index a2e380539..b2ef25af9 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -1,3 +1,3 @@ gem 'fastlane-plugin-versioning' -gem 'fastlane-plugin-stream_actions', '0.3.57' +gem 'fastlane-plugin-stream_actions', '0.3.59' gem 'fastlane-plugin-create_xcframework' From 3679c135c29ffae7ebf016ccb781e6f6bef2cf8f Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Wed, 14 Aug 2024 14:39:31 +0200 Subject: [PATCH 20/38] Removed unused code (#486) --- DemoApp/Sources/Models/CallKitState.swift | 11 ----- .../generated/JSONEncodingHelper.swift | 45 ------------------- .../OpenApi/generated/Models.swift | 38 ---------------- .../Models/BackstageSettingsResponse.swift | 33 -------------- .../WebSockets/Client/WebSocketClient.swift | 6 --- .../Client/WebSocketConstants.swift | 11 ----- StreamVideo.xcodeproj/project.pbxproj | 18 -------- 7 files changed, 162 deletions(-) delete mode 100644 DemoApp/Sources/Models/CallKitState.swift delete mode 100644 Sources/StreamVideo/OpenApi/generated/JSONEncodingHelper.swift delete mode 100644 Sources/StreamVideo/OpenApi/generated/Models/BackstageSettingsResponse.swift delete mode 100644 Sources/StreamVideo/WebSockets/Client/WebSocketConstants.swift diff --git a/DemoApp/Sources/Models/CallKitState.swift b/DemoApp/Sources/Models/CallKitState.swift deleted file mode 100644 index 5e5f0a140..000000000 --- a/DemoApp/Sources/Models/CallKitState.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Copyright © 2024 Stream.io Inc. All rights reserved. -// - -import Foundation - -enum CallKitState { - case idle - case joining - case inCall -} diff --git a/Sources/StreamVideo/OpenApi/generated/JSONEncodingHelper.swift b/Sources/StreamVideo/OpenApi/generated/JSONEncodingHelper.swift deleted file mode 100644 index 02f78ffb4..000000000 --- a/Sources/StreamVideo/OpenApi/generated/JSONEncodingHelper.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// JSONEncodingHelper.swift -// -// Generated by openapi-generator -// https://openapi-generator.tech -// - -import Foundation - -open class JSONEncodingHelper { - - open class func encodingParameters(forEncodableObject encodableObj: T?) -> [String: Any]? { - var params: [String: Any]? - - // Encode the Encodable object - if let encodableObj = encodableObj { - let encodeResult = CodableHelper.encode(encodableObj) - do { - let data = try encodeResult.get() - params = JSONDataEncoding.encodingParameters(jsonData: data) - } catch { - print(error.localizedDescription) - } - } - - return params - } - - open class func encodingParameters(forEncodableObject encodableObj: Any?) -> [String: Any]? { - var params: [String: Any]? - - if let encodableObj = encodableObj { - do { - let data = try JSONSerialization.data(withJSONObject: encodableObj, options: .prettyPrinted) - params = JSONDataEncoding.encodingParameters(jsonData: data) - } catch { - print(error.localizedDescription) - return nil - } - } - - return params - } - -} diff --git a/Sources/StreamVideo/OpenApi/generated/Models.swift b/Sources/StreamVideo/OpenApi/generated/Models.swift index b49cf8d12..9092bd661 100644 --- a/Sources/StreamVideo/OpenApi/generated/Models.swift +++ b/Sources/StreamVideo/OpenApi/generated/Models.swift @@ -62,26 +62,6 @@ extension NullEncodable: Codable where Wrapped: Codable { } } -public enum ErrorResponse: Error { - case error(Int, Data?, URLResponse?, Error) -} - -public enum DownloadException: Error { - case responseDataMissing - case responseFailed - case requestMissing - case requestMissingPath - case requestMissingURL -} - -public enum DecodableRequestBuilderError: Error { - case emptyDataResponse - case nilHTTPResponse - case unsuccessfulHTTPStatusCode - case jsonDecoding(DecodingError) - case generalError(Error) -} - open class Response { public let statusCode: Int public let header: [String: String] @@ -106,21 +86,3 @@ open class Response { self.init(statusCode: response.statusCode, header: header, body: body, bodyData: bodyData) } } - -public final class RequestTask: @unchecked Sendable { - private var lock = NSRecursiveLock() - private var task: URLSessionTask? - - internal func set(task: URLSessionTask) { - lock.lock() - defer { lock.unlock() } - self.task = task - } - - public func cancel() { - lock.lock() - defer { lock.unlock() } - task?.cancel() - task = nil - } -} diff --git a/Sources/StreamVideo/OpenApi/generated/Models/BackstageSettingsResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/BackstageSettingsResponse.swift deleted file mode 100644 index 2cd2bb307..000000000 --- a/Sources/StreamVideo/OpenApi/generated/Models/BackstageSettingsResponse.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// BackstageSettingsResponse.swift -// -// Generated by openapi-generator -// https://openapi-generator.tech -// - -import Foundation - - -public struct BackstageSettingsResponse: Codable, JSONEncodable, Hashable { - public var enabled: Bool - public var joinAheadTimeSeconds: Int? - - public init(enabled: Bool, joinAheadTimeSeconds: Int? = nil) { - self.enabled = enabled - self.joinAheadTimeSeconds = joinAheadTimeSeconds - } - - public enum CodingKeys: String, CodingKey, CaseIterable { - case enabled - case joinAheadTimeSeconds = "join_ahead_time_seconds" - } - - // Encodable protocol methods - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(enabled, forKey: .enabled) - try container.encodeIfPresent(joinAheadTimeSeconds, forKey: .joinAheadTimeSeconds) - } -} - diff --git a/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift b/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift index 5a88e2c3e..06e022620 100644 --- a/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift +++ b/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift @@ -317,11 +317,5 @@ extension ClientError { public class WebSocket: ClientError {} } -/// WebSocket Error -struct WebSocketErrorContainer: Decodable { - /// A server error was received. - let error: ErrorPayload -} - struct WSDisconnected: Event {} struct WSConnected: Event {} diff --git a/Sources/StreamVideo/WebSockets/Client/WebSocketConstants.swift b/Sources/StreamVideo/WebSockets/Client/WebSocketConstants.swift deleted file mode 100644 index ed560941d..000000000 --- a/Sources/StreamVideo/WebSockets/Client/WebSocketConstants.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Copyright © 2024 Stream.io Inc. All rights reserved. -// - -import Foundation - -enum WebSocketConstants { - static let callId = "callId" - static let callType = "callType" - static let sessionId = "sessionId" -} diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 514aa82b7..5335505ee 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -156,7 +156,6 @@ 4069A0052AD985D3009A3A06 /* CallParticipant_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406303412AD848000091AE77 /* CallParticipant_Mock.swift */; }; 406A8E8D2AA1D78C001F598A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4030E59F2A9DF5BD003E8CBA /* AppEnvironment.swift */; }; 406A8E8E2AA1D79D001F598A /* UserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AD2A9DFC34004BE3DA /* UserState.swift */; }; - 406A8E922AA1D7B3001F598A /* CallKitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AF2A9DFC58004BE3DA /* CallKitState.swift */; }; 406A8E932AA1D7BF001F598A /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8456E6C3287EB43A004E180E /* LoginViewModel.swift */; }; 406A8E942AA1D7C5001F598A /* UserCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AB2A9DFC13004BE3DA /* UserCredentials.swift */; }; 406A8E952AA1D7CB001F598A /* AddUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84201792288AB699004964B3 /* AddUserView.swift */; }; @@ -358,7 +357,6 @@ 40F18B8E2BEBB65100ADF76E /* View+OptionalPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F18B8D2BEBB65100ADF76E /* View+OptionalPublisher.swift */; }; 40F445AC2A9DFC13004BE3DA /* UserCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AB2A9DFC13004BE3DA /* UserCredentials.swift */; }; 40F445AE2A9DFC34004BE3DA /* UserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AD2A9DFC34004BE3DA /* UserState.swift */; }; - 40F445B02A9DFC58004BE3DA /* CallKitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445AF2A9DFC58004BE3DA /* CallKitState.swift */; }; 40F445B22A9DFFBB004BE3DA /* User+Demo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445B12A9DFFBB004BE3DA /* User+Demo.swift */; }; 40F445B42A9E01B2004BE3DA /* AuthenticationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445B32A9E01B2004BE3DA /* AuthenticationProvider.swift */; }; 40F445BE2A9E0823004BE3DA /* RobotVoiceFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F445BD2A9E0823004BE3DA /* RobotVoiceFilter.swift */; }; @@ -674,7 +672,6 @@ 8442993A29422BEA0037232A /* BackportStateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442993929422BEA0037232A /* BackportStateObject.swift */; }; 8442993C294232360037232A /* IncomingCallView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442993B294232360037232A /* IncomingCallView_iOS13.swift */; }; 844299412942394C0037232A /* VideoView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844299402942394C0037232A /* VideoView_iOS13.swift */; }; - 844542F02C296AAB001D5ADF /* BackstageSettingsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */; }; 8446AF912A4D84F4002AB07B /* Retries_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */; }; 844ADA652AD3F1AB00769F6A /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 844ADA642AD3F1AB00769F6A /* GoogleSignInSwift */; }; 844ADA672AD3F21000769F6A /* GoogleSignIn.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844ADA662AD3F21000769F6A /* GoogleSignIn.plist */; }; @@ -883,7 +880,6 @@ 84D6E53A2B3AD10000D0056C /* RepeatingTimer_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D6E5392B3AD10000D0056C /* RepeatingTimer_Tests.swift */; }; 84DC382D29A8B9EC00946713 /* CallParticipantMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC382C29A8B9EC00946713 /* CallParticipantMenuAction.swift */; }; 84DC382F29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC382E29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift */; }; - 84DC388E29ADFCFD00946713 /* JSONEncodingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC383C29ADFCFB00946713 /* JSONEncodingHelper.swift */; }; 84DC388F29ADFCFD00946713 /* CodableHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC383D29ADFCFB00946713 /* CodableHelper.swift */; }; 84DC389029ADFCFD00946713 /* SendEventResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC383F29ADFCFC00946713 /* SendEventResponse.swift */; }; 84DC389129ADFCFD00946713 /* VideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC384029ADFCFC00946713 /* VideoSettings.swift */; }; @@ -976,7 +972,6 @@ 84EA5D4328C1E944004D3531 /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EA5D4228C1E944004D3531 /* AudioSession.swift */; }; 84EBA4A22A72B81100577297 /* BroadcastBufferConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EBA4A12A72B81100577297 /* BroadcastBufferConnection.swift */; }; 84ED240D286C9515002A3186 /* DemoCallContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED240C286C9515002A3186 /* DemoCallContainerView.swift */; }; - 84F3B0DA289083E70088751D /* WebSocketConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F3B0D9289083E70088751D /* WebSocketConstants.swift */; }; 84F3B0DE28913E0F0088751D /* CallControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F3B0DD28913E0E0088751D /* CallControlsView.swift */; }; 84F3B0E0289150B10088751D /* CallParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F3B0DF289150B10088751D /* CallParticipant.swift */; }; 84F3B0E228916FF20088751D /* CallParticipantsInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F3B0E128916FF20088751D /* CallParticipantsInfoView.swift */; }; @@ -1431,7 +1426,6 @@ 40F18B8D2BEBB65100ADF76E /* View+OptionalPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OptionalPublisher.swift"; sourceTree = ""; }; 40F445AB2A9DFC13004BE3DA /* UserCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentials.swift; sourceTree = ""; }; 40F445AD2A9DFC34004BE3DA /* UserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserState.swift; sourceTree = ""; }; - 40F445AF2A9DFC58004BE3DA /* CallKitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitState.swift; sourceTree = ""; }; 40F445B12A9DFFBB004BE3DA /* User+Demo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "User+Demo.swift"; sourceTree = ""; }; 40F445B32A9E01B2004BE3DA /* AuthenticationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationProvider.swift; sourceTree = ""; }; 40F445BD2A9E0823004BE3DA /* RobotVoiceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RobotVoiceFilter.swift; sourceTree = ""; }; @@ -1696,7 +1690,6 @@ 8442993929422BEA0037232A /* BackportStateObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackportStateObject.swift; sourceTree = ""; }; 8442993B294232360037232A /* IncomingCallView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView_iOS13.swift; sourceTree = ""; }; 844299402942394C0037232A /* VideoView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView_iOS13.swift; sourceTree = ""; }; - 844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackstageSettingsResponse.swift; sourceTree = ""; }; 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retries_Tests.swift; sourceTree = ""; }; 844ADA662AD3F21000769F6A /* GoogleSignIn.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = GoogleSignIn.plist; sourceTree = ""; }; 844ADA682AD3F78F00769F6A /* GoogleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleHelper.swift; sourceTree = ""; }; @@ -1906,7 +1899,6 @@ 84D6E5392B3AD10000D0056C /* RepeatingTimer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer_Tests.swift; sourceTree = ""; }; 84DC382C29A8B9EC00946713 /* CallParticipantMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantMenuAction.swift; sourceTree = ""; }; 84DC382E29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantsInfoViewModel.swift; sourceTree = ""; }; - 84DC383C29ADFCFB00946713 /* JSONEncodingHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONEncodingHelper.swift; sourceTree = ""; }; 84DC383D29ADFCFB00946713 /* CodableHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableHelper.swift; sourceTree = ""; }; 84DC383F29ADFCFC00946713 /* SendEventResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEventResponse.swift; sourceTree = ""; }; 84DC384029ADFCFC00946713 /* VideoSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoSettings.swift; sourceTree = ""; }; @@ -1996,7 +1988,6 @@ 84EBA4A12A72B81100577297 /* BroadcastBufferConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastBufferConnection.swift; sourceTree = ""; }; 84EBAA92288C137E00BE3176 /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = ""; }; 84ED240C286C9515002A3186 /* DemoCallContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCallContainerView.swift; sourceTree = ""; }; - 84F3B0D9289083E70088751D /* WebSocketConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketConstants.swift; sourceTree = ""; }; 84F3B0DD28913E0E0088751D /* CallControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallControlsView.swift; sourceTree = ""; }; 84F3B0DF289150B10088751D /* CallParticipant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipant.swift; sourceTree = ""; }; 84F3B0E128916FF20088751D /* CallParticipantsInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantsInfoView.swift; sourceTree = ""; }; @@ -2243,7 +2234,6 @@ 401A64B02A9DF83200534ED1 /* TokenResponse.swift */, 40F445AB2A9DFC13004BE3DA /* UserCredentials.swift */, 40F445AD2A9DFC34004BE3DA /* UserState.swift */, - 40F445AF2A9DFC58004BE3DA /* CallKitState.swift */, 40F445D32A9E2051004BE3DA /* Reaction.swift */, ); path = Models; @@ -3467,7 +3457,6 @@ 84A7E1952883661A00526C98 /* BackgroundTaskScheduler.swift */, 84A7E1852883632100526C98 /* ConnectionStatus.swift */, 84A7E1A92883E4AD00526C98 /* APIKey.swift */, - 84F3B0D9289083E70088751D /* WebSocketConstants.swift */, ); path = Client; sourceTree = ""; @@ -3944,7 +3933,6 @@ 84DC383D29ADFCFB00946713 /* CodableHelper.swift */, 84DC388929ADFCFC00946713 /* Extensions.swift */, 84DC388A29ADFCFC00946713 /* JSONDataEncoding.swift */, - 84DC383C29ADFCFB00946713 /* JSONEncodingHelper.swift */, 84DC388C29ADFCFC00946713 /* Models.swift */, 84DC388829ADFCFC00946713 /* OpenISO8601DateFormatter.swift */, ); @@ -4070,7 +4058,6 @@ 84DC383E29ADFCFC00946713 /* Models */ = { isa = PBXGroup; children = ( - 844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */, 845C09962C11AAA100F725B3 /* RejectCallRequest.swift */, 845C09822C0DEB5C00F725B3 /* LimitsSettingsRequest.swift */, 845C09832C0DEB5C00F725B3 /* LimitsSettingsResponse.swift */, @@ -5086,7 +5073,6 @@ 84093811288A90390089A35B /* DetailedCallingView.swift in Sources */, 84ED240D286C9515002A3186 /* DemoCallContainerView.swift in Sources */, 40F445B22A9DFFBB004BE3DA /* User+Demo.swift in Sources */, - 40F445B02A9DFC58004BE3DA /* CallKitState.swift in Sources */, 40D946452AA5F67E00C8861B /* DemoCallingTopView.swift in Sources */, 847B47B72A260CF1000714CE /* CustomCallView.swift in Sources */, 40F445EA2A9E297B004BE3DA /* CallStateResponseFields+Identifiable.swift in Sources */, @@ -5229,7 +5215,6 @@ 407F29FF2AA6011500C3EAF8 /* MemoryLogViewer.swift in Sources */, 40AB35572B738C7100E465CC /* CallsViewModel.swift in Sources */, 408D29AE2B6D681000885473 /* SnapshotTrigger.swift in Sources */, - 406A8E922AA1D7B3001F598A /* CallKitState.swift in Sources */, 406A8E8D2AA1D78C001F598A /* AppEnvironment.swift in Sources */, 40AB35442B738C4100E465CC /* DemoReactionButton.swift in Sources */, 849322612908385C0013C029 /* HomeViewController.swift in Sources */, @@ -5270,7 +5255,6 @@ 8409465B29AF4EEC007AF5BF /* ListRecordingsResponse.swift in Sources */, 8490DD21298D4ADF007E53D2 /* StreamJsonDecoder.swift in Sources */, 84C4004229E3F446007B69C2 /* ConnectedEvent.swift in Sources */, - 844542F02C296AAB001D5ADF /* BackstageSettingsResponse.swift in Sources */, 84DC389C29ADFCFD00946713 /* GetOrCreateCallResponse.swift in Sources */, 4065839B2B877ADA00B4F979 /* CIImage+Sendable.swift in Sources */, 84DCA2242A3A0F0D000C3411 /* HTTPClient.swift in Sources */, @@ -5292,7 +5276,6 @@ 84DCA21F2A39DA15000C3411 /* APIHelper.swift in Sources */, 84DC38D329ADFCFD00946713 /* CallEndedEvent.swift in Sources */, 84A737D028F4716E001A6769 /* models.pb.swift in Sources */, - 84DC388E29ADFCFD00946713 /* JSONEncodingHelper.swift in Sources */, 846D16222A52B8D00036CE4C /* MicrophoneManager.swift in Sources */, 842E70DB2B91BE1700D2D68B /* ListTranscriptionsResponse.swift in Sources */, 4012B1902BFCA4D3006B0031 /* StreamCallStateMachine+AcceptedStage.swift in Sources */, @@ -5414,7 +5397,6 @@ 4012B1942BFCAC1C006B0031 /* StreamCallStateMachine+RejectingStage.swift in Sources */, 84DCA2152A38A79E000C3411 /* Token.swift in Sources */, 40FB151B2BF77EEE00D5E580 /* StreamCallStateMachine+JoiningStage.swift in Sources */, - 84F3B0DA289083E70088751D /* WebSocketConstants.swift in Sources */, 840F59902A77FDCB00EF3EB2 /* UnpinRequest.swift in Sources */, 8440861E2901A1700027849C /* SfuMiddleware.swift in Sources */, 40FB15172BF77EA600D5E580 /* StreamCallStateMachine.swift in Sources */, From f2c3de3f3294fea981432efb9e9a92ee1b740a12 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 15 Aug 2024 16:48:07 +0100 Subject: [PATCH 21/38] Bump fastlane plugin version --- Gemfile.lock | 4 ++-- fastlane/Pluginfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b819143ba..d82a70999 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,7 +202,7 @@ GEM bundler fastlane pry - fastlane-plugin-stream_actions (0.3.59) + fastlane-plugin-stream_actions (0.3.60) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) ffi (1.17.0) @@ -430,7 +430,7 @@ DEPENDENCIES fastlane fastlane-plugin-create_xcframework fastlane-plugin-lizard - fastlane-plugin-stream_actions (= 0.3.59) + fastlane-plugin-stream_actions (= 0.3.60) fastlane-plugin-versioning jazzy json diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index b2ef25af9..f02744589 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -1,3 +1,3 @@ gem 'fastlane-plugin-versioning' -gem 'fastlane-plugin-stream_actions', '0.3.59' gem 'fastlane-plugin-create_xcframework' +gem 'fastlane-plugin-stream_actions', '0.3.60' From a5b78f071960d16398d778927c58b44882d3f683 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 16 Aug 2024 17:32:24 +0100 Subject: [PATCH 22/38] [CI] Update release-related fastlane lanes --- .github/workflows/release-merge.yml | 2 +- .github/workflows/release-publish.yml | 29 +++++++++++++---- Gemfile.lock | 4 +-- fastlane/Fastfile | 46 +++------------------------ fastlane/Pluginfile | 2 +- 5 files changed, 30 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release-merge.yml b/.github/workflows/release-merge.yml index a100effec..c8c29253c 100644 --- a/.github/workflows/release-merge.yml +++ b/.github/workflows/release-merge.yml @@ -24,7 +24,7 @@ jobs: - uses: ./.github/actions/ruby-cache - name: Merge - run: bundle exec fastlane merge_release_to_main author:"$USER_LOGIN" --verbose + run: bundle exec fastlane merge_release author:"$USER_LOGIN" --verbose env: GITHUB_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} # A token with the "admin:org" scope to get the list of the team members on GitHub GITHUB_PR_NUM: ${{ github.event.issue.number }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 712c0fca7..e1b18635a 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -7,11 +7,18 @@ on: types: - closed + workflow_dispatch: + inputs: + version: + description: 'Release version' + type: string + required: true + jobs: release: name: Publish new release runs-on: macos-13 - if: github.event.pull_request.merged == true # only merged pull requests must trigger this job + if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true }} # only merged pull requests must trigger this job steps: - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 @@ -19,20 +26,28 @@ jobs: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v4.1.1 - with: - fetch-depth: 0 + + - uses: ./.github/actions/ruby-cache + + - name: Extract version from input (for workflow dispatch) + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + if [ "$BRANCH_NAME" != "main" ]; then + echo "This workflow can only be run on the main branch." + exit 1 + fi + echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - name: Extract version from branch name (for release branches) - if: startsWith(github.event.pull_request.head.ref, 'release/') + if: ${{ github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'release/') }} run: | BRANCH_NAME="${{ github.event.pull_request.head.ref }}" VERSION=${BRANCH_NAME#release/} echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV - - uses: ./.github/actions/ruby-cache - - name: "Fastlane - Publish Release" - if: startsWith(github.event.pull_request.head.ref, 'release/') + if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.pull_request.head.ref, 'release/') }} env: GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/Gemfile.lock b/Gemfile.lock index d82a70999..9cdb7e098 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,7 +202,7 @@ GEM bundler fastlane pry - fastlane-plugin-stream_actions (0.3.60) + fastlane-plugin-stream_actions (0.3.63) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) ffi (1.17.0) @@ -430,7 +430,7 @@ DEPENDENCIES fastlane fastlane-plugin-create_xcframework fastlane-plugin-lizard - fastlane-plugin-stream_actions (= 0.3.60) + fastlane-plugin-stream_actions (= 0.3.63) fastlane-plugin-versioning jazzy json diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d0e9646a2..6af6c1841 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -70,6 +70,10 @@ lane :release do |options| ) end +lane :merge_release do |options| + merge_release_to_main(author: options[:author]) +end + desc "Publish a new release to GitHub and CocoaPods" lane :publish_release do |options| xcversion(version: '15.0.1') @@ -483,48 +487,6 @@ private_lane :build_example_app do |options| ) end -lane :merge_release_to_main do |options| - ensure_git_status_clean - - release_branch = - if is_ci - # This API operation needs the "admin:org" scope. - ios_team = sh('gh api orgs/GetStream/teams/ios-developers/members -q ".[].login"', log: false).split - UI.user_error!("#{options[:author]} is not a member of the iOS Team") unless ios_team.include?(options[:author]) - - current_branch - else - release_branches = sh(command: 'git branch -a', log: false).delete(' ').split("\n").grep(%r(origin/.*release/)) - UI.user_error!("Expected 1 release branch, found #{release_branches.size}") if release_branches.size != 1 - - release_branches.first - end - - UI.user_error!("`#{release_branch}`` branch does not match the release branch pattern: `release/*`") unless release_branch.start_with?('release/') - - sh('git checkout origin/main') - sh('git pull origin main') - - # Merge release branch to main. For more info, read: https://notion.so/iOS-Branching-Strategy-37c10127dc26493e937769d44b1d6d9a - sh("git merge #{release_branch} --ff-only") - sh('git push origin main') - - comment = "[Publication of the release](https://github.com/#{github_repo}/actions/workflows/release-publish.yml) has been launched 👍" - UI.important(comment) - pr_comment(text: comment) -end - -lane :merge_main_to_develop do - ensure_git_status_clean - sh('git checkout main') - sh('git pull origin main') - sh('git checkout origin/develop') - sh('git pull origin develop') - sh('git log develop..main') - sh('git merge main') - sh('git push origin develop') -end - desc 'Compresses the XCFrameworks into zip files' lane :compress_frameworks do Dir.chdir('..') do diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index f02744589..5ac3f66f1 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -1,3 +1,3 @@ gem 'fastlane-plugin-versioning' gem 'fastlane-plugin-create_xcframework' -gem 'fastlane-plugin-stream_actions', '0.3.60' +gem 'fastlane-plugin-stream_actions', '0.3.63' From 6edf57d5c594af430cfe05ce4e09db0f71817d3c Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Mon, 19 Aug 2024 10:18:15 +0200 Subject: [PATCH 23/38] Updates to the livestream docs (#484) --- .../03-call-and-participant-state.mdx | 21 ++++++++++++++++-- .../05-ui-cookbook/14-livestream-player.mdx | 22 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docusaurus/docs/iOS/03-guides/03-call-and-participant-state.mdx b/docusaurus/docs/iOS/03-guides/03-call-and-participant-state.mdx index d68640e05..ced3ccce4 100644 --- a/docusaurus/docs/iOS/03-guides/03-call-and-participant-state.mdx +++ b/docusaurus/docs/iOS/03-guides/03-call-and-participant-state.mdx @@ -63,15 +63,32 @@ The following fields are available on the call: ### Participant State -The `CallParticipant` is the most essential component used to render a participant in a call. It contains all of the information to render a participant, such as audio & video renderers, availabilities of audio & video, the screen sharing session, reactions, and etc. Here's how you iterate over the participants: +The `CallParticipant` is the most essential component used to render a participant in a call. It contains all of the information to render a participant, such as audio & video tracks, availabilities of audio & video, the screen sharing session, reactions, and etc. Here's how you can subscribe to participants updates: ```swift // all participants let cancellable = call.state.$participants.sink { participants in // .. } +``` + +Filtering of the participants is also supported. You can get all the participants with the role "host", with the following code: + +```swift +var hosts: [CallParticipant] { + call.state.participants.filter { $0.roles.contains("host") } +} +``` -// you +When you join a call with many participants, maximum of 250 participants are returned in the join response. The list of participants is updated dynamically when there are join call events. + +The participants that are publishing video, audio or screensharing are prioritized over the other participants in the list. + +The total number of participants is updated realtime via health check events. This value is available from the call state's `participantCount` property. + +You can get the current user with the following code: + +```swift let localParticipant: CallParticipant? = call.state.localParticipant ``` diff --git a/docusaurus/docs/iOS/05-ui-cookbook/14-livestream-player.mdx b/docusaurus/docs/iOS/05-ui-cookbook/14-livestream-player.mdx index 5e34b3e4b..d66d0403b 100644 --- a/docusaurus/docs/iOS/05-ui-cookbook/14-livestream-player.mdx +++ b/docusaurus/docs/iOS/05-ui-cookbook/14-livestream-player.mdx @@ -45,4 +45,24 @@ Apart from the required parameters, you can also specify some optional ones in t - `muted`: `Bool` - whether the livestream audio should be on when joining the stream (default is `false`). - `showParticipantCount`: `Bool` - whether the participant count should be shown (default is `true`). -- `onFullScreenStateChange`: `((Bool) -> ())?` - closure that is invoked when the full screen state changes. Useful if you use the livestream component as part of your custom views, since this is the chance to update the visibility of your custom UI elements. \ No newline at end of file +- `onFullScreenStateChange`: `((Bool) -> ())?` - closure that is invoked when the full screen state changes. Useful if you use the livestream component as part of your custom views, since this is the chance to update the visibility of your custom UI elements. + +## Accessing the livestream track + +You can also build your own version of a livestream player, depending on your requirements. In those cases, you need to have access to the livestream track (or tracks). + +If there is only one video track (you only have one person livestreaming), you can get it with the following code: + +```swift +let livestream = call.state.participants.first(where: { $0.track != nil }) +``` + +If you have multiple hosts that are livestreaming, and you want to show them all, you can fetch the hosts by role: + +```swift +var hosts: [CallParticipant] { + call.state.participants.filter { $0.roles.contains("host") } +} +``` + +Then, you can access the video track they are streaming, with the `track` property. \ No newline at end of file From d560e0657d7ce18272a91504fb216abfb384659d Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Tue, 20 Aug 2024 12:52:17 +0200 Subject: [PATCH 24/38] Generated RTMP-related code (#488) --- Sources/StreamVideo/CallState.swift | 10 +- .../OpenApi/generated/APIs/DefaultAPI.swift | 811 ++++++++++++------ .../generated/Models/CallMissedEvent.swift | 60 ++ .../CallRtmpBroadcastStartedEvent.swift | 42 + .../CallRtmpBroadcastStoppedEvent.swift | 42 + .../generated/Models/CallUserMutedEvent.swift | 46 + .../generated/Models/DeleteCallRequest.swift | 30 + .../generated/Models/DeleteCallResponse.swift | 38 + .../Models/DeleteRecordingResponse.swift | 29 + .../Models/DeleteTranscriptionResponse.swift | 29 + .../generated/Models/GoLiveRequest.swift | 6 +- .../generated/Models/LayoutSettings.swift | 48 ++ .../Models/StartRTMPBroadcastsRequest.swift | 53 ++ .../Models/StartRTMPBroadcastsResponse.swift | 30 + .../StopAllRTMPBroadcastsResponse.swift | 30 + .../Models/StopRTMPBroadcastsResponse.swift | 30 + .../OpenApi/generated/Models/WSEvent.swift | 70 +- Sources/StreamVideo/StreamVideo.swift | 2 +- StreamVideo.xcodeproj/project.pbxproj | 52 ++ 19 files changed, 1161 insertions(+), 297 deletions(-) create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallMissedEvent.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStartedEvent.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStoppedEvent.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallUserMutedEvent.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/DeleteCallRequest.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/DeleteCallResponse.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/DeleteRecordingResponse.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/DeleteTranscriptionResponse.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/LayoutSettings.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsRequest.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsResponse.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/StopAllRTMPBroadcastsResponse.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/StopRTMPBroadcastsResponse.swift diff --git a/Sources/StreamVideo/CallState.swift b/Sources/StreamVideo/CallState.swift index 2a523034c..b3b03c0fa 100644 --- a/Sources/StreamVideo/CallState.swift +++ b/Sources/StreamVideo/CallState.swift @@ -207,8 +207,6 @@ public class CallState: ObservableObject { case .typeHealthCheckEvent: // note: health checks are not relevant for call state sync'ing break - case .typeCallUserMuted: - break case .typeCallDeletedEvent: break case .typeCallHLSBroadcastingFailedEvent: @@ -227,6 +225,14 @@ public class CallState: ObservableObject { transcribing = true case .typeCallTranscriptionStoppedEvent: transcribing = false + case .typeCallMissedEvent: + break + case .typeCallRtmpBroadcastStartedEvent: + break + case .typeCallRtmpBroadcastStoppedEvent: + break + case .typeCallUserMutedEvent: + break } } diff --git a/Sources/StreamVideo/OpenApi/generated/APIs/DefaultAPI.swift b/Sources/StreamVideo/OpenApi/generated/APIs/DefaultAPI.swift index 0c2208c27..b5331d9db 100644 --- a/Sources/StreamVideo/OpenApi/generated/APIs/DefaultAPI.swift +++ b/Sources/StreamVideo/OpenApi/generated/APIs/DefaultAPI.swift @@ -157,12 +157,11 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { return r } - /** Accept Call - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: AcceptCallResponse */ @@ -186,19 +185,19 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Accept Call - POST /video/call/{type}/{id}/accept - - Sends events: - call.accepted Required permissions: - JoinCall - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Sends events: - call.accepted Required permissions: - JoinCall + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Block user on a call - - parameter type: (path) - - parameter id: (path) - - parameter blockUserRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter blockUserRequest: (body) - returns: BlockUserResponse */ @@ -223,21 +222,21 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Block user on a call - POST /video/call/{type}/{id}/block - - Block a user, preventing them from joining the call until they are unblocked. Sends events: - call.blocked_user Required permissions: - BlockUser - - parameter type: (path) - - parameter id: (path) - - parameter blockUserRequest: (body) - - returns: RequestBuilder + - Block a user, preventing them from joining the call until they are unblocked. Sends events: - call.blocked_user Required permissions: - BlockUser + - parameter type: (path) + - parameter id: (path) + - parameter blockUserRequest: (body) + - returns: RequestBuilder */ /** Collect user feedback - - parameter type: (path) - - parameter id: (path) - - parameter session: (path) - - parameter collectUserFeedbackRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter collectUserFeedbackRequest: (body) - returns: CollectUserFeedbackResponse */ @@ -262,11 +261,22 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { try self.jsonDecoder.decode(CollectUserFeedbackResponse.self, from: $0) } } - + /** + Collect user feedback + - POST /video/call/{type}/{id}/feedback/{session} + - Required permissions: - JoinCall + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter collectUserFeedbackRequest: (body) + - returns: RequestBuilder + */ + + /** Create device - - parameter createDeviceRequest: (body) + - parameter createDeviceRequest: (body) - returns: ModelResponse */ @@ -285,16 +295,16 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Create device - POST /video/devices - - Adds a new device to a user, if the same device already exists the call will have no effect - - parameter createDeviceRequest: (body) - - returns: RequestBuilder + - Adds a new device to a user, if the same device already exists the call will have no effect + - parameter createDeviceRequest: (body) + - returns: RequestBuilder */ /** Create Guest - - parameter createGuestRequest: (body) + - parameter createGuestRequest: (body) - returns: CreateGuestResponse */ @@ -313,25 +323,61 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Create Guest - POST /video/guest - - - - parameter createGuestRequest: (body) - - returns: RequestBuilder + - + - parameter createGuestRequest: (body) + - returns: RequestBuilder + */ + + + /** + Delete Call + + - parameter type: (path) + - parameter id: (path) + - parameter deleteCallRequest: (body) + - returns: DeleteCallResponse + */ + + open func deleteCall(type: String, id: String, deleteCallRequest: DeleteCallRequest) async throws -> DeleteCallResponse { + var localVariablePath = "/video/call/{type}/{id}/delete" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "POST", + request: deleteCallRequest + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(DeleteCallResponse.self, from: $0) + } + } + /** + Delete Call + - POST /video/call/{type}/{id}/delete + - Sends events: - call.deleted Required permissions: - DeleteCall + - parameter type: (path) + - parameter id: (path) + - parameter deleteCallRequest: (body) + - returns: RequestBuilder */ /** Delete device - - parameter id: (query) (optional) - - parameter userId: (query) (optional) + - parameter id: (query) - returns: ModelResponse */ - open func deleteDevice(id: String? = nil, userId: String? = nil) async throws -> ModelResponse { + open func deleteDevice(id: String) async throws -> ModelResponse { let localVariablePath = "/video/devices" let queryParams = APIHelper.mapValuesToQueryItems([ - "id": (wrappedValue: id?.encodeToJSON(), isExplode: true), - "user_id": (wrappedValue: userId?.encodeToJSON(), isExplode: true), + "id": (wrappedValue: id.encodeToJSON(), isExplode: true), ]) let urlRequest = try makeRequest( uriPath: localVariablePath, @@ -345,18 +391,107 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Delete device - DELETE /video/devices - - Deletes one device - - parameter id: (query) (optional) - - parameter userId: (query) (optional) - - returns: RequestBuilder + - Deletes one device + - parameter id: (query) + - returns: RequestBuilder + */ + + + /** + Delete recording + + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter filename: (path) + - returns: DeleteRecordingResponse + */ + + open func deleteRecording(type: String, id: String, session: String, filename: String) async throws -> DeleteRecordingResponse { + var localVariablePath = "/video/call/{type}/{id}/{session}/recordings/{filename}" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + let sessionPreEscape = "\(APIHelper.mapValueToPathItem(session))" + let sessionPostEscape = sessionPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{session}", with: sessionPostEscape, options: .literal, range: nil) + let filenamePreEscape = "\(APIHelper.mapValueToPathItem(filename))" + let filenamePostEscape = filenamePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{filename}", with: filenamePostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "DELETE" + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(DeleteRecordingResponse.self, from: $0) + } + } + /** + Delete recording + - DELETE /video/call/{type}/{id}/{session}/recordings/{filename} + - Deletes recording Required permissions: - DeleteRecording + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter filename: (path) + - returns: RequestBuilder + */ + + + /** + Delete transcription + + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter filename: (path) + - returns: DeleteTranscriptionResponse + */ + + open func deleteTranscription(type: String, id: String, session: String, filename: String) async throws -> DeleteTranscriptionResponse { + var localVariablePath = "/video/call/{type}/{id}/{session}/transcriptions/{filename}" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + let sessionPreEscape = "\(APIHelper.mapValueToPathItem(session))" + let sessionPostEscape = sessionPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{session}", with: sessionPostEscape, options: .literal, range: nil) + let filenamePreEscape = "\(APIHelper.mapValueToPathItem(filename))" + let filenamePostEscape = filenamePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{filename}", with: filenamePostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "DELETE" + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(DeleteTranscriptionResponse.self, from: $0) + } + } + /** + Delete transcription + - DELETE /video/call/{type}/{id}/{session}/transcriptions/{filename} + - Deletes transcription Required permissions: - DeleteTranscription + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - parameter filename: (path) + - returns: RequestBuilder */ /** End call - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: EndCallResponse */ @@ -380,26 +515,27 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** End call - POST /video/call/{type}/{id}/mark_ended - - Sends events: - call.ended Required permissions: - EndCall - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Sends events: - call.ended Required permissions: - EndCall + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Get Call - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - parameter connectionId: (query) (optional) - parameter membersLimit: (query) (optional) - parameter ring: (query) (optional) - parameter notify: (query) (optional) + - parameter video: (query) (optional) - returns: GetCallResponse */ - open func getCall(type: String, id: String, connectionId: String? = nil, membersLimit: Int? = nil, ring: Bool? = nil, notify: Bool? = nil) async throws -> GetCallResponse { + open func getCall(type: String, id: String, connectionId: String? = nil, membersLimit: Int? = nil, ring: Bool? = nil, notify: Bool? = nil, video: Bool? = nil) async throws -> GetCallResponse { var localVariablePath = "/video/call/{type}/{id}" let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" @@ -412,6 +548,7 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { "members_limit": (wrappedValue: membersLimit?.encodeToJSON(), isExplode: true), "ring": (wrappedValue: ring?.encodeToJSON(), isExplode: true), "notify": (wrappedValue: notify?.encodeToJSON(), isExplode: true), + "video": (wrappedValue: video?.encodeToJSON(), isExplode: true), ]) let urlRequest = try makeRequest( uriPath: localVariablePath, @@ -425,23 +562,24 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Get Call - GET /video/call/{type}/{id} - - Required permissions: - ReadCall - - parameter type: (path) - - parameter id: (path) + - Required permissions: - ReadCall + - parameter type: (path) + - parameter id: (path) - parameter connectionId: (query) (optional) - parameter membersLimit: (query) (optional) - parameter ring: (query) (optional) - parameter notify: (query) (optional) - - returns: RequestBuilder + - parameter video: (query) (optional) + - returns: RequestBuilder */ /** Get Call Stats - - parameter type: (path) - - parameter id: (path) - - parameter session: (path) + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) - returns: GetCallStatsResponse */ @@ -468,11 +606,11 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Get Call Stats - GET /video/call/{type}/{id}/stats/{session} - - Required permissions: - ReadCallStats - - parameter type: (path) - - parameter id: (path) - - parameter session: (path) - - returns: RequestBuilder + - Required permissions: - ReadCallStats + - parameter type: (path) + - parameter id: (path) + - parameter session: (path) + - returns: RequestBuilder */ @@ -496,17 +634,17 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Get Edges - GET /video/edges - - Returns the list of all edges available for video calls. - - returns: RequestBuilder + - Returns the list of all edges available for video calls. + - returns: RequestBuilder */ /** Get or create a call - - parameter type: (path) - - parameter id: (path) - - parameter getOrCreateCallRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter getOrCreateCallRequest: (body) - parameter connectionId: (query) (optional) - returns: GetOrCreateCallResponse */ @@ -535,21 +673,21 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Get or create a call - POST /video/call/{type}/{id} - - Gets or creates a new call Sends events: - call.created - call.notification - call.ring Required permissions: - CreateCall - ReadCall - UpdateCallSettings - - parameter type: (path) - - parameter id: (path) - - parameter getOrCreateCallRequest: (body) + - Gets or creates a new call Sends events: - call.created - call.notification - call.ring Required permissions: - CreateCall - ReadCall - UpdateCallSettings + - parameter type: (path) + - parameter id: (path) + - parameter getOrCreateCallRequest: (body) - parameter connectionId: (query) (optional) - - returns: RequestBuilder + - returns: RequestBuilder */ /** Set call as live - - parameter type: (path) - - parameter id: (path) - - parameter goLiveRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter goLiveRequest: (body) - returns: GoLiveResponse */ @@ -574,20 +712,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Set call as live - POST /video/call/{type}/{id}/go_live - - Sends events: - call.live_started Required permissions: - UpdateCall - - parameter type: (path) - - parameter id: (path) - - parameter goLiveRequest: (body) - - returns: RequestBuilder + - Sends events: - call.live_started Required permissions: - UpdateCall + - parameter type: (path) + - parameter id: (path) + - parameter goLiveRequest: (body) + - returns: RequestBuilder */ /** Join call - - parameter type: (path) - - parameter id: (path) - - parameter joinCallRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter joinCallRequest: (body) - parameter connectionId: (query) (optional) - returns: JoinCallResponse */ @@ -616,30 +754,26 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Join call - POST /video/call/{type}/{id}/join - - Request to join a call Required permissions: - CreateCall - JoinCall - - parameter type: (path) - - parameter id: (path) - - parameter joinCallRequest: (body) + - Request to join a call Required permissions: - CreateCall - JoinCall + - parameter type: (path) + - parameter id: (path) + - parameter joinCallRequest: (body) - parameter connectionId: (query) (optional) - - returns: RequestBuilder + - returns: RequestBuilder */ /** List devices - - parameter userId: (query) (optional) - returns: ListDevicesResponse */ - open func listDevices(userId: String? = nil) async throws -> ListDevicesResponse { + open func listDevices() async throws -> ListDevicesResponse { let localVariablePath = "/video/devices" - let queryParams = APIHelper.mapValuesToQueryItems([ - "user_id": (wrappedValue: userId?.encodeToJSON(), isExplode: true), - ]) + let urlRequest = try makeRequest( uriPath: localVariablePath, - queryParams: queryParams ?? [], httpMethod: "GET" ) return try await send(request: urlRequest) { @@ -649,17 +783,16 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** List devices - GET /video/devices - - Returns all available devices - - parameter userId: (query) (optional) - - returns: RequestBuilder + - Returns all available devices + - returns: RequestBuilder */ /** List recordings - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: ListRecordingsResponse */ @@ -683,18 +816,18 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** List recordings - GET /video/call/{type}/{id}/recordings - - Lists recordings Required permissions: - ListRecordings - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Lists recordings Required permissions: - ListRecordings + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** List transcriptions - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: ListTranscriptionsResponse */ @@ -718,19 +851,19 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** List transcriptions - GET /video/call/{type}/{id}/transcriptions - - Lists transcriptions Required permissions: - ListTranscriptions - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Lists transcriptions Required permissions: - ListTranscriptions + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Mute users - - parameter type: (path) - - parameter id: (path) - - parameter muteUsersRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter muteUsersRequest: (body) - returns: MuteUsersResponse */ @@ -755,18 +888,46 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Mute users - POST /video/call/{type}/{id}/mute_users - - Mutes users in a call Required permissions: - MuteUsers - - parameter type: (path) - - parameter id: (path) - - parameter muteUsersRequest: (body) - - returns: RequestBuilder + - Mutes users in a call Required permissions: - MuteUsers + - parameter type: (path) + - parameter id: (path) + - parameter muteUsersRequest: (body) + - returns: RequestBuilder + */ + + + /** + Query call members + + - parameter queryMembersRequest: (body) + - returns: QueryMembersResponse + */ + + open func queryMembers(queryMembersRequest: QueryMembersRequest) async throws -> QueryMembersResponse { + let localVariablePath = "/video/call/members" + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "POST", + request: queryMembersRequest + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(QueryMembersResponse.self, from: $0) + } + } + /** + Query call members + - POST /video/call/members + - Query call members with filter query Required permissions: - ReadCall + - parameter queryCallMembersRequest: (body) + - returns: RequestBuilder */ /** Query Call Stats - - parameter queryCallStatsRequest: (body) + - parameter queryCallStatsRequest: (body) - returns: QueryCallStatsResponse */ @@ -785,16 +946,16 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Query Call Stats - POST /video/call/stats - - Required permissions: - ReadCallStats - - parameter queryCallStatsRequest: (body) - - returns: RequestBuilder + - Required permissions: - ReadCallStats + - parameter queryCallStatsRequest: (body) + - returns: RequestBuilder */ /** Query call - - parameter queryCallsRequest: (body) + - parameter queryCallsRequest: (body) - parameter connectionId: (query) (optional) - returns: QueryCallsResponse */ @@ -817,38 +978,10 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Query call - POST /video/calls - - Query calls with filter query Required permissions: - ReadCall - - parameter queryCallsRequest: (body) + - Query calls with filter query Required permissions: - ReadCall + - parameter queryCallsRequest: (body) - parameter connectionId: (query) (optional) - - returns: RequestBuilder - */ - - - /** - Query call members - - - parameter queryMembersRequest: (body) - - returns: QueryMembersResponse - */ - - open func queryMembers(queryMembersRequest: QueryMembersRequest) async throws -> QueryMembersResponse { - let localVariablePath = "/video/call/members" - - let urlRequest = try makeRequest( - uriPath: localVariablePath, - httpMethod: "POST", - request: queryMembersRequest - ) - return try await send(request: urlRequest) { - try self.jsonDecoder.decode(QueryMembersResponse.self, from: $0) - } - } - /** - Query call members - - POST /video/call/members - - Query call members with filter query Required permissions: - ReadCall - - parameter queryMembersRequest: (body) - - returns: RequestBuilder + - returns: RequestBuilder */ @@ -879,13 +1012,23 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { try self.jsonDecoder.decode(RejectCallResponse.self, from: $0) } } + /** + Reject Call + - POST /video/call/{type}/{id}/reject + - Sends events: - call.rejected Required permissions: - JoinCall + - parameter type: (path) + - parameter id: (path) + - parameter rejectCallRequest: (body) + - returns: RequestBuilder + */ + /** Request permission - - parameter type: (path) - - parameter id: (path) - - parameter requestPermissionRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter requestPermissionRequest: (body) - returns: RequestPermissionResponse */ @@ -910,11 +1053,11 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Request permission - POST /video/call/{type}/{id}/request_permission - - Request permission to perform an action Sends events: - call.permission_request - - parameter type: (path) - - parameter id: (path) - - parameter requestPermissionRequest: (body) - - returns: RequestBuilder + - Request permission to perform an action Sends events: - call.permission_request + - parameter type: (path) + - parameter id: (path) + - parameter requestPermissionRequest: (body) + - returns: RequestBuilder */ @@ -948,20 +1091,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Send custom event - POST /video/call/{type}/{id}/event - - Sends custom event to the call Sends events: - custom Required permissions: - SendEvent - - parameter type: (path) - - parameter id: (path) - - parameter sendEventRequest: (body) - - returns: RequestBuilder + - Sends custom event to the call Sends events: - custom Required permissions: - SendEvent + - parameter type: (path) + - parameter id: (path) + - parameter sendCallEventRequest: (body) + - returns: RequestBuilder */ /** Send reaction to the call - - parameter type: (path) - - parameter id: (path) - - parameter sendReactionRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter sendReactionRequest: (body) - returns: SendReactionResponse */ @@ -986,19 +1129,19 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Send reaction to the call - POST /video/call/{type}/{id}/reaction - - Sends reaction to the call Sends events: - call.reaction_new Required permissions: - CreateCallReaction - - parameter type: (path) - - parameter id: (path) - - parameter sendReactionRequest: (body) - - returns: RequestBuilder + - Sends reaction to the call Sends events: - call.reaction_new Required permissions: - CreateCallReaction + - parameter type: (path) + - parameter id: (path) + - parameter sendReactionRequest: (body) + - returns: RequestBuilder */ /** Start HLS broadcasting - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: StartHLSBroadcastingResponse */ @@ -1022,19 +1165,57 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Start HLS broadcasting - POST /video/call/{type}/{id}/start_broadcasting - - Starts HLS broadcasting Required permissions: - StartBroadcasting - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Starts HLS broadcasting Required permissions: - StartBroadcasting + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder + */ + + + /** + Start RTMP broadcasts + + - parameter type: (path) + - parameter id: (path) + - parameter startRTMPBroadcastsRequest: (body) + - returns: StartRTMPBroadcastsResponse + */ + + open func startRTMPBroadcast(type: String, id: String, startRTMPBroadcastsRequest: StartRTMPBroadcastsRequest) async throws -> StartRTMPBroadcastsResponse { + var localVariablePath = "/video/call/{type}/{id}/rtmp_broadcasts" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "POST", + request: startRTMPBroadcastsRequest + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(StartRTMPBroadcastsResponse.self, from: $0) + } + } + /** + Start RTMP broadcasts + - POST /video/call/{type}/{id}/rtmp_broadcasts + - Starts RTMP broadcasts for the provided RTMP destinations Required permissions: - StartBroadcasting + - parameter type: (path) + - parameter id: (path) + - parameter startRTMPBroadcastsRequest: (body) + - returns: RequestBuilder */ /** Start recording - - parameter type: (path) - - parameter id: (path) - - parameter startRecordingRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter startRecordingRequest: (body) - returns: StartRecordingResponse */ @@ -1059,20 +1240,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Start recording - POST /video/call/{type}/{id}/start_recording - - Starts recording Sends events: - call.recording_started Required permissions: - StartRecording - - parameter type: (path) - - parameter id: (path) - - parameter startRecordingRequest: (body) - - returns: RequestBuilder + - Starts recording Sends events: - call.recording_started Required permissions: - StartRecording + - parameter type: (path) + - parameter id: (path) + - parameter startRecordingRequest: (body) + - returns: RequestBuilder */ /** Start transcription - - parameter type: (path) - - parameter id: (path) - - parameter startTranscriptionRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter startTranscriptionRequest: (body) - returns: StartTranscriptionResponse */ @@ -1097,19 +1278,54 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Start transcription - POST /video/call/{type}/{id}/start_transcription - - Starts transcription Required permissions: - StartTranscription - - parameter type: (path) - - parameter id: (path) - - parameter startTranscriptionRequest: (body) - - returns: RequestBuilder + - Starts transcription Required permissions: - StartTranscription + - parameter type: (path) + - parameter id: (path) + - parameter startTranscriptionRequest: (body) + - returns: RequestBuilder + */ + + + /** + Stop all RTMP broadcasts for a call + + - parameter type: (path) + - parameter id: (path) + - returns: StopAllRTMPBroadcastsResponse + */ + + open func stopAllRTMPBroadcasts(type: String, id: String) async throws -> StopAllRTMPBroadcastsResponse { + var localVariablePath = "/video/call/{type}/{id}/rtmp_broadcasts/stop" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "POST" + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(StopAllRTMPBroadcastsResponse.self, from: $0) + } + } + /** + Stop all RTMP broadcasts for a call + - POST /video/call/{type}/{id}/rtmp_broadcasts/stop + - Stop all RTMP broadcasts for the provided call Required permissions: - StopBroadcasting + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Stop HLS broadcasting - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: StopHLSBroadcastingResponse */ @@ -1133,18 +1349,18 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Stop HLS broadcasting - POST /video/call/{type}/{id}/stop_broadcasting - - Stops HLS broadcasting Required permissions: - StopBroadcasting - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Stops HLS broadcasting Required permissions: - StopBroadcasting + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Set call as not live - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: StopLiveResponse */ @@ -1168,18 +1384,61 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Set call as not live - POST /video/call/{type}/{id}/stop_live - - Sends events: - call.updated Required permissions: - UpdateCall - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Sends events: - call.updated Required permissions: - UpdateCall + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder + */ + + + /** + Stop RTMP broadcasts + + - parameter type: (path) + - parameter id: (path) + - parameter name: (path) + - parameter body: (body) + - returns: StopRTMPBroadcastsResponse + */ + + open func stopRTMPBroadcast(type: String, id: String, name: String, body: [String: RawJSON]) async throws -> StopRTMPBroadcastsResponse { + var localVariablePath = "/video/call/{type}/{id}/rtmp_broadcasts/{name}/stop" + let typePreEscape = "\(APIHelper.mapValueToPathItem(type))" + let typePostEscape = typePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{type}", with: typePostEscape, options: .literal, range: nil) + let idPreEscape = "\(APIHelper.mapValueToPathItem(id))" + let idPostEscape = idPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{id}", with: idPostEscape, options: .literal, range: nil) + let namePreEscape = "\(APIHelper.mapValueToPathItem(name))" + let namePostEscape = namePreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + localVariablePath = localVariablePath.replacingOccurrences(of: "{name}", with: namePostEscape, options: .literal, range: nil) + + let urlRequest = try makeRequest( + uriPath: localVariablePath, + httpMethod: "POST", + request: body + ) + return try await send(request: urlRequest) { + try self.jsonDecoder.decode(StopRTMPBroadcastsResponse.self, from: $0) + } + } + /** + Stop RTMP broadcasts + - POST /video/call/{type}/{id}/rtmp_broadcasts/{name}/stop + - Stop RTMP broadcasts for the provided RTMP destinations Required permissions: - StopBroadcasting + - parameter type: (path) + - parameter id: (path) + - parameter name: (path) + - parameter body: (body) + - returns: RequestBuilder */ /** Stop recording - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: StopRecordingResponse */ @@ -1203,18 +1462,18 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Stop recording - POST /video/call/{type}/{id}/stop_recording - - Stops recording Sends events: - call.recording_stopped Required permissions: - StopRecording - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Stops recording Sends events: - call.recording_stopped Required permissions: - StopRecording + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Stop transcription - - parameter type: (path) - - parameter id: (path) + - parameter type: (path) + - parameter id: (path) - returns: StopTranscriptionResponse */ @@ -1238,19 +1497,19 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Stop transcription - POST /video/call/{type}/{id}/stop_transcription - - Stops transcription Sends events: - call.transcription_stopped Required permissions: - StopTranscription - - parameter type: (path) - - parameter id: (path) - - returns: RequestBuilder + - Stops transcription Sends events: - call.transcription_stopped Required permissions: - StopTranscription + - parameter type: (path) + - parameter id: (path) + - returns: RequestBuilder */ /** Unblocks user on a call - - parameter type: (path) - - parameter id: (path) - - parameter unblockUserRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter unblockUserRequest: (body) - returns: UnblockUserResponse */ @@ -1275,20 +1534,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Unblocks user on a call - POST /video/call/{type}/{id}/unblock - - Removes the block for a user on a call. The user will be able to join the call again. Sends events: - call.unblocked_user Required permissions: - BlockUser - - parameter type: (path) - - parameter id: (path) - - parameter unblockUserRequest: (body) - - returns: RequestBuilder + - Removes the block for a user on a call. The user will be able to join the call again. Sends events: - call.unblocked_user Required permissions: - BlockUser + - parameter type: (path) + - parameter id: (path) + - parameter unblockUserRequest: (body) + - returns: RequestBuilder */ /** Update Call - - parameter type: (path) - - parameter id: (path) - - parameter updateCallRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter updateCallRequest: (body) - returns: UpdateCallResponse */ @@ -1313,20 +1572,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Update Call - PATCH /video/call/{type}/{id} - - Sends events: - call.updated Required permissions: - UpdateCall - - parameter type: (path) - - parameter id: (path) - - parameter updateCallRequest: (body) - - returns: RequestBuilder + - Sends events: - call.updated Required permissions: - UpdateCall + - parameter type: (path) + - parameter id: (path) + - parameter updateCallRequest: (body) + - returns: RequestBuilder */ /** Update Call Member - - parameter type: (path) - - parameter id: (path) - - parameter updateCallMembersRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter updateCallMembersRequest: (body) - returns: UpdateCallMembersResponse */ @@ -1351,20 +1610,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Update Call Member - POST /video/call/{type}/{id}/members - - Sends events: - call.member_added - call.member_removed - call.member_updated Required permissions: - RemoveCallMember - UpdateCallMember - UpdateCallMemberRole - - parameter type: (path) - - parameter id: (path) - - parameter updateCallMembersRequest: (body) - - returns: RequestBuilder + - Sends events: - call.member_added - call.member_removed - call.member_updated Required permissions: - RemoveCallMember - UpdateCallMember - UpdateCallMemberRole + - parameter type: (path) + - parameter id: (path) + - parameter updateCallMembersRequest: (body) + - returns: RequestBuilder */ /** Update user permissions - - parameter type: (path) - - parameter id: (path) - - parameter updateUserPermissionsRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter updateUserPermissionsRequest: (body) - returns: UpdateUserPermissionsResponse */ @@ -1389,20 +1648,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Update user permissions - POST /video/call/{type}/{id}/user_permissions - - Updates user permissions Sends events: - call.permissions_updated Required permissions: - UpdateCallPermissions - - parameter type: (path) - - parameter id: (path) - - parameter updateUserPermissionsRequest: (body) - - returns: RequestBuilder + - Updates user permissions Sends events: - call.permissions_updated Required permissions: - UpdateCallPermissions + - parameter type: (path) + - parameter id: (path) + - parameter updateUserPermissionsRequest: (body) + - returns: RequestBuilder */ /** Pin - - parameter type: (path) - - parameter id: (path) - - parameter pinRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter pinRequest: (body) - returns: PinResponse */ @@ -1427,20 +1686,20 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { /** Pin - POST /video/call/{type}/{id}/pin - - Pins a track for all users in the call. Required permissions: - PinCallTrack - - parameter type: (path) - - parameter id: (path) - - parameter pinRequest: (body) - - returns: RequestBuilder + - Pins a track for all users in the call. Required permissions: - PinCallTrack + - parameter type: (path) + - parameter id: (path) + - parameter pinRequest: (body) + - returns: RequestBuilder */ /** Unpin - - parameter type: (path) - - parameter id: (path) - - parameter unpinRequest: (body) + - parameter type: (path) + - parameter id: (path) + - parameter unpinRequest: (body) - returns: UnpinResponse */ @@ -1462,16 +1721,6 @@ open class DefaultAPI: DefaultAPIEndpoints, @unchecked Sendable { try self.jsonDecoder.decode(UnpinResponse.self, from: $0) } } - /** - Unpin - - POST /video/call/{type}/{id}/unpin - - Unpins a track for all users in the call. Required permissions: - PinCallTrack - - parameter type: (path) - - parameter id: (path) - - parameter unpinRequest: (body) - - returns: RequestBuilder - */ - } protocol DefaultAPIEndpoints { @@ -1492,13 +1741,22 @@ protocol DefaultAPIEndpoints { func createGuest(createGuestRequest: CreateGuestRequest) async throws -> CreateGuestResponse - func deleteDevice(id: String?, userId: String?) async throws -> ModelResponse + func deleteCall(type: String, id: String, deleteCallRequest: DeleteCallRequest) async throws -> DeleteCallResponse + + + func deleteDevice(id: String) async throws -> ModelResponse + + + func deleteRecording(type: String, id: String, session: String, filename: String) async throws -> DeleteRecordingResponse + + + func deleteTranscription(type: String, id: String, session: String, filename: String) async throws -> DeleteTranscriptionResponse func endCall(type: String, id: String) async throws -> EndCallResponse - func getCall(type: String, id: String, connectionId: String?, membersLimit: Int?, ring: Bool?, notify: Bool?) async throws -> GetCallResponse + func getCall(type: String, id: String, connectionId: String?, membersLimit: Int?, ring: Bool?, notify: Bool?, video: Bool?) async throws -> GetCallResponse func getCallStats(type: String, id: String, session: String) async throws -> GetCallStatsResponse @@ -1516,7 +1774,7 @@ protocol DefaultAPIEndpoints { func joinCall(type: String, id: String, joinCallRequest: JoinCallRequest, connectionId: String?) async throws -> JoinCallResponse - func listDevices(userId: String?) async throws -> ListDevicesResponse + func listDevices() async throws -> ListDevicesResponse func listRecordings(type: String, id: String) async throws -> ListRecordingsResponse @@ -1552,18 +1810,27 @@ protocol DefaultAPIEndpoints { func startHLSBroadcasting(type: String, id: String) async throws -> StartHLSBroadcastingResponse + func startRTMPBroadcast(type: String, id: String, startRTMPBroadcastsRequest: StartRTMPBroadcastsRequest) async throws -> StartRTMPBroadcastsResponse + + func startRecording(type: String, id: String, startRecordingRequest: StartRecordingRequest) async throws -> StartRecordingResponse func startTranscription(type: String, id: String, startTranscriptionRequest: StartTranscriptionRequest) async throws -> StartTranscriptionResponse + func stopAllRTMPBroadcasts(type: String, id: String) async throws -> StopAllRTMPBroadcastsResponse + + func stopHLSBroadcasting(type: String, id: String) async throws -> StopHLSBroadcastingResponse func stopLive(type: String, id: String) async throws -> StopLiveResponse + func stopRTMPBroadcast(type: String, id: String, name: String, body: [String: RawJSON]) async throws -> StopRTMPBroadcastsResponse + + func stopRecording(type: String, id: String) async throws -> StopRecordingResponse diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallMissedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallMissedEvent.swift new file mode 100644 index 000000000..374688f87 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallMissedEvent.swift @@ -0,0 +1,60 @@ +// +// CallMissedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent to call members who did not accept/reject/join the call to notify they missed the call */ + +public struct CallMissedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + public var call: CallResponse + public var callCid: String + public var createdAt: Date + /** List of members who missed the call */ + public var members: [MemberResponse] + public var notifyUser: Bool + /** Call session ID */ + public var sessionId: String + /** The type of event: \"call.notification\" in this case */ + public var type: String = "call.missed" + public var user: UserResponse + + public init(call: CallResponse, callCid: String, createdAt: Date, members: [MemberResponse], notifyUser: Bool, sessionId: String, type: String = "call.missed", user: UserResponse) { + self.call = call + self.callCid = callCid + self.createdAt = createdAt + self.members = members + self.notifyUser = notifyUser + self.sessionId = sessionId + self.type = type + self.user = user + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case call + case callCid = "call_cid" + case createdAt = "created_at" + case members + case notifyUser = "notify_user" + case sessionId = "session_id" + case type + case user + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(call, forKey: .call) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(members, forKey: .members) + try container.encode(notifyUser, forKey: .notifyUser) + try container.encode(sessionId, forKey: .sessionId) + try container.encode(type, forKey: .type) + try container.encode(user, forKey: .user) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStartedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStartedEvent.swift new file mode 100644 index 000000000..bfb6e30de --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStartedEvent.swift @@ -0,0 +1,42 @@ +// +// CallRtmpBroadcastStartedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent when RTMP broadcast has started */ + +public struct CallRtmpBroadcastStartedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + public var callCid: String + public var createdAt: Date + public var name: String + /** The type of event: \"call.rtmp_broadcast_started\" in this case */ + public var type: String = "call.rtmp_broadcast_started" + + public init(callCid: String, createdAt: Date, name: String, type: String = "call.rtmp_broadcast_started") { + self.callCid = callCid + self.createdAt = createdAt + self.name = name + self.type = type + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case callCid = "call_cid" + case createdAt = "created_at" + case name + case type + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(name, forKey: .name) + try container.encode(type, forKey: .type) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStoppedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStoppedEvent.swift new file mode 100644 index 000000000..23a390c36 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastStoppedEvent.swift @@ -0,0 +1,42 @@ +// +// CallRtmpBroadcastStoppedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent when RTMP broadcast has stopped */ + +public struct CallRtmpBroadcastStoppedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + public var callCid: String + public var createdAt: Date + public var name: String + /** The type of event: \"call.rtmp_broadcast_stopped\" in this case */ + public var type: String = "call.rtmp_broadcast_stopped" + + public init(callCid: String, createdAt: Date, name: String, type: String = "call.rtmp_broadcast_stopped") { + self.callCid = callCid + self.createdAt = createdAt + self.name = name + self.type = type + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case callCid = "call_cid" + case createdAt = "created_at" + case name + case type + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(name, forKey: .name) + try container.encode(type, forKey: .type) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallUserMutedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallUserMutedEvent.swift new file mode 100644 index 000000000..8dae55d65 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallUserMutedEvent.swift @@ -0,0 +1,46 @@ +// +// CallUserMutedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent when a call member is muted */ + +public struct CallUserMutedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + public var callCid: String + public var createdAt: Date + public var fromUserId: String + public var mutedUserIds: [String] + /** The type of event: \"call.user_muted\" in this case */ + public var type: String = "call.user_muted" + + public init(callCid: String, createdAt: Date, fromUserId: String, mutedUserIds: [String], type: String = "call.user_muted") { + self.callCid = callCid + self.createdAt = createdAt + self.fromUserId = fromUserId + self.mutedUserIds = mutedUserIds + self.type = type + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case callCid = "call_cid" + case createdAt = "created_at" + case fromUserId = "from_user_id" + case mutedUserIds = "muted_user_ids" + case type + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(fromUserId, forKey: .fromUserId) + try container.encode(mutedUserIds, forKey: .mutedUserIds) + try container.encode(type, forKey: .type) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallRequest.swift b/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallRequest.swift new file mode 100644 index 000000000..5ae8074b5 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallRequest.swift @@ -0,0 +1,30 @@ +// +// DeleteCallRequest.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct DeleteCallRequest: Codable, JSONEncodable, Hashable { + /** if true the call will be hard deleted along with all related data */ + public var hard: Bool? + + public init(hard: Bool? = nil) { + self.hard = hard + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case hard + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(hard, forKey: .hard) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallResponse.swift new file mode 100644 index 000000000..6b7dae0a6 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/DeleteCallResponse.swift @@ -0,0 +1,38 @@ +// +// DeleteCallResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct DeleteCallResponse: Codable, JSONEncodable, Hashable { + public var call: CallResponse + /** Duration of the request in milliseconds */ + public var duration: String + public var taskId: String? + + public init(call: CallResponse, duration: String, taskId: String? = nil) { + self.call = call + self.duration = duration + self.taskId = taskId + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case call + case duration + case taskId = "task_id" + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(call, forKey: .call) + try container.encode(duration, forKey: .duration) + try container.encodeIfPresent(taskId, forKey: .taskId) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/DeleteRecordingResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/DeleteRecordingResponse.swift new file mode 100644 index 000000000..8bb982434 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/DeleteRecordingResponse.swift @@ -0,0 +1,29 @@ +// +// DeleteRecordingResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct DeleteRecordingResponse: Codable, JSONEncodable, Hashable { + public var duration: String + + public init(duration: String) { + self.duration = duration + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case duration + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(duration, forKey: .duration) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/DeleteTranscriptionResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/DeleteTranscriptionResponse.swift new file mode 100644 index 000000000..62f5b5ef8 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/DeleteTranscriptionResponse.swift @@ -0,0 +1,29 @@ +// +// DeleteTranscriptionResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct DeleteTranscriptionResponse: Codable, JSONEncodable, Hashable { + public var duration: String + + public init(duration: String) { + self.duration = duration + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case duration + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(duration, forKey: .duration) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/GoLiveRequest.swift b/Sources/StreamVideo/OpenApi/generated/Models/GoLiveRequest.swift index b1836568e..dc06eb434 100644 --- a/Sources/StreamVideo/OpenApi/generated/Models/GoLiveRequest.swift +++ b/Sources/StreamVideo/OpenApi/generated/Models/GoLiveRequest.swift @@ -12,13 +12,15 @@ public struct GoLiveRequest: Codable, JSONEncodable, Hashable { public var recordingStorageName: String? public var startHls: Bool? public var startRecording: Bool? + public var startRtmpBroadcasts: Bool? public var startTranscription: Bool? public var transcriptionStorageName: String? - public init(recordingStorageName: String? = nil, startHls: Bool? = nil, startRecording: Bool? = nil, startTranscription: Bool? = nil, transcriptionStorageName: String? = nil) { + public init(recordingStorageName: String? = nil, startHls: Bool? = nil, startRecording: Bool? = nil, startRtmpBroadcasts: Bool? = nil, startTranscription: Bool? = nil, transcriptionStorageName: String? = nil) { self.recordingStorageName = recordingStorageName self.startHls = startHls self.startRecording = startRecording + self.startRtmpBroadcasts = startRtmpBroadcasts self.startTranscription = startTranscription self.transcriptionStorageName = transcriptionStorageName } @@ -27,6 +29,7 @@ public struct GoLiveRequest: Codable, JSONEncodable, Hashable { case recordingStorageName = "recording_storage_name" case startHls = "start_hls" case startRecording = "start_recording" + case startRtmpBroadcasts = "start_rtmp_broadcasts" case startTranscription = "start_transcription" case transcriptionStorageName = "transcription_storage_name" } @@ -38,6 +41,7 @@ public struct GoLiveRequest: Codable, JSONEncodable, Hashable { try container.encodeIfPresent(recordingStorageName, forKey: .recordingStorageName) try container.encodeIfPresent(startHls, forKey: .startHls) try container.encodeIfPresent(startRecording, forKey: .startRecording) + try container.encodeIfPresent(startRtmpBroadcasts, forKey: .startRtmpBroadcasts) try container.encodeIfPresent(startTranscription, forKey: .startTranscription) try container.encodeIfPresent(transcriptionStorageName, forKey: .transcriptionStorageName) } diff --git a/Sources/StreamVideo/OpenApi/generated/Models/LayoutSettings.swift b/Sources/StreamVideo/OpenApi/generated/Models/LayoutSettings.swift new file mode 100644 index 000000000..1ec07c2f1 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/LayoutSettings.swift @@ -0,0 +1,48 @@ +// +// LayoutSettings.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct LayoutSettings: Codable, JSONEncodable, Hashable { + public enum Name: String, Codable, CaseIterable { + case spotlight = "spotlight" + case grid = "grid" + case singleParticipant = "single-participant" + case mobile = "mobile" + case custom = "custom" + } + public var externalAppUrl: String? + public var externalCssUrl: String? + public var name: Name + public var options: [String: RawJSON]? + + public init(externalAppUrl: String? = nil, externalCssUrl: String? = nil, name: Name, options: [String: RawJSON]? = nil) { + self.externalAppUrl = externalAppUrl + self.externalCssUrl = externalCssUrl + self.name = name + self.options = options + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case externalAppUrl = "external_app_url" + case externalCssUrl = "external_css_url" + case name + case options + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(externalAppUrl, forKey: .externalAppUrl) + try container.encodeIfPresent(externalCssUrl, forKey: .externalCssUrl) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(options, forKey: .options) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsRequest.swift b/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsRequest.swift new file mode 100644 index 000000000..46861c6af --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsRequest.swift @@ -0,0 +1,53 @@ +// +// StartRTMPBroadcastsRequest.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct StartRTMPBroadcastsRequest: Codable, JSONEncodable, Hashable { + public var layout: LayoutSettings? + public var name: String + public var password: String? + public var quality: String? + public var streamKey: String? + public var streamUrl: String + public var username: String? + + public init(layout: LayoutSettings? = nil, name: String, password: String? = nil, quality: String? = nil, streamKey: String? = nil, streamUrl: String, username: String? = nil) { + self.layout = layout + self.name = name + self.password = password + self.quality = quality + self.streamKey = streamKey + self.streamUrl = streamUrl + self.username = username + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case layout + case name + case password + case quality + case streamKey = "stream_key" + case streamUrl = "stream_url" + case username + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(layout, forKey: .layout) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(password, forKey: .password) + try container.encodeIfPresent(quality, forKey: .quality) + try container.encodeIfPresent(streamKey, forKey: .streamKey) + try container.encode(streamUrl, forKey: .streamUrl) + try container.encodeIfPresent(username, forKey: .username) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsResponse.swift new file mode 100644 index 000000000..a016247cf --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/StartRTMPBroadcastsResponse.swift @@ -0,0 +1,30 @@ +// +// StartRTMPBroadcastsResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct StartRTMPBroadcastsResponse: Codable, JSONEncodable, Hashable { + /** Duration of the request in milliseconds */ + public var duration: String + + public init(duration: String) { + self.duration = duration + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case duration + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(duration, forKey: .duration) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/StopAllRTMPBroadcastsResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/StopAllRTMPBroadcastsResponse.swift new file mode 100644 index 000000000..7c0a6beb7 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/StopAllRTMPBroadcastsResponse.swift @@ -0,0 +1,30 @@ +// +// StopAllRTMPBroadcastsResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct StopAllRTMPBroadcastsResponse: Codable, JSONEncodable, Hashable { + /** Duration of the request in milliseconds */ + public var duration: String + + public init(duration: String) { + self.duration = duration + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case duration + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(duration, forKey: .duration) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/StopRTMPBroadcastsResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/StopRTMPBroadcastsResponse.swift new file mode 100644 index 000000000..b75325b35 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/StopRTMPBroadcastsResponse.swift @@ -0,0 +1,30 @@ +// +// StopRTMPBroadcastsResponse.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation + + +public struct StopRTMPBroadcastsResponse: Codable, JSONEncodable, Hashable { + /** Duration of the request in milliseconds */ + public var duration: String + + public init(duration: String) { + self.duration = duration + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case duration + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(duration, forKey: .duration) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift index 507c4ec4e..29efbdc3b 100644 --- a/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift +++ b/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift @@ -28,6 +28,7 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { case typeCallMemberRemovedEvent(CallMemberRemovedEvent) case typeCallMemberUpdatedEvent(CallMemberUpdatedEvent) case typeCallMemberUpdatedPermissionEvent(CallMemberUpdatedPermissionEvent) + case typeCallMissedEvent(CallMissedEvent) case typeCallNotificationEvent(CallNotificationEvent) case typeCallReactionEvent(CallReactionEvent) case typeCallRecordingFailedEvent(CallRecordingFailedEvent) @@ -36,6 +37,8 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { case typeCallRecordingStoppedEvent(CallRecordingStoppedEvent) case typeCallRejectedEvent(CallRejectedEvent) case typeCallRingEvent(CallRingEvent) + case typeCallRtmpBroadcastStartedEvent(CallRtmpBroadcastStartedEvent) + case typeCallRtmpBroadcastStoppedEvent(CallRtmpBroadcastStoppedEvent) case typeCallSessionEndedEvent(CallSessionEndedEvent) case typeCallSessionParticipantJoinedEvent(CallSessionParticipantJoinedEvent) case typeCallSessionParticipantLeftEvent(CallSessionParticipantLeftEvent) @@ -45,14 +48,13 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { case typeCallTranscriptionStartedEvent(CallTranscriptionStartedEvent) case typeCallTranscriptionStoppedEvent(CallTranscriptionStoppedEvent) case typeCallUpdatedEvent(CallUpdatedEvent) - case typeCallUserMuted(CallUserMuted) + case typeCallUserMutedEvent(CallUserMutedEvent) case typeClosedCaptionEvent(ClosedCaptionEvent) case typeConnectedEvent(ConnectedEvent) case typeConnectionErrorEvent(ConnectionErrorEvent) case typePermissionRequestEvent(PermissionRequestEvent) case typeUnblockedUserEvent(UnblockedUserEvent) case typeUpdatedCallPermissionsEvent(UpdatedCallPermissionsEvent) - public var type: String { switch self { case .typeBlockedUserEvent(let value): @@ -81,6 +83,8 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value.type case .typeCallMemberUpdatedPermissionEvent(let value): return value.type + case .typeCallMissedEvent(let value): + return value.type case .typeCallNotificationEvent(let value): return value.type case .typeCallReactionEvent(let value): @@ -97,6 +101,10 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value.type case .typeCallRingEvent(let value): return value.type + case .typeCallRtmpBroadcastStartedEvent(let value): + return value.type + case .typeCallRtmpBroadcastStoppedEvent(let value): + return value.type case .typeCallSessionEndedEvent(let value): return value.type case .typeCallSessionParticipantJoinedEvent(let value): @@ -115,7 +123,7 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value.type case .typeCallUpdatedEvent(let value): return value.type - case .typeCallUserMuted(let value): + case .typeCallUserMutedEvent(let value): return value.type case .typeClosedCaptionEvent(let value): return value.type @@ -123,16 +131,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value.type case .typeConnectionErrorEvent(let value): return value.type + case .typeCustomVideoEvent(let value): + return value.type + case .typeHealthCheckEvent(let value): + return value.type case .typePermissionRequestEvent(let value): return value.type case .typeUnblockedUserEvent(let value): return value.type case .typeUpdatedCallPermissionsEvent(let value): return value.type - case .typeHealthCheckEvent(let value): - return value.type - case .typeCustomVideoEvent(let value): - return value.type } } public var rawValue: Event { @@ -163,6 +171,8 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value case .typeCallMemberUpdatedPermissionEvent(let value): return value + case .typeCallMissedEvent(let value): + return value case .typeCallNotificationEvent(let value): return value case .typeCallReactionEvent(let value): @@ -179,6 +189,10 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value case .typeCallRingEvent(let value): return value + case .typeCallRtmpBroadcastStartedEvent(let value): + return value + case .typeCallRtmpBroadcastStoppedEvent(let value): + return value case .typeCallSessionEndedEvent(let value): return value case .typeCallSessionParticipantJoinedEvent(let value): @@ -197,7 +211,7 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value case .typeCallUpdatedEvent(let value): return value - case .typeCallUserMuted(let value): + case .typeCallUserMutedEvent(let value): return value case .typeClosedCaptionEvent(let value): return value @@ -205,16 +219,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value case .typeConnectionErrorEvent(let value): return value + case .typeCustomVideoEvent(let value): + return value + case .typeHealthCheckEvent(let value): + return value case .typePermissionRequestEvent(let value): return value case .typeUnblockedUserEvent(let value): return value case .typeUpdatedCallPermissionsEvent(let value): return value - case .typeHealthCheckEvent(let value): - return value - case .typeCustomVideoEvent(let value): - return value } } public func encode(to encoder: Encoder) throws { @@ -246,6 +260,8 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { try container.encode(value) case .typeCallMemberUpdatedPermissionEvent(let value): try container.encode(value) + case .typeCallMissedEvent(let value): + try container.encode(value) case .typeCallNotificationEvent(let value): try container.encode(value) case .typeCallReactionEvent(let value): @@ -262,6 +278,10 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { try container.encode(value) case .typeCallRingEvent(let value): try container.encode(value) + case .typeCallRtmpBroadcastStartedEvent(let value): + try container.encode(value) + case .typeCallRtmpBroadcastStoppedEvent(let value): + try container.encode(value) case .typeCallSessionEndedEvent(let value): try container.encode(value) case .typeCallSessionParticipantJoinedEvent(let value): @@ -280,7 +300,7 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { try container.encode(value) case .typeCallUpdatedEvent(let value): try container.encode(value) - case .typeCallUserMuted(let value): + case .typeCallUserMutedEvent(let value): try container.encode(value) case .typeClosedCaptionEvent(let value): try container.encode(value) @@ -288,16 +308,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { try container.encode(value) case .typeConnectionErrorEvent(let value): try container.encode(value) + case .typeCustomVideoEvent(let value): + try container.encode(value) + case .typeHealthCheckEvent(let value): + try container.encode(value) case .typePermissionRequestEvent(let value): try container.encode(value) case .typeUnblockedUserEvent(let value): try container.encode(value) case .typeUpdatedCallPermissionsEvent(let value): try container.encode(value) - case .typeHealthCheckEvent(let value): - try container.encode(value) - case .typeCustomVideoEvent(let value): - try container.encode(value) } } @@ -346,6 +366,9 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } else if dto.type == "call.member_updated_permission" { let value = try container.decode(CallMemberUpdatedPermissionEvent.self) self = .typeCallMemberUpdatedPermissionEvent(value) + } else if dto.type == "call.missed" { + let value = try container.decode(CallMissedEvent.self) + self = .typeCallMissedEvent(value) } else if dto.type == "call.notification" { let value = try container.decode(CallNotificationEvent.self) self = .typeCallNotificationEvent(value) @@ -376,6 +399,12 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } else if dto.type == "call.ring" { let value = try container.decode(CallRingEvent.self) self = .typeCallRingEvent(value) + } else if dto.type == "call.rtmp_broadcast_started" { + let value = try container.decode(CallRtmpBroadcastStartedEvent.self) + self = .typeCallRtmpBroadcastStartedEvent(value) + } else if dto.type == "call.rtmp_broadcast_stopped" { + let value = try container.decode(CallRtmpBroadcastStoppedEvent.self) + self = .typeCallRtmpBroadcastStoppedEvent(value) } else if dto.type == "call.session_ended" { let value = try container.decode(CallSessionEndedEvent.self) self = .typeCallSessionEndedEvent(value) @@ -407,8 +436,8 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { let value = try container.decode(CallUpdatedEvent.self) self = .typeCallUpdatedEvent(value) } else if dto.type == "call.user_muted" { - let value = try container.decode(CallUserMuted.self) - self = .typeCallUserMuted(value) + let value = try container.decode(CallUserMutedEvent.self) + self = .typeCallUserMutedEvent(value) } else if dto.type == "connection.error" { let value = try container.decode(ConnectionErrorEvent.self) self = .typeConnectionErrorEvent(value) @@ -421,8 +450,7 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } else if dto.type == "health.check" { let value = try container.decode(HealthCheckEvent.self) self = .typeHealthCheckEvent(value) - } - else { + } else { throw DecodingError.typeMismatch(Self.Type.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode instance of WSEvent")) } } diff --git a/Sources/StreamVideo/StreamVideo.swift b/Sources/StreamVideo/StreamVideo.swift index b633d3ea4..ee0c2b92e 100644 --- a/Sources/StreamVideo/StreamVideo.swift +++ b/Sources/StreamVideo/StreamVideo.swift @@ -272,7 +272,7 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { /// - Parameter id: the id of the device that will be deleted. @discardableResult public func deleteDevice(id: String) async throws -> ModelResponse { - try await coordinatorClient.deleteDevice(id: id, userId: user.id) + try await coordinatorClient.deleteDevice(id: id) } /// Lists the devices registered for the user. diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 5335505ee..868bc70b3 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -547,6 +547,7 @@ 8415D3E1290B2AF2006E53CB /* outgoing.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 8415D3E0290B2AF2006E53CB /* outgoing.m4a */; }; 8415D3E3290BC882006E53CB /* Sounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8415D3E2290BC882006E53CB /* Sounds.swift */; }; 841947982886D9CD0007B36E /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841947972886D9CD0007B36E /* BundleExtensions.swift */; }; + 841AE18A2C738CCC005B6560 /* LayoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841AE1892C738CCC005B6560 /* LayoutSettings.swift */; }; 841BAA312BD15CDE000C73E4 /* VideoQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841BAA182BD15CDC000C73E4 /* VideoQuality.swift */; }; 841BAA322BD15CDE000C73E4 /* CallTranscriptionStoppedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841BAA192BD15CDC000C73E4 /* CallTranscriptionStoppedEvent.swift */; }; 841BAA332BD15CDE000C73E4 /* SFULocationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841BAA1A2BD15CDC000C73E4 /* SFULocationResponse.swift */; }; @@ -673,6 +674,14 @@ 8442993C294232360037232A /* IncomingCallView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442993B294232360037232A /* IncomingCallView_iOS13.swift */; }; 844299412942394C0037232A /* VideoView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844299402942394C0037232A /* VideoView_iOS13.swift */; }; 8446AF912A4D84F4002AB07B /* Retries_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */; }; + 844982472C738A830029734D /* DeleteRecordingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8449823F2C738A830029734D /* DeleteRecordingResponse.swift */; }; + 844982482C738A830029734D /* DeleteCallResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982402C738A830029734D /* DeleteCallResponse.swift */; }; + 844982492C738A830029734D /* StartRTMPBroadcastsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982412C738A830029734D /* StartRTMPBroadcastsRequest.swift */; }; + 8449824A2C738A830029734D /* StopRTMPBroadcastsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982422C738A830029734D /* StopRTMPBroadcastsResponse.swift */; }; + 8449824B2C738A830029734D /* DeleteCallRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982432C738A830029734D /* DeleteCallRequest.swift */; }; + 8449824C2C738A830029734D /* DeleteTranscriptionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982442C738A830029734D /* DeleteTranscriptionResponse.swift */; }; + 8449824D2C738A830029734D /* StartRTMPBroadcastsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982452C738A830029734D /* StartRTMPBroadcastsResponse.swift */; }; + 8449824E2C738A830029734D /* StopAllRTMPBroadcastsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844982462C738A830029734D /* StopAllRTMPBroadcastsResponse.swift */; }; 844ADA652AD3F1AB00769F6A /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 844ADA642AD3F1AB00769F6A /* GoogleSignInSwift */; }; 844ADA672AD3F21000769F6A /* GoogleSignIn.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844ADA662AD3F21000769F6A /* GoogleSignIn.plist */; }; 844ADA692AD3F78F00769F6A /* GoogleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844ADA682AD3F78F00769F6A /* GoogleHelper.swift */; }; @@ -861,6 +870,10 @@ 84CBBE0B29228BA900D0DA61 /* StreamVideoTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8492B8722908024800006649 /* StreamVideoTestCase.swift */; }; 84CC05892A530C3F00EE9815 /* SpeakerManager_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC05882A530C3F00EE9815 /* SpeakerManager_Tests.swift */; }; 84CC058B2A531B0B00EE9815 /* CallSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC058A2A531B0B00EE9815 /* CallSettingsManager.swift */; }; + 84CD12162C73831000056640 /* CallRtmpBroadcastStartedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD12082C73830F00056640 /* CallRtmpBroadcastStartedEvent.swift */; }; + 84CD12202C73831000056640 /* CallMissedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD12122C73831000056640 /* CallMissedEvent.swift */; }; + 84CD12222C73831000056640 /* CallRtmpBroadcastStoppedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD12142C73831000056640 /* CallRtmpBroadcastStoppedEvent.swift */; }; + 84CD12252C73840300056640 /* CallUserMutedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD12242C73840300056640 /* CallUserMutedEvent.swift */; }; 84D114DA29F092E700BCCB0C /* CallController_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D114D929F092E700BCCB0C /* CallController_Tests.swift */; }; 84D2E37629DC856D001D2118 /* CallMemberRemovedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2E37029DC856C001D2118 /* CallMemberRemovedEvent.swift */; }; 84D2E37729DC856D001D2118 /* CallMemberUpdatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2E37129DC856C001D2118 /* CallMemberUpdatedEvent.swift */; }; @@ -1566,6 +1579,7 @@ 8415D3E0290B2AF2006E53CB /* outgoing.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = outgoing.m4a; sourceTree = ""; }; 8415D3E2290BC882006E53CB /* Sounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sounds.swift; sourceTree = ""; }; 841947972886D9CD0007B36E /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; + 841AE1892C738CCC005B6560 /* LayoutSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutSettings.swift; sourceTree = ""; }; 841BAA182BD15CDC000C73E4 /* VideoQuality.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoQuality.swift; sourceTree = ""; }; 841BAA192BD15CDC000C73E4 /* CallTranscriptionStoppedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallTranscriptionStoppedEvent.swift; sourceTree = ""; }; 841BAA1A2BD15CDC000C73E4 /* SFULocationResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SFULocationResponse.swift; sourceTree = ""; }; @@ -1691,6 +1705,14 @@ 8442993B294232360037232A /* IncomingCallView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView_iOS13.swift; sourceTree = ""; }; 844299402942394C0037232A /* VideoView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView_iOS13.swift; sourceTree = ""; }; 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retries_Tests.swift; sourceTree = ""; }; + 8449823F2C738A830029734D /* DeleteRecordingResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteRecordingResponse.swift; sourceTree = ""; }; + 844982402C738A830029734D /* DeleteCallResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteCallResponse.swift; sourceTree = ""; }; + 844982412C738A830029734D /* StartRTMPBroadcastsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartRTMPBroadcastsRequest.swift; sourceTree = ""; }; + 844982422C738A830029734D /* StopRTMPBroadcastsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StopRTMPBroadcastsResponse.swift; sourceTree = ""; }; + 844982432C738A830029734D /* DeleteCallRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteCallRequest.swift; sourceTree = ""; }; + 844982442C738A830029734D /* DeleteTranscriptionResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteTranscriptionResponse.swift; sourceTree = ""; }; + 844982452C738A830029734D /* StartRTMPBroadcastsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartRTMPBroadcastsResponse.swift; sourceTree = ""; }; + 844982462C738A830029734D /* StopAllRTMPBroadcastsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StopAllRTMPBroadcastsResponse.swift; sourceTree = ""; }; 844ADA662AD3F21000769F6A /* GoogleSignIn.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = GoogleSignIn.plist; sourceTree = ""; }; 844ADA682AD3F78F00769F6A /* GoogleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleHelper.swift; sourceTree = ""; }; 844ADA6A2AD4439D00769F6A /* DemoCallsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCallsView.swift; sourceTree = ""; }; @@ -1880,6 +1902,10 @@ 84C4003E29E3F446007B69C2 /* ConnectedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedEvent.swift; sourceTree = ""; }; 84CC05882A530C3F00EE9815 /* SpeakerManager_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerManager_Tests.swift; sourceTree = ""; }; 84CC058A2A531B0B00EE9815 /* CallSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettingsManager.swift; sourceTree = ""; }; + 84CD12082C73830F00056640 /* CallRtmpBroadcastStartedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRtmpBroadcastStartedEvent.swift; sourceTree = ""; }; + 84CD12122C73831000056640 /* CallMissedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallMissedEvent.swift; sourceTree = ""; }; + 84CD12142C73831000056640 /* CallRtmpBroadcastStoppedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRtmpBroadcastStoppedEvent.swift; sourceTree = ""; }; + 84CD12242C73840300056640 /* CallUserMutedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallUserMutedEvent.swift; sourceTree = ""; }; 84D114D929F092E700BCCB0C /* CallController_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController_Tests.swift; sourceTree = ""; }; 84D2E37029DC856C001D2118 /* CallMemberRemovedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallMemberRemovedEvent.swift; sourceTree = ""; }; 84D2E37129DC856C001D2118 /* CallMemberUpdatedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallMemberUpdatedEvent.swift; sourceTree = ""; }; @@ -4058,6 +4084,19 @@ 84DC383E29ADFCFC00946713 /* Models */ = { isa = PBXGroup; children = ( + 841AE1892C738CCC005B6560 /* LayoutSettings.swift */, + 844982432C738A830029734D /* DeleteCallRequest.swift */, + 844982402C738A830029734D /* DeleteCallResponse.swift */, + 8449823F2C738A830029734D /* DeleteRecordingResponse.swift */, + 844982442C738A830029734D /* DeleteTranscriptionResponse.swift */, + 844982412C738A830029734D /* StartRTMPBroadcastsRequest.swift */, + 844982452C738A830029734D /* StartRTMPBroadcastsResponse.swift */, + 844982462C738A830029734D /* StopAllRTMPBroadcastsResponse.swift */, + 844982422C738A830029734D /* StopRTMPBroadcastsResponse.swift */, + 84CD12242C73840300056640 /* CallUserMutedEvent.swift */, + 84CD12122C73831000056640 /* CallMissedEvent.swift */, + 84CD12082C73830F00056640 /* CallRtmpBroadcastStartedEvent.swift */, + 84CD12142C73831000056640 /* CallRtmpBroadcastStoppedEvent.swift */, 845C09962C11AAA100F725B3 /* RejectCallRequest.swift */, 845C09822C0DEB5C00F725B3 /* LimitsSettingsRequest.swift */, 845C09832C0DEB5C00F725B3 /* LimitsSettingsResponse.swift */, @@ -5250,6 +5289,7 @@ 8454A3192AAB374B00A012C6 /* CallStatsReport.swift in Sources */, 84E5C51C2A013C440003A27A /* PushNotificationsConfig.swift in Sources */, 84A7E184288362DF00526C98 /* Atomic.swift in Sources */, + 8449824E2C738A830029734D /* StopAllRTMPBroadcastsResponse.swift in Sources */, 84D2E37729DC856D001D2118 /* CallMemberUpdatedEvent.swift in Sources */, 40149DD02B7E839500473176 /* AudioSessionProtocol.swift in Sources */, 8409465B29AF4EEC007AF5BF /* ListRecordingsResponse.swift in Sources */, @@ -5316,11 +5356,13 @@ 841BAA332BD15CDE000C73E4 /* SFULocationResponse.swift in Sources */, 84DC38D129ADFCFD00946713 /* Credentials.swift in Sources */, 84A7E1B02883E73100526C98 /* EventBatcher.swift in Sources */, + 84CD12222C73831000056640 /* CallRtmpBroadcastStoppedEvent.swift in Sources */, 40FB15212BF78FA100D5E580 /* Publisher+NextValue.swift in Sources */, 84BAD77C2A6BFF4300733156 /* BroadcastBufferReaderConnection.swift in Sources */, 84DC38DB29ADFCFD00946713 /* JSONDataEncoding.swift in Sources */, 40FB15112BF77D5800D5E580 /* StreamStateMachineStage.swift in Sources */, 8496A9A629CC500F00F15FF1 /* StreamVideoCaptureHandler.swift in Sources */, + 84CD12162C73831000056640 /* CallRtmpBroadcastStartedEvent.swift in Sources */, 8411925E28C5E5D00074EF88 /* DefaultRTCConfiguration.swift in Sources */, 8409465929AF4EEC007AF5BF /* SendReactionResponse.swift in Sources */, 8412903729DDD1ED00C70A6D /* UpdateCallMembersResponse.swift in Sources */, @@ -5330,6 +5372,7 @@ 406583992B877AB400B4F979 /* CIImage+Resize.swift in Sources */, 84EA5D4328C1E944004D3531 /* AudioSession.swift in Sources */, 841BAA382BD15CDE000C73E4 /* CallTimeline.swift in Sources */, + 8449824D2C738A830029734D /* StartRTMPBroadcastsResponse.swift in Sources */, 84DC38C629ADFCFD00946713 /* CallRejectedEvent.swift in Sources */, 84DC38B729ADFCFD00946713 /* CallRecordingStartedEvent.swift in Sources */, 40F161AB2A4C6B5C00846E3E /* ScreenSharingSession.swift in Sources */, @@ -5349,6 +5392,7 @@ 406AF2012AF3D98F00ED4D0C /* SimulatorScreenCapturer.swift in Sources */, 84A7E18A2883638200526C98 /* URLSessionWebSocketEngine.swift in Sources */, 40C4DF492C1C2C210035DBC2 /* Publisher+WeakAssign.swift in Sources */, + 844982472C738A830029734D /* DeleteRecordingResponse.swift in Sources */, 408679F72BD12F1000D027E0 /* AudioFilter.swift in Sources */, 8456E6D2287EC343004E180E /* ConsoleLogDestination.swift in Sources */, 84DC38A529ADFCFD00946713 /* SFUResponse.swift in Sources */, @@ -5357,6 +5401,7 @@ 4089378B2C062B17000EEB69 /* StreamUUIDFactory.swift in Sources */, 84DC38A829ADFCFD00946713 /* QueryCallsResponse.swift in Sources */, 840F59912A77FDCB00EF3EB2 /* HLSSettingsRequest.swift in Sources */, + 8449824C2C738A830029734D /* DeleteTranscriptionResponse.swift in Sources */, 842B8E1B2A2DFED900863A87 /* CallSessionStartedEvent.swift in Sources */, 40FA12F22B76AC8300CE3EC9 /* RTCCVPixelBuffer+Convenience.swift in Sources */, 8490032629D308A000AD9BB4 /* TranscriptionSettings.swift in Sources */, @@ -5367,6 +5412,7 @@ 84B9A56D29112F39004DE31A /* EndpointConfig.swift in Sources */, 8469593829BB6B4E00134EA0 /* GetEdgesResponse.swift in Sources */, 84DC389A29ADFCFD00946713 /* APIError.swift in Sources */, + 8449824B2C738A830029734D /* DeleteCallRequest.swift in Sources */, 84AF64DB287C7A2C0012A503 /* ErrorPayload.swift in Sources */, 84A7E1812883629700526C98 /* WebSocketPingController.swift in Sources */, 84DCA2232A39E432000C3411 /* AuthMiddlewares.swift in Sources */, @@ -5391,6 +5437,7 @@ 842E70D62B91BE1700D2D68B /* StartRecordingRequest.swift in Sources */, 840042CB2A701C2000917B30 /* BroadcastUtils.swift in Sources */, 844ECF4F2A33458A0023263C /* Member.swift in Sources */, + 84CD12252C73840300056640 /* CallUserMutedEvent.swift in Sources */, 84DC38AC29ADFCFD00946713 /* CallAcceptedEvent.swift in Sources */, 84FC2C2828AD350100181490 /* WebRTCEvents.swift in Sources */, 84DC38A129ADFCFD00946713 /* BlockUserResponse.swift in Sources */, @@ -5402,6 +5449,7 @@ 40FB15172BF77EA600D5E580 /* StreamCallStateMachine.swift in Sources */, 84DC38C829ADFCFD00946713 /* Device.swift in Sources */, 8412903629DDD1ED00C70A6D /* UpdateCallMembersRequest.swift in Sources */, + 8449824A2C738A830029734D /* StopRTMPBroadcastsResponse.swift in Sources */, 84CC058B2A531B0B00EE9815 /* CallSettingsManager.swift in Sources */, 84DC38B929ADFCFD00946713 /* MemberRequest.swift in Sources */, 84DC38BE29ADFCFD00946713 /* CallSettingsRequest.swift in Sources */, @@ -5524,6 +5572,7 @@ 847BE09C29DADE0100B55D21 /* Call.swift in Sources */, 848CCCEF2AB8ED8F002E83A2 /* ThumbnailsSettings.swift in Sources */, 40C4DF4D2C1C2CD80035DBC2 /* DefaultParticipantAutoLeavePolicy.swift in Sources */, + 844982492C738A830029734D /* StartRTMPBroadcastsRequest.swift in Sources */, 84FC2C2228ACF2E000181490 /* PeerConnection.swift in Sources */, 401A0F032AB1C1B600BE2DBD /* ThermalStateObserver.swift in Sources */, 84D2E37929DC856D001D2118 /* CallMemberAddedEvent.swift in Sources */, @@ -5540,6 +5589,7 @@ 84530C6C2A3C4E0700F2678E /* CallState.swift in Sources */, 84DC38C729ADFCFD00946713 /* UpdateCallResponse.swift in Sources */, 84A7E1AA2883E4AD00526C98 /* APIKey.swift in Sources */, + 841AE18A2C738CCC005B6560 /* LayoutSettings.swift in Sources */, 848CCCEE2AB8ED8F002E83A2 /* CallUserMuted.swift in Sources */, 435F01B32A501148009CD0BD /* OwnCapability+Identifiable.swift in Sources */, 40FB150F2BF77CEC00D5E580 /* StreamStateMachine.swift in Sources */, @@ -5547,6 +5597,7 @@ 84E4F7D1294CB5F300DD4CE3 /* ConnectionQuality.swift in Sources */, 848CCCE72AB8ED8F002E83A2 /* ThumbnailsSettingsRequest.swift in Sources */, 8206D8532A5FF3260099F5EC /* SystemEnvironment+Version.swift in Sources */, + 84CD12202C73831000056640 /* CallMissedEvent.swift in Sources */, 842B8E1C2A2DFED900863A87 /* AcceptCallResponse.swift in Sources */, 40F18B8E2BEBB65100ADF76E /* View+OptionalPublisher.swift in Sources */, 842B8E262A2DFED900863A87 /* CallParticipantResponse.swift in Sources */, @@ -5572,6 +5623,7 @@ 8490032429D308A000AD9BB4 /* RingSettings.swift in Sources */, 840F598E2A77FDCB00EF3EB2 /* BroadcastSettingsRequest.swift in Sources */, 842B8E272A2DFED900863A87 /* CallSessionEndedEvent.swift in Sources */, + 844982482C738A830029734D /* DeleteCallResponse.swift in Sources */, 84DC38CF29ADFCFD00946713 /* QueryCallsRequest.swift in Sources */, 84A7E1822883629700526C98 /* RetryStrategy.swift in Sources */, 841BAA402BD15CDE000C73E4 /* UserStats.swift in Sources */, From b257cc68889a6dadddf11783c8fb0c6dcbd951a9 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Wed, 21 Aug 2024 10:48:03 +0200 Subject: [PATCH 25/38] Added muted property to announced tracks data (#490) --- Sources/StreamVideo/protobuf/sfu/models/models.pb.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift index 66d016a88..9f0e4f987 100644 --- a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift +++ b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift @@ -869,6 +869,8 @@ struct Stream_Video_Sfu_Models_TrackInfo { var red: Bool = false + var muted: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1711,6 +1713,7 @@ extension Stream_Video_Sfu_Models_TrackInfo: SwiftProtobuf.Message, SwiftProtobu 7: .same(proto: "dtx"), 8: .same(proto: "stereo"), 9: .same(proto: "red"), + 10: .same(proto: "muted"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1726,6 +1729,7 @@ extension Stream_Video_Sfu_Models_TrackInfo: SwiftProtobuf.Message, SwiftProtobu case 7: try { try decoder.decodeSingularBoolField(value: &self.dtx) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.stereo) }() case 9: try { try decoder.decodeSingularBoolField(value: &self.red) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.muted) }() default: break } } @@ -1753,6 +1757,9 @@ extension Stream_Video_Sfu_Models_TrackInfo: SwiftProtobuf.Message, SwiftProtobu if self.red != false { try visitor.visitSingularBoolField(value: self.red, fieldNumber: 9) } + if self.muted != false { + try visitor.visitSingularBoolField(value: self.muted, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -1764,6 +1771,7 @@ extension Stream_Video_Sfu_Models_TrackInfo: SwiftProtobuf.Message, SwiftProtobu if lhs.dtx != rhs.dtx {return false} if lhs.stereo != rhs.stereo {return false} if lhs.red != rhs.red {return false} + if lhs.muted != rhs.muted {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } From c691dac5b3f5f5da302a6d238bf13fceaf227609 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 22 Aug 2024 11:33:34 +0100 Subject: [PATCH 26/38] [CI] Update publish release flow (#492) --- .github/workflows/release-publish.yml | 30 ++------------------------ fastlane/Fastfile | 31 +++++++-------------------- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index e1b18635a..b5986f7b7 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1,24 +1,16 @@ name: "Publish new release" on: - pull_request: + push: branches: - main - types: - - closed workflow_dispatch: - inputs: - version: - description: 'Release version' - type: string - required: true jobs: release: name: Publish new release runs-on: macos-13 - if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true }} # only merged pull requests must trigger this job steps: - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 @@ -29,28 +21,10 @@ jobs: - uses: ./.github/actions/ruby-cache - - name: Extract version from input (for workflow dispatch) - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - if [ "$BRANCH_NAME" != "main" ]; then - echo "This workflow can only be run on the main branch." - exit 1 - fi - echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - - name: Extract version from branch name (for release branches) - if: ${{ github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'release/') }} - run: | - BRANCH_NAME="${{ github.event.pull_request.head.ref }}" - VERSION=${BRANCH_NAME#release/} - echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV - - name: "Fastlane - Publish Release" - if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.pull_request.head.ref, 'release/') }} env: GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} - run: bundle exec fastlane publish_release version:${{ env.RELEASE_VERSION }} --verbose + run: bundle exec fastlane publish_release --verbose diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6af6c1841..274ddfc66 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,6 +21,7 @@ gci = ENV['GOOGLE_CLIENT_ID'] || '' reversed_gci = gci.split('.').reverse.join('.') is_localhost = !is_ci @force_check = false +swift_environment_path = File.absolute_path('../Sources/StreamVideo/Generated/SystemEnvironment+Version.swift') swiftformat_excluded_paths = ["**/Generated", "**/generated", "**/protobuf", "**/OpenApi"] swiftformat_source_paths = ["Sources", "DemoApp", "DemoAppUIKit", "StreamVideoTests", "StreamVideoSwiftUITests", "StreamVideoUIKitTests"] @@ -43,7 +44,6 @@ desc "Release a new version" lane :release do |options| previous_version_number = last_git_tag artifacts_path = File.absolute_path('../StreamVideoArtifacts.json') - swift_environment_path = File.absolute_path('../Sources/StreamVideo/Generated/SystemEnvironment+Version.swift') extra_changes = lambda do |release_version| # Set the framework version on the artifacts artifacts = JSON.parse(File.read(artifacts_path)) @@ -76,8 +76,12 @@ end desc "Publish a new release to GitHub and CocoaPods" lane :publish_release do |options| - xcversion(version: '15.0.1') + release_version = File.read(swift_environment_path).match(/String\s+=\s+"([\d.]+)"/)[1] + UI.user_error!("Release #{release_version} has already been published.") if git_tag_exists(tag: release_version, remote: true) + UI.user_error!('Release version cannot be empty') if release_version.to_s.empty? + ensure_git_branch(branch: 'main') + xcversion(version: '15.0.1') clean_products build_xcframeworks compress_frameworks @@ -85,14 +89,14 @@ lane :publish_release do |options| publish_ios_sdk( skip_git_status_check: false, - version: options[:version], + version: release_version, sdk_names: sdk_names, podspec_names: podspec_names, github_repo: github_repo, upload_assets: ['Products/StreamVideo.zip', 'Products/StreamVideoSwiftUI.zip', 'Products/StreamVideoUIKit.zip', 'Products/StreamVideo-All.zip'] ) - update_spm(version: options[:version]) + update_spm(version: release_version) merge_main_to_develop end @@ -583,25 +587,6 @@ lane :code_generation do sync_xcodeproj_references end -private_lane :git_status do |options| - UI.user_error!('Extension should be provided') unless options[:ext] - - untracked_files = sh('git status -s', log: false).split("\n").map(&:strip) - UI.important("Git Status: #{untracked_files}") - - deleted_files = select_files_from(files: untracked_files, with_extension: options[:ext], that_start_with: 'D') - added_files = select_files_from(files: untracked_files, with_extension: options[:ext], that_start_with: ['A', '??']) - renamed_files = select_files_from(files: untracked_files, with_extension: options[:ext], that_start_with: 'R') - modified_files = select_files_from(files: untracked_files, with_extension: options[:ext], that_start_with: 'M') - - renamed_files.each do |renamed_file| - content = renamed_file.split.drop(1).join.split('->').map(&:strip) - deleted_files << content.first - added_files << content.last - end - { a: added_files, d: deleted_files, m: modified_files } -end - lane :sync_xcodeproj_references do Dir.chdir('..') do project = Xcodeproj::Project.open(xcode_project) From d5bf2ed60a1441240a1d1e7a3d667431bc6c30ff Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 22 Aug 2024 13:58:26 +0300 Subject: [PATCH 27/38] Enhancement/custom call settings when joining from callkit (#491) --- .../06-advanced/03-callkit-integration.swift | 6 +++ .../StreamVideo/CallKit/CallKitAdapter.swift | 6 +++ .../StreamVideo/CallKit/CallKitService.swift | 4 +- .../CallKit/CallKitAdapterTests.swift | 10 +++++ .../CallKit/CallKitServiceTests.swift | 38 +++++++++++++++++++ .../Controllers/CallsController_Tests.swift | 6 +-- StreamVideoTests/Mock/MockCall.swift | 24 +++++++++++- StreamVideoTests/StreamVideoTestCase.swift | 6 +-- .../06-advanced/03-callkit-integration.mdx | 10 +++++ 9 files changed, 100 insertions(+), 10 deletions(-) diff --git a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift index 8a8ac510e..319bf4c94 100644 --- a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift +++ b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift @@ -139,6 +139,12 @@ fileprivate func content() { } } + container { + @Injected(\.callKitAdapter) var callKitAdapter + + callKitAdapter.callSettings = CallSettings(audioOn: true, videoOn: false) + } + container { @Injected(\.callKitService) var callKitService diff --git a/Sources/StreamVideo/CallKit/CallKitAdapter.swift b/Sources/StreamVideo/CallKit/CallKitAdapter.swift index ac6ee23df..6e9137f45 100644 --- a/Sources/StreamVideo/CallKit/CallKitAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitAdapter.swift @@ -20,6 +20,12 @@ open class CallKitAdapter { set { callKitService.iconTemplateImageData = newValue } } + /// The callSettings to use when joining a call (after accepting it on CallKit) + /// default: nil + open var callSettings: CallSettings? { + didSet { callKitService.callSettings = callSettings } + } + /// The currently active StreamVideo client. /// - Important: We need to update it whenever a user logins. public var streamVideo: StreamVideo? { diff --git a/Sources/StreamVideo/CallKit/CallKitService.swift b/Sources/StreamVideo/CallKit/CallKitService.swift index 0c6d7ca49..a7978da6a 100644 --- a/Sources/StreamVideo/CallKit/CallKitService.swift +++ b/Sources/StreamVideo/CallKit/CallKitService.swift @@ -58,6 +58,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { /// - Note: defaults to `false`. open var supportsVideo: Bool = false + var callSettings: CallSettings? + /// The call controller used for managing calls. open internal(set) lazy var callController = CXCallController() /// The call provider responsible for handling call-related actions. @@ -307,7 +309,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { } do { - try await callToJoinEntry.call.join() + try await callToJoinEntry.call.join(callSettings: callSettings) action.fulfill() } catch { callToJoinEntry.call.leave() diff --git a/StreamVideoTests/CallKit/CallKitAdapterTests.swift b/StreamVideoTests/CallKit/CallKitAdapterTests.swift index 530648a29..5e9c6cd26 100644 --- a/StreamVideoTests/CallKit/CallKitAdapterTests.swift +++ b/StreamVideoTests/CallKit/CallKitAdapterTests.swift @@ -67,6 +67,16 @@ final class CallKitAdapterTests: XCTestCase { XCTAssertTrue(callKitPushNotificationAdapter.unregisterWasCalled) } + // MARK: - callSettings updated + + func test_callSettings_callKitServiceReceivedTheUpdatedValue() { + let callSettings = CallSettings(audioOn: false, videoOn: true) + + subject.callSettings = callSettings + + XCTAssertEqual(callKitService.callSettings, callSettings) + } + // MARK: - Private Helpers private func makeStreamVideo() async throws -> StreamVideo { diff --git a/StreamVideoTests/CallKit/CallKitServiceTests.swift b/StreamVideoTests/CallKit/CallKitServiceTests.swift index 432628f04..4f6912c8e 100644 --- a/StreamVideoTests/CallKit/CallKitServiceTests.swift +++ b/StreamVideoTests/CallKit/CallKitServiceTests.swift @@ -276,6 +276,44 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { } } + // MARK: - accept + + @MainActor + func test_accept_callWasJoinedAsExpected() async throws { + let customCallSettings = CallSettings(audioOn: false, videoOn: true) + subject.callSettings = customCallSettings + + let firstCallUUID = UUID() + uuidFactory.getResult = firstCallUUID + let call = stubCall(response: defaultGetCallResponse) + subject.streamVideo = mockedStreamVideo + + subject.reportIncomingCall( + cid, + localizedCallerName: localizedCallerName, + callerId: callerId + ) { _ in } + + await waitExpectation(timeout: 1) + + // Accept call + subject.provider( + callProvider, + perform: CXAnswerCallAction( + call: firstCallUUID + ) + ) + + await waitExpectation(timeout: 1) + + XCTAssertEqual(call.stubbedFunctionInput[.join]?.count, 1) + let input = try XCTUnwrap(call.stubbedFunctionInput[.join]?.first) + switch input { + case let .join(_, _, _, _, callSettings): + XCTAssertEqual(callSettings, customCallSettings) + } + } + // MARK: - callAccepted @MainActor diff --git a/StreamVideoTests/Controllers/CallsController_Tests.swift b/StreamVideoTests/Controllers/CallsController_Tests.swift index 22cda991e..8ce56a0c3 100644 --- a/StreamVideoTests/Controllers/CallsController_Tests.swift +++ b/StreamVideoTests/Controllers/CallsController_Tests.swift @@ -75,11 +75,7 @@ final class CallsController_Tests: ControllerTestCase { streamVideo?.state.connection = .disconnected() try await waitForCallEvent() streamVideo?.state.connection = .connected - try await waitForCallEvent() - - // Then - // Calls should be rewatched - XCTAssertEqual(httpClient.requestCounter, 2) + try await fulfillment { self.httpClient.requestCounter == 2 } } func test_callsController_noWatchingCalls() async throws { diff --git a/StreamVideoTests/Mock/MockCall.swift b/StreamVideoTests/Mock/MockCall.swift index 1b336e642..7ff484e01 100644 --- a/StreamVideoTests/Mock/MockCall.swift +++ b/StreamVideoTests/Mock/MockCall.swift @@ -9,14 +9,27 @@ final class MockCall: Call, Mockable { typealias FunctionKey = MockCallFunctionKey - enum MockCallFunctionKey: Hashable { + enum MockCallFunctionKey: Hashable, CaseIterable { case get case accept case join } + enum MockCallFunctionInputKey { + case join( + create: Bool, + options: CreateCallOptions?, + ring: Bool, + notify: Bool, + callSettings: CallSettings? + ) + } + var stubbedProperty: [String: Any] var stubbedFunction: [FunctionKey: Any] = [:] + @Atomic var stubbedFunctionInput: [FunctionKey: [MockCallFunctionInputKey]] = MockCallFunctionKey + .allCases + .reduce(into: [FunctionKey: [MockCallFunctionInputKey]]()) { $0[$1] = [] } override var state: CallState { get { self[dynamicMember: \.state] } @@ -66,6 +79,15 @@ final class MockCall: Call, Mockable { notify: Bool = false, callSettings: CallSettings? = nil ) async throws -> JoinCallResponse { + stubbedFunctionInput[.join]?.append( + .join( + create: create, + options: options, + ring: ring, + notify: notify, + callSettings: callSettings + ) + ) if let stub = stubbedFunction[.join] as? JoinCallResponse { return stub } else { diff --git a/StreamVideoTests/StreamVideoTestCase.swift b/StreamVideoTests/StreamVideoTestCase.swift index 6edbfb631..537a1299b 100644 --- a/StreamVideoTests/StreamVideoTestCase.swift +++ b/StreamVideoTests/StreamVideoTestCase.swift @@ -7,14 +7,14 @@ import XCTest open class StreamVideoTestCase: XCTestCase { - public var streamVideo: StreamVideo! + public internal(set) var streamVideo: StreamVideo! var httpClient: HTTPClient_Mock! = HTTPClient_Mock() override open func setUp() { super.setUp() streamVideo = StreamVideo.mock(httpClient: httpClient) } - + override open func tearDown() async throws { try await super.tearDown() await streamVideo?.disconnect() @@ -29,7 +29,7 @@ open class StreamVideoTestCase: XCTestCase { } // TODO: replace this with something a bit better - func waitForCallEvent(nanoseconds: UInt64 = 500_000_000) async throws { + func waitForCallEvent(nanoseconds: UInt64 = 5_000_000_000) async throws { try await Task.sleep(nanoseconds: nanoseconds) } } diff --git a/docusaurus/docs/iOS/06-advanced/03-callkit-integration.mdx b/docusaurus/docs/iOS/06-advanced/03-callkit-integration.mdx index a038cd70a..3fa064790 100644 --- a/docusaurus/docs/iOS/06-advanced/03-callkit-integration.mdx +++ b/docusaurus/docs/iOS/06-advanced/03-callkit-integration.mdx @@ -161,6 +161,16 @@ If none of the fields above are being set, the property will be an empty string. - **created_by_display_name** The property is always set and contains the name of the user who created the call. +#### Call Settings when accepting a call + +Depending on your business logic, you may need users to join call with different `CallSettings`(e.g auioOn=true while videoOn=false). In order to achieve that when using the `CallKitAdapter` you can provide your custom `CallSettings` at any point before you receive a call: + +```swift +@Injected(\.callKitAdapter) var callKitAdapter + +callKitAdapter.callSettings = CallSettings(audioOn: true, videoOn: false) +``` + #### Call's type suffix Depending on the `Call` type `CallKit` adds a suffix in the push notification's subtitle (which contains the application name). That suffix can either be `Audio` or `Video`. `CallKitService` allows you to configure what the supported call types are, by setting the `CallKitService.supportsVideo` property like below: From 03c3a308b68e700288438443df08bc0011c2e856 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 22 Aug 2024 18:50:32 +0300 Subject: [PATCH 28/38] [Fix]Ensure Call.state.startedAt is non-nil when session is valid (#493) --- Sources/StreamVideo/CallState.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/StreamVideo/CallState.swift b/Sources/StreamVideo/CallState.swift index b3b03c0fa..f6b486200 100644 --- a/Sources/StreamVideo/CallState.swift +++ b/Sources/StreamVideo/CallState.swift @@ -427,10 +427,17 @@ public class CallState: ObservableObject { } private func didUpdate(_ session: CallSessionResponse?) { - if startedAt != session?.liveStartedAt { - startedAt = session?.liveStartedAt + guard let session else { return } + if let startedAt = session.startedAt { + self.startedAt = startedAt + } else if let liveStartedAt = session.liveStartedAt { + startedAt = liveStartedAt + } else if startedAt == nil { + /// If we don't receive a value from the SFU we start the timer on the current date. + startedAt = Date() } - if session?.liveEndedAt != nil { + + if session.liveEndedAt != nil { resetTimer() } } From 0e8e550ae55ece37ac633957c5970a403083e377 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Mon, 26 Aug 2024 17:09:08 +0200 Subject: [PATCH 29/38] Generated code for participant count session event (#494) --- Sources/StreamVideo/CallState.swift | 4 ++ .../Models/CallRtmpBroadcastFailedEvent.swift | 45 ++++++++++++++++ ...SessionParticipantCountsUpdatedEvent.swift | 51 +++++++++++++++++++ .../Models/CallSessionResponse.swift | 11 +++- .../OpenApi/generated/Models/WSEvent.swift | 21 +++++++- StreamVideo.xcodeproj/project.pbxproj | 8 +++ .../Mock/MockResponseBuilder.swift | 2 + .../Dummy/CallSessionResponse+Dummy.swift | 2 + 8 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastFailedEvent.swift create mode 100644 Sources/StreamVideo/OpenApi/generated/Models/CallSessionParticipantCountsUpdatedEvent.swift diff --git a/Sources/StreamVideo/CallState.swift b/Sources/StreamVideo/CallState.swift index f6b486200..754785c1d 100644 --- a/Sources/StreamVideo/CallState.swift +++ b/Sources/StreamVideo/CallState.swift @@ -233,6 +233,10 @@ public class CallState: ObservableObject { break case .typeCallUserMutedEvent: break + case .typeCallRtmpBroadcastFailedEvent: + break + case .typeCallSessionParticipantCountsUpdatedEvent: + break } } diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastFailedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastFailedEvent.swift new file mode 100644 index 000000000..24e503d2a --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallRtmpBroadcastFailedEvent.swift @@ -0,0 +1,45 @@ +// +// CallRtmpBroadcastFailedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent when a call RTMP broadcast has failed */ + +public struct CallRtmpBroadcastFailedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + /** The unique identifier for a call (:) */ + public var callCid: String + /** Date/time of creation */ + public var createdAt: Date + /** Name of the given RTMP broadcast */ + public var name: String + /** The type of event: \"call.rtmp_broadcast_failed\" in this case */ + public var type: String = "call.rtmp_broadcast_failed" + + public init(callCid: String, createdAt: Date, name: String, type: String = "call.rtmp_broadcast_failed") { + self.callCid = callCid + self.createdAt = createdAt + self.name = name + self.type = type + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case callCid = "call_cid" + case createdAt = "created_at" + case name + case type + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(name, forKey: .name) + try container.encode(type, forKey: .type) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallSessionParticipantCountsUpdatedEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallSessionParticipantCountsUpdatedEvent.swift new file mode 100644 index 000000000..90ad4ce67 --- /dev/null +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallSessionParticipantCountsUpdatedEvent.swift @@ -0,0 +1,51 @@ +// +// CallSessionParticipantCountsUpdatedEvent.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +/** This event is sent when the participant counts in a call session are updated */ + +public struct CallSessionParticipantCountsUpdatedEvent: @unchecked Sendable, Event, Codable, JSONEncodable, Hashable, WSCallEvent { + public var anonymousParticipantCount: Int + public var callCid: String + public var createdAt: Date + public var participantsCountByRole: [String: Int] + /** Call session ID */ + public var sessionId: String + /** The type of event: \"call.session_participant_count_updated\" in this case */ + public var type: String = "call.session_participant_count_updated" + + public init(anonymousParticipantCount: Int, callCid: String, createdAt: Date, participantsCountByRole: [String: Int], sessionId: String, type: String = "call.session_participant_count_updated") { + self.anonymousParticipantCount = anonymousParticipantCount + self.callCid = callCid + self.createdAt = createdAt + self.participantsCountByRole = participantsCountByRole + self.sessionId = sessionId + self.type = type + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case anonymousParticipantCount = "anonymous_participant_count" + case callCid = "call_cid" + case createdAt = "created_at" + case participantsCountByRole = "participants_count_by_role" + case sessionId = "session_id" + case type + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(anonymousParticipantCount, forKey: .anonymousParticipantCount) + try container.encode(callCid, forKey: .callCid) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(participantsCountByRole, forKey: .participantsCountByRole) + try container.encode(sessionId, forKey: .sessionId) + try container.encode(type, forKey: .type) + } +} + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/CallSessionResponse.swift b/Sources/StreamVideo/OpenApi/generated/Models/CallSessionResponse.swift index ec8369917..504eda601 100644 --- a/Sources/StreamVideo/OpenApi/generated/Models/CallSessionResponse.swift +++ b/Sources/StreamVideo/OpenApi/generated/Models/CallSessionResponse.swift @@ -10,22 +10,26 @@ import Foundation public struct CallSessionResponse: Codable, JSONEncodable, Hashable { public var acceptedBy: [String: Date] + public var anonymousParticipantCount: Int? public var endedAt: Date? public var id: String public var liveEndedAt: Date? public var liveStartedAt: Date? + public var missedBy: [String: Date]? public var participants: [CallParticipantResponse] public var participantsCountByRole: [String: Int] public var rejectedBy: [String: Date] public var startedAt: Date? public var timerEndsAt: Date? - public init(acceptedBy: [String: Date], endedAt: Date? = nil, id: String, liveEndedAt: Date? = nil, liveStartedAt: Date? = nil, participants: [CallParticipantResponse], participantsCountByRole: [String: Int], rejectedBy: [String: Date], startedAt: Date? = nil, timerEndsAt: Date? = nil) { + public init(acceptedBy: [String: Date], anonymousParticipantCount: Int?, endedAt: Date? = nil, id: String, liveEndedAt: Date? = nil, liveStartedAt: Date? = nil, missedBy: [String: Date]?, participants: [CallParticipantResponse], participantsCountByRole: [String: Int], rejectedBy: [String: Date], startedAt: Date? = nil, timerEndsAt: Date? = nil) { self.acceptedBy = acceptedBy + self.anonymousParticipantCount = anonymousParticipantCount self.endedAt = endedAt self.id = id self.liveEndedAt = liveEndedAt self.liveStartedAt = liveStartedAt + self.missedBy = missedBy self.participants = participants self.participantsCountByRole = participantsCountByRole self.rejectedBy = rejectedBy @@ -35,10 +39,12 @@ public struct CallSessionResponse: Codable, JSONEncodable, Hashable { public enum CodingKeys: String, CodingKey, CaseIterable { case acceptedBy = "accepted_by" + case anonymousParticipantCount = "anonymous_participant_count" case endedAt = "ended_at" case id case liveEndedAt = "live_ended_at" case liveStartedAt = "live_started_at" + case missedBy = "missed_by" case participants case participantsCountByRole = "participants_count_by_role" case rejectedBy = "rejected_by" @@ -51,10 +57,12 @@ public struct CallSessionResponse: Codable, JSONEncodable, Hashable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(acceptedBy, forKey: .acceptedBy) + try container.encodeIfPresent(anonymousParticipantCount, forKey: .anonymousParticipantCount) try container.encodeIfPresent(endedAt, forKey: .endedAt) try container.encode(id, forKey: .id) try container.encodeIfPresent(liveEndedAt, forKey: .liveEndedAt) try container.encodeIfPresent(liveStartedAt, forKey: .liveStartedAt) + try container.encodeIfPresent(missedBy, forKey: .missedBy) try container.encode(participants, forKey: .participants) try container.encode(participantsCountByRole, forKey: .participantsCountByRole) try container.encode(rejectedBy, forKey: .rejectedBy) @@ -62,3 +70,4 @@ public struct CallSessionResponse: Codable, JSONEncodable, Hashable { try container.encodeIfPresent(timerEndsAt, forKey: .timerEndsAt) } } + diff --git a/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift b/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift index 29efbdc3b..f8903974c 100644 --- a/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift +++ b/Sources/StreamVideo/OpenApi/generated/Models/WSEvent.swift @@ -37,9 +37,11 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { case typeCallRecordingStoppedEvent(CallRecordingStoppedEvent) case typeCallRejectedEvent(CallRejectedEvent) case typeCallRingEvent(CallRingEvent) + case typeCallRtmpBroadcastFailedEvent(CallRtmpBroadcastFailedEvent) case typeCallRtmpBroadcastStartedEvent(CallRtmpBroadcastStartedEvent) case typeCallRtmpBroadcastStoppedEvent(CallRtmpBroadcastStoppedEvent) case typeCallSessionEndedEvent(CallSessionEndedEvent) + case typeCallSessionParticipantCountsUpdatedEvent(CallSessionParticipantCountsUpdatedEvent) case typeCallSessionParticipantJoinedEvent(CallSessionParticipantJoinedEvent) case typeCallSessionParticipantLeftEvent(CallSessionParticipantLeftEvent) case typeCallSessionStartedEvent(CallSessionStartedEvent) @@ -101,12 +103,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value.type case .typeCallRingEvent(let value): return value.type + case .typeCallRtmpBroadcastFailedEvent(let value): + return value.type case .typeCallRtmpBroadcastStartedEvent(let value): return value.type case .typeCallRtmpBroadcastStoppedEvent(let value): return value.type case .typeCallSessionEndedEvent(let value): return value.type + case .typeCallSessionParticipantCountsUpdatedEvent(let value): + return value.type case .typeCallSessionParticipantJoinedEvent(let value): return value.type case .typeCallSessionParticipantLeftEvent(let value): @@ -189,12 +195,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { return value case .typeCallRingEvent(let value): return value + case .typeCallRtmpBroadcastFailedEvent(let value): + return value case .typeCallRtmpBroadcastStartedEvent(let value): return value case .typeCallRtmpBroadcastStoppedEvent(let value): return value case .typeCallSessionEndedEvent(let value): return value + case .typeCallSessionParticipantCountsUpdatedEvent(let value): + return value case .typeCallSessionParticipantJoinedEvent(let value): return value case .typeCallSessionParticipantLeftEvent(let value): @@ -278,12 +288,16 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { try container.encode(value) case .typeCallRingEvent(let value): try container.encode(value) + case .typeCallRtmpBroadcastFailedEvent(let value): + try container.encode(value) case .typeCallRtmpBroadcastStartedEvent(let value): try container.encode(value) case .typeCallRtmpBroadcastStoppedEvent(let value): try container.encode(value) case .typeCallSessionEndedEvent(let value): try container.encode(value) + case .typeCallSessionParticipantCountsUpdatedEvent(let value): + try container.encode(value) case .typeCallSessionParticipantJoinedEvent(let value): try container.encode(value) case .typeCallSessionParticipantLeftEvent(let value): @@ -399,6 +413,9 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } else if dto.type == "call.ring" { let value = try container.decode(CallRingEvent.self) self = .typeCallRingEvent(value) + } else if dto.type == "call.rtmp_broadcast_failed" { + let value = try container.decode(CallRtmpBroadcastFailedEvent.self) + self = .typeCallRtmpBroadcastFailedEvent(value) } else if dto.type == "call.rtmp_broadcast_started" { let value = try container.decode(CallRtmpBroadcastStartedEvent.self) self = .typeCallRtmpBroadcastStartedEvent(value) @@ -408,6 +425,9 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } else if dto.type == "call.session_ended" { let value = try container.decode(CallSessionEndedEvent.self) self = .typeCallSessionEndedEvent(value) + } else if dto.type == "call.session_participant_count_updated" { + let value = try container.decode(CallSessionParticipantCountsUpdatedEvent.self) + self = .typeCallSessionParticipantCountsUpdatedEvent(value) } else if dto.type == "call.session_participant_joined" { let value = try container.decode(CallSessionParticipantJoinedEvent.self) self = .typeCallSessionParticipantJoinedEvent(value) @@ -456,4 +476,3 @@ public enum VideoEvent: Codable, JSONEncodable, Hashable { } } - diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 868bc70b3..1af1e3221 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -891,6 +891,8 @@ 84D6494429E9AD08002CA428 /* CallIngressResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D6494229E9AD08002CA428 /* CallIngressResponse.swift */; }; 84D6494729E9F2D0002CA428 /* WebRTCClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D6494629E9F2D0002CA428 /* WebRTCClient_Tests.swift */; }; 84D6E53A2B3AD10000D0056C /* RepeatingTimer_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D6E5392B3AD10000D0056C /* RepeatingTimer_Tests.swift */; }; + 84D91E9C2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D91E9A2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift */; }; + 84D91E9D2C7CB0AA00B163A0 /* CallRtmpBroadcastFailedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D91E9B2C7CB0AA00B163A0 /* CallRtmpBroadcastFailedEvent.swift */; }; 84DC382D29A8B9EC00946713 /* CallParticipantMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC382C29A8B9EC00946713 /* CallParticipantMenuAction.swift */; }; 84DC382F29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC382E29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift */; }; 84DC388F29ADFCFD00946713 /* CodableHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DC383D29ADFCFB00946713 /* CodableHelper.swift */; }; @@ -1923,6 +1925,8 @@ 84D6494229E9AD08002CA428 /* CallIngressResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallIngressResponse.swift; sourceTree = ""; }; 84D6494629E9F2D0002CA428 /* WebRTCClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCClient_Tests.swift; sourceTree = ""; }; 84D6E5392B3AD10000D0056C /* RepeatingTimer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer_Tests.swift; sourceTree = ""; }; + 84D91E9A2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallSessionParticipantCountsUpdatedEvent.swift; sourceTree = ""; }; + 84D91E9B2C7CB0AA00B163A0 /* CallRtmpBroadcastFailedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRtmpBroadcastFailedEvent.swift; sourceTree = ""; }; 84DC382C29A8B9EC00946713 /* CallParticipantMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantMenuAction.swift; sourceTree = ""; }; 84DC382E29A8BB8D00946713 /* CallParticipantsInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantsInfoViewModel.swift; sourceTree = ""; }; 84DC383D29ADFCFB00946713 /* CodableHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableHelper.swift; sourceTree = ""; }; @@ -4084,6 +4088,8 @@ 84DC383E29ADFCFC00946713 /* Models */ = { isa = PBXGroup; children = ( + 84D91E9B2C7CB0AA00B163A0 /* CallRtmpBroadcastFailedEvent.swift */, + 84D91E9A2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift */, 841AE1892C738CCC005B6560 /* LayoutSettings.swift */, 844982432C738A830029734D /* DeleteCallRequest.swift */, 844982402C738A830029734D /* DeleteCallResponse.swift */, @@ -5315,6 +5321,7 @@ 8409465629AF4EEC007AF5BF /* CallReactionEvent.swift in Sources */, 84DCA21F2A39DA15000C3411 /* APIHelper.swift in Sources */, 84DC38D329ADFCFD00946713 /* CallEndedEvent.swift in Sources */, + 84D91E9D2C7CB0AA00B163A0 /* CallRtmpBroadcastFailedEvent.swift in Sources */, 84A737D028F4716E001A6769 /* models.pb.swift in Sources */, 846D16222A52B8D00036CE4C /* MicrophoneManager.swift in Sources */, 842E70DB2B91BE1700D2D68B /* ListTranscriptionsResponse.swift in Sources */, @@ -5345,6 +5352,7 @@ 8490DD1F298D39D9007E53D2 /* JsonEventDecoder.swift in Sources */, 40FB15192BF77EE700D5E580 /* StreamCallStateMachine+IdleStage.swift in Sources */, 84BAD7842A6C01AF00733156 /* BroadcastBufferReader.swift in Sources */, + 84D91E9C2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift in Sources */, 846E4AF529CDEA66003733AB /* ConnectUserDetailsRequest.swift in Sources */, 846D16262A52CE8C0036CE4C /* SpeakerManager.swift in Sources */, 841BAA3A2BD15CDE000C73E4 /* Location.swift in Sources */, diff --git a/StreamVideoTests/Mock/MockResponseBuilder.swift b/StreamVideoTests/Mock/MockResponseBuilder.swift index d4355c2de..61ee1bccd 100644 --- a/StreamVideoTests/Mock/MockResponseBuilder.swift +++ b/StreamVideoTests/Mock/MockResponseBuilder.swift @@ -175,10 +175,12 @@ class MockResponseBuilder { ) -> CallSessionResponse { CallSessionResponse( acceptedBy: acceptedBy, + anonymousParticipantCount: 0, endedAt: liveEndedAt, id: cid, liveEndedAt: liveEndedAt, liveStartedAt: liveStartedAt, + missedBy: [:], participants: [], participantsCountByRole: [:], rejectedBy: rejectedBy, diff --git a/StreamVideoTests/Utilities/Dummy/CallSessionResponse+Dummy.swift b/StreamVideoTests/Utilities/Dummy/CallSessionResponse+Dummy.swift index a4b53b249..91490d7f8 100644 --- a/StreamVideoTests/Utilities/Dummy/CallSessionResponse+Dummy.swift +++ b/StreamVideoTests/Utilities/Dummy/CallSessionResponse+Dummy.swift @@ -19,10 +19,12 @@ extension CallSessionResponse { ) -> CallSessionResponse { .init( acceptedBy: acceptedBy, + anonymousParticipantCount: 0, endedAt: endedAt, id: id, liveEndedAt: liveEndedAt, liveStartedAt: liveStartedAt, + missedBy: [:], participants: participants, participantsCountByRole: participantsCountByRole, rejectedBy: rejectedBy, From 7544d4602d879f48c429bdcd42c8291ee7ff50d7 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Tue, 27 Aug 2024 17:21:11 +0200 Subject: [PATCH 30/38] Updated the docs structure and content (#495) --- .../03-guides/02-joining-creating-calls.mdx | 66 ++++++++- .../iOS/03-guides/07-dependency-injection.mdx | 54 ------- .../docs/iOS/03-guides/10-view-slots.mdx | 135 ------------------ .../docs/iOS/03-guides/11-call-lifecycle.mdx | 21 --- .../iOS/03-guides/13-ui-customization.mdx | 35 ----- .../docs/iOS/04-ui-components/01-overview.mdx | 87 ++++++++++- .../02-swiftui-vs-uikit.mdx} | 0 .../04-ui-components/04-customizing-views.mdx | 130 ++++++++++++++++- ...deo-renderer.mdx => 07-video-renderer.mdx} | 0 .../01-call-container.mdx | 0 .../{07-call => 08-call}/02-outgoing-call.mdx | 0 .../{07-call => 08-call}/03-incoming-call.mdx | 0 .../{07-call => 08-call}/04-active-call.mdx | 0 .../{07-call => 08-call}/05-call-controls.mdx | 0 .../{07-call => 08-call}/06-call-app-bar.mdx | 0 .../07-screen-share-content.mdx | 0 .../{07-call => 08-call}/_category_.json | 0 .../01-call-participant.mdx | 0 .../02-call-participants.mdx | 0 .../03-call-participants-info-menu.mdx | 0 .../04-local-video.mdx | 0 .../_category_.json | 0 .../02-sound-indicator.mdx | 0 .../{09-utility => 10-utility}/03-avatars.mdx | 0 .../04-connection-quality-indicator.mdx | 0 .../05-call-background.mdx | 0 .../_category_.json | 0 .../iOS/06-advanced/15-sdk-size-impact.mdx | 31 +--- .../16-migration-from-dolby.mdx} | 0 .../iOS/assets/call_member-grant-joincall.png | Bin 0 -> 116550 bytes .../docs/iOS/assets/user-revoke-joincall.png | Bin 0 -> 122551 bytes 31 files changed, 282 insertions(+), 277 deletions(-) delete mode 100644 docusaurus/docs/iOS/03-guides/07-dependency-injection.mdx delete mode 100644 docusaurus/docs/iOS/03-guides/10-view-slots.mdx delete mode 100644 docusaurus/docs/iOS/03-guides/11-call-lifecycle.mdx delete mode 100644 docusaurus/docs/iOS/03-guides/13-ui-customization.mdx rename docusaurus/docs/iOS/{03-guides/14-swiftui-vs-uikit.mdx => 04-ui-components/02-swiftui-vs-uikit.mdx} (100%) rename docusaurus/docs/iOS/04-ui-components/{02-video-renderer.mdx => 07-video-renderer.mdx} (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/01-call-container.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/02-outgoing-call.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/03-incoming-call.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/04-active-call.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/05-call-controls.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/06-call-app-bar.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/07-screen-share-content.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{07-call => 08-call}/_category_.json (100%) rename docusaurus/docs/iOS/04-ui-components/{08-participants => 09-participants}/01-call-participant.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{08-participants => 09-participants}/02-call-participants.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{08-participants => 09-participants}/03-call-participants-info-menu.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{08-participants => 09-participants}/04-local-video.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{08-participants => 09-participants}/_category_.json (100%) rename docusaurus/docs/iOS/04-ui-components/{09-utility => 10-utility}/02-sound-indicator.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{09-utility => 10-utility}/03-avatars.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{09-utility => 10-utility}/04-connection-quality-indicator.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{09-utility => 10-utility}/05-call-background.mdx (100%) rename docusaurus/docs/iOS/04-ui-components/{09-utility => 10-utility}/_category_.json (100%) rename docusaurus/docs/iOS/{03-guides/15-migration-from-dolby.mdx => 06-advanced/16-migration-from-dolby.mdx} (100%) create mode 100644 docusaurus/docs/iOS/assets/call_member-grant-joincall.png create mode 100644 docusaurus/docs/iOS/assets/user-revoke-joincall.png diff --git a/docusaurus/docs/iOS/03-guides/02-joining-creating-calls.mdx b/docusaurus/docs/iOS/03-guides/02-joining-creating-calls.mdx index d87d7cbf1..3452e47a8 100644 --- a/docusaurus/docs/iOS/03-guides/02-joining-creating-calls.mdx +++ b/docusaurus/docs/iOS/03-guides/02-joining-creating-calls.mdx @@ -31,6 +31,33 @@ let call = streamVideo.call(callType: "default", callId: "123") let result = try await call.join() ``` +### Create and join a call + +For convenience, you can create and join a call in a single operation. One of the flags you can provide there is `create`. +Set this to `true` if you want to enable creating new call. Set it to `false` if you only want to join an existing call. + +```swift +try await call.join(create: true) +``` + +### Leave call + +To leave a call, you can use the `leave` method: + +```swift +call.leave() +``` + +### End call + +Ending a call requires a [special permission](../permissions-and-moderation). This action terminates the call for everyone. + +```typescript +try await call.end() +``` + +Only users with special permission can join an ended call. + ### Call CRUD Basic CRUD operations are available on the call object @@ -131,4 +158,41 @@ You can filter the member list on these fields, and sort on the selected fields. | `role` | The member's role. | No | | `custom` | The custom data on the member. | No | | `created_at` | When the member was created. | Yes | -| `updated_at` | When the member was last updated. | No | \ No newline at end of file +| `updated_at` | When the member was last updated. | No | + +## Restricting access + +You can restrict access to a call by tweaking the [Call Type](../configuring-call-types/) permissions and roles. +A typical use case is to restrict access to a call to a specific set of users -> call members. + +#### Step 1: Set up the roles and permissions + +On our [dashboard](https://dashboard.getstream.io/), navigate to the **Video & Audio -> Roles & Permissions** section and select the appropriate role and scope. +In this example, we will use `my-call-type` scope. + +By default, all users unless specified otherwise, have the `user` role. + +We start by removing the `JoinCall` permission from the `user` role for the `my-call-type` scope. +It will prevent regular users from joining a call of this type. + +![Revoke JoinCall for user role](../assets/user-revoke-joincall.png) + +Next, let's ensure that the `call_member` role has the `JoinCall` permission for the `my-call-type` scope. +It will allow users with the `call_member` role to join a call of this type. + +![Grant JoinCall for call_member role](../assets/call_member-grant-joincall.png) + +Once this is set, we can proceed with setting up a `call` instance. + +#### Step 2: Set up the call + +```swift +let call = streamVideo.call(callType: "my-call-type", callId: "my-call-id") +try await call.create(members: [.init(role: "call_member", userId: "alice")]) + +// and if necessary, to grant access to more users +try await call.addMembers(members: [.init(role: "call_member", userId: "charlie")]) + +// or, to remove access from some users +try await call.removeMembers(ids: ["charlie"]) +``` \ No newline at end of file diff --git a/docusaurus/docs/iOS/03-guides/07-dependency-injection.mdx b/docusaurus/docs/iOS/03-guides/07-dependency-injection.mdx deleted file mode 100644 index d08991528..000000000 --- a/docusaurus/docs/iOS/03-guides/07-dependency-injection.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Dependency Injection ---- - -For injecting dependencies in the SwiftUI SDK, we are using an approach based on [this article](https://www.avanderlee.com/swift/dependency-injection/). It works similarly to the @Environment in SwiftUI, but it also allows access to the dependencies in non-view related code. - -When you initialize the SDK (by creating the `StreamVideoUI` object), all the dependencies are created too, and you can use them anywhere in your code. In order to access a particular type, you need to use the `@Injected(\.keyPath)` property wrapper: - -```swift -@Injected(\.streamVideo) var streamVideo -@Injected(\.fonts) var fonts -@Injected(\.colors) var colors -@Injected(\.images) var images -@Injected(\.sounds) var sounds -@Injected(\.utils) var utils -``` - -### Extending the DI with Custom Types - -In some cases, you might also need to extend our DI mechanism with your own types. For example, you may want to be able to access your custom types like this: - -```swift -@Injected(\.customType) var customType -``` - -In order to achieve this, you first need to define your own `InjectionKey`, and define it's `currentValue`, which basically creates the new instance of your type. - -```swift -class CustomType { - // your custom logic here -} - -struct CustomInjectionKey: InjectionKey { - static var currentValue: CustomType = CustomType() -} -``` - -Next, you need to extend our `InjectedValues` with your own custom type, by defining its getter and setter. - -```swift -extension InjectedValues { - /// Provides access to the `CustomType` instance in the views and view models. - var customType: CustomType { - get { - Self[CustomInjectionKey.self] - } - set { - Self[CustomInjectionKey.self] = newValue - } - } -} -``` - -With these few simple steps, you can now access your custom functionality in both your app code and in your custom implementations of the views used throughout the SDK. diff --git a/docusaurus/docs/iOS/03-guides/10-view-slots.mdx b/docusaurus/docs/iOS/03-guides/10-view-slots.mdx deleted file mode 100644 index b41dea366..000000000 --- a/docusaurus/docs/iOS/03-guides/10-view-slots.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: View Slots ---- - -You can swap certain views in the SwiftUI SDK, by implementing your own version of the `ViewFactory` protocol. Here's a list of the supported slots that can be swapped with your custom views. - -In most of these slots, the whole `CallViewModel` is provided, allowing you to update the state from these views. - -### Outgoing Call View - -In order to swap the outgoing call view, we will need to implement the `makeOutgoingCallView(viewModel: CallViewModel) -> some View` in the `ViewFactory`: - -```swift - -class CustomViewFactory: ViewFactory { - - func makeOutgoingCallView(viewModel: CallViewModel) -> some View { - CustomOutgoingCallView(viewModel: viewModel) - } - -} -``` - -### Incoming Call View - -Similarly, the incoming call view can be replaced by implementing the `makeIncomingCallView(viewModel: CallViewModel, callInfo: IncomingCall) -> some View` in the `ViewFactory`: - -```swift -public func makeIncomingCallView(viewModel: CallViewModel, callInfo: IncomingCall) -> some View { - CustomIncomingCallView(callInfo: callInfo, viewModel: viewModel) -} -``` - -### Call View - -When the call state change to `.inCall`, the call view slot is shown. The default implementation provides several customizable parts, such as the video participants, the call controls (mute/unmute, hang up) and the top trailing view (which by default displays participants' info). - -In order to swap the default call view, you will need to implement the `makeCallView(viewModel: CallViewModel) -> some View`: - -```swift -public func makeCallView(viewModel: CallViewModel) -> some View { - CustomCallView(viewModel: viewModel) -} -``` - -Apart from the main call view, you can also swap its building blocks. - -#### Call Controls View - -The call controls view by default displays controls for hiding/showing the camera, muting/unmuting the microphone, changing the camera source (front/back) and hanging up. If you want to change these controls, you will need to implement the `makeCallControlsView(viewModel: CallViewModel) -> some View` method: - -```swift -func makeCallControlsView(viewModel: CallViewModel) -> some View { - CustomCallControlsView(viewModel: viewModel) -} -``` - -#### Video Participants View - -The video participants view slot presents the grid of users that are in the call. If you want to provide a different variation of the participants display, you will need to implement the `makeVideoParticipantsView` in the `ViewFactory`: - -```swift -public func makeVideoParticipantsView( - viewModel: CallViewModel, - availableFrame: CGRect, - onChangeTrackVisibility: @escaping @MainActor(CallParticipant, Bool) -> Void -) -> some View { - VideoParticipantsView( - viewFactory: self, - viewModel: viewModel, - availableFrame: availableFrame, - onChangeTrackVisibility: onChangeTrackVisibility - ) -} -``` - -In the method, the following parameters are provided: - -- `viewModel` - the viewModel that manages the call. -- `availableFrame` - the available frame for the participants view. -- `onChangeTrackVisibility` - callback when the track changes its visibility. - -#### Video Participant View - -If you want to customize one particular participant view, you can change it via the method `makeVideoParticipantView`: - -```swift -func makeVideoParticipantView( - participant: CallParticipant, - id: String, - availableFrame: CGRect, - contentMode: UIView.ContentMode, - customData: [String: RawJSON], - call: Call? -) -> some View { - VideoCallParticipantView( - participant: participant, - id: id, - availableFrame: availableFrame, - contentMode: contentMode, - customData: customData, - call: call - ) -} -``` - -Additionally, you can change the modifier applied to the view, by implementing the `makeVideoCallParticipantModifier`: - -```swift -public func makeVideoCallParticipantModifier( - participant: CallParticipant, - call: Call?, - availableFrame: CGRect, - ratio: CGFloat, - showAllInfo: Bool -) -> some ViewModifier { - VideoCallParticipantModifier( - participant: participant, - call: call, - availableFrame: availableFrame, - ratio: ratio, - showAllInfo: showAllInfo - ) -} -``` - -#### Top View - -This is the view presented in the top area of the call view. By default, it displays a back button (to go in minimized mode) and a button that shows the list of participants. You can swap this view with your own implementation, by implementing the `makeCallTopView` in the `ViewFactory`: - -```swift -public func makeCallTopView(viewModel: CallViewModel) -> some View { - CallTopView(viewModel: viewModel) -} -``` diff --git a/docusaurus/docs/iOS/03-guides/11-call-lifecycle.mdx b/docusaurus/docs/iOS/03-guides/11-call-lifecycle.mdx deleted file mode 100644 index a3123e210..000000000 --- a/docusaurus/docs/iOS/03-guides/11-call-lifecycle.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Call Lifecycle ---- - -## Call - -The `Call` object manages everything related to a particular call, such as creating, joining a call, performing actions for a user (mute/unmute, camera change, invite, etc) and listening to events. - -When a call starts, the iOS SDK communicates with our backend infrastructure, to find the best Selective Forwarding Unit (SFU) to host the call, based on the locations of the participants. It then establishes the connection with that SFU and provides updates on all events related to a call. - -You can create a new call via the `StreamVideo`'s method `func call(callType: String, callId: String)`. - -It's a lower-level component than the stateful `CallViewModel`, and it's suitable if you want to create your own presentation logic and state handling. - -The `Call` object should exist while the call is active. Afterwards, you should clean up all the state related to the call (provided you don't use our `CallViewModel`), by calling the `leave` method and de-allocating the instance. - -Every call has a call id and type. You can join a call with the same id as many times as you need. However, the call sends ringing events only the first time. If you want to receive ring events, you should always use a unique call id. - -## Web Socket Connection - -The web socket connection with our backend is established when the `StreamVideo` object is being created. If you go into the background, and come back, the SDK tries to re-establish this connection. The web socket connection is persisted in order to listen to events such as incoming calls, that can be presented in-app (if you're not using CallKit). diff --git a/docusaurus/docs/iOS/03-guides/13-ui-customization.mdx b/docusaurus/docs/iOS/03-guides/13-ui-customization.mdx deleted file mode 100644 index 06dfb1e90..000000000 --- a/docusaurus/docs/iOS/03-guides/13-ui-customization.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: UI customization ---- - -## UI components vs Custom - -StreamVideo provides both ready made components to use directly in your app, as well as extension points that you can use to inject your own custom UI. If you just need the calling functionality and completely custom UI, you can use only our low-level client. - -Let's explore the different possibilities and how they would impact your app and the integration efforts. - -## Using only the low-level client - -If your app needs a completely custom UI and calling flow, you can use only our low-level client that implements the WebRTC protocol and communicates with our backend services. If you go with this approach, you can either use our stateful `CallViewModel` that allows you to observe the call state (list of participants, camera & microphone state, etc), or use our lower level `Call` object and implement your own presentation objects. - -Additionally, if you go with this approach, you can still use some components from our UI SDKs (if they fit your use-case), to facilitate your development. We have several examples for this in [our cookbook](../../ui-cookbook/overview). - -This approach would require some familiarity with our low-level client, and the highest development efforts compared to the other two options. On the other hand, it gives you a maximum flexibility to customize the calling flow according to your needs. - -In any case, our view components are highly customizable and flexible for many video/audio calling cases, and they can save big development efforts. Therefore, we recommend that you consider the other two options below, before deciding on starting from scratch. - -## Mix & match - -The mix & match approach is ideal if you need one of the standard calling flows, but with a possibility to replace parts of the UI with your own implementation. Our UI SDK allows you to completely swap views with your own custom interface elements. - -For example, if you are building an app with incoming / outgoing calling screens, you can easily swap only those screens. For building your custom screens, you can still reuse our lower level components. - -This approach provides a nice balance between levels of customization and development efforts. Find examples and extension slots to get started in our docs [here](../view-slots). - -## Simple theming - -If you need a standard video calling experience that needs to match the rest of your app's look and feel, you can use our theming customizations. - -This is the fastest way to add calling support to your app, just setup our video client and attach our `CallModifier` to your hosting view. You can change the fonts, colors, icons, texts and sounds used in the SDK, by interacting with our `Appearance` class. - -You can find more details about how to customize the theming [here](../../ui-components/overview). diff --git a/docusaurus/docs/iOS/04-ui-components/01-overview.mdx b/docusaurus/docs/iOS/04-ui-components/01-overview.mdx index 2a0a543be..a461382f1 100644 --- a/docusaurus/docs/iOS/04-ui-components/01-overview.mdx +++ b/docusaurus/docs/iOS/04-ui-components/01-overview.mdx @@ -5,10 +5,40 @@ description: Overview of the UI components ## Introduction -The StreamVideo SDK provides UI components to facilitate the integration of video capabilities into your apps. You can either use our out-of-the-box solution (and customize theming and some views), or completely build your own UI, while reusing our lower-level components whenever you see them fit. +The StreamVideo SDK provides UI components to facilitate the integration of video capabilities into your apps. The UI components are provided in SwiftUI. If you use UIKit, we also provide UIKit wrappers, that can make it easier for you to integrate video in UIKit-based apps. +## UI components vs Custom + +StreamVideo provides both ready made components to use directly in your app, as well as extension points that you can use to inject your own custom UI. If you only need the calling functionality to support your (custom built) UI, you can simply rely on our low-level client. + +Let's explore the different possibilities and how they would impact your app and the integration efforts. + +## Using only the low-level client + +If your app needs a completely custom UI and calling flow, you can use only our low-level client that implements the WebRTC protocol and communicates with our backend services. If you go with this approach, you can either use our stateful `CallViewModel` that allows you to observe the call state (list of participants, camera & microphone state, etc), or use our lower level `Call` object and implement your own presentation objects. + +Additionally, if you go with this approach, you can still use some components from our UI SDKs (if they fit your use-case), to facilitate your development. We have several examples for this in [our cookbook](../../ui-cookbook/overview). + +This approach would require some familiarity with our low-level client, and the highest development efforts compared to the other two options. On the other hand, it gives you maximum flexibility to customize the calling flow according to your needs. + +In any case, our view components are highly customizable and flexible for many video/audio calling cases, and they can save big development efforts. Therefore, we recommend that you consider the other two options below, before deciding on starting from scratch. + +## Mix & match + +The mix & match approach is ideal if you need one of the standard calling flows, but with a possibility to replace parts of the UI with your own implementation. Our UI SDK allows you to completely swap views with your own interface elements. + +For example, if you are building an app with incoming / outgoing calling screens, you can easily swap only those screens. For building your custom screens, you can still reuse our lower level components. + +This approach provides a nice balance between levels of customization and development efforts. Find examples and extension slots to get started in our docs [here](../view-slots). + +## Simple theming + +If you need a standard video calling experience that needs to match the rest of your app's look and feel, you can use our theming customizations. + +This is the fastest way to add calling support to your app, just setup our video client and attach our `CallModifier` to your hosting view. You can change the fonts, colors, icons, texts and sounds used in the SDK, by interacting with our `Appearance` class. + ## StreamVideoUI object The UI SDK provides a context provider object that allows simple access to functionalities exposed by the SDK, such as branding, presentation logic, icons, and the low-level video client. @@ -55,3 +85,58 @@ Find more details on how to do this on [this page](../video-theme). ### Changing Views Apart from the basic theming customizations, you can also swap certain views, with your implementation. You can find more details on how to do that on this [page](../customizing-views). + +## Dependency Injection + +For injecting dependencies in the SwiftUI SDK, we are using an approach based on [this article](https://www.avanderlee.com/swift/dependency-injection/). It works similarly to the @Environment in SwiftUI, but it also allows access to the dependencies in non-view related code. + +When you initialize the SDK (by creating the `StreamVideoUI` object), all the dependencies are created too, and you can use them anywhere in your code. In order to access a particular type, you need to use the `@Injected(\.keyPath)` property wrapper: + +```swift +@Injected(\.streamVideo) var streamVideo +@Injected(\.fonts) var fonts +@Injected(\.colors) var colors +@Injected(\.images) var images +@Injected(\.sounds) var sounds +@Injected(\.utils) var utils +``` + +### Extending the DI with Custom Types + +In some cases, you might also need to extend our DI mechanism with your own types. For example, you may want to be able to access your custom types like this: + +```swift +@Injected(\.customType) var customType +``` + +In order to achieve this, you first need to define your own `InjectionKey`, and define it's `currentValue`, which basically creates the new instance of your type. + +```swift +class CustomType { + // your custom logic here +} + +struct CustomInjectionKey: InjectionKey { + static var currentValue: CustomType = CustomType() +} +``` + +Next, you need to extend our `InjectedValues` with your own custom type, by defining its getter and setter. + +```swift +extension InjectedValues { + /// Provides access to the `CustomType` instance in the views and view models. + var customType: CustomType { + get { + Self[CustomInjectionKey.self] + } + set { + Self[CustomInjectionKey.self] = newValue + } + } +} +``` + +With these few simple steps, you can now access your custom functionality in both your app code and in your custom implementations of the views used throughout the SDK. + +Additionally, DI entries can be accessed by using the `InjectedValues[\.]` syntax (for example `InjectedValues[\.customType]`). This approach can be useful in case you want to override our default injected values. \ No newline at end of file diff --git a/docusaurus/docs/iOS/03-guides/14-swiftui-vs-uikit.mdx b/docusaurus/docs/iOS/04-ui-components/02-swiftui-vs-uikit.mdx similarity index 100% rename from docusaurus/docs/iOS/03-guides/14-swiftui-vs-uikit.mdx rename to docusaurus/docs/iOS/04-ui-components/02-swiftui-vs-uikit.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/04-customizing-views.mdx b/docusaurus/docs/iOS/04-ui-components/04-customizing-views.mdx index 9bf22050c..ef24738ca 100644 --- a/docusaurus/docs/iOS/04-ui-components/04-customizing-views.mdx +++ b/docusaurus/docs/iOS/04-ui-components/04-customizing-views.mdx @@ -35,4 +35,132 @@ var body: some View { } ``` -For the full list of supported view slots that can be swapped, please refer to this [page](../../guides/view-slots). +Here are all the slots available for customization in the SwiftUI SDK. + +### Outgoing Call View + +In order to swap the outgoing call view, we will need to implement the `makeOutgoingCallView(viewModel: CallViewModel) -> some View` in the `ViewFactory`: + +```swift + +class CustomViewFactory: ViewFactory { + + func makeOutgoingCallView(viewModel: CallViewModel) -> some View { + CustomOutgoingCallView(viewModel: viewModel) + } + +} +``` + +### Incoming Call View + +Similarly, the incoming call view can be replaced by implementing the `makeIncomingCallView(viewModel: CallViewModel, callInfo: IncomingCall) -> some View` in the `ViewFactory`: + +```swift +public func makeIncomingCallView(viewModel: CallViewModel, callInfo: IncomingCall) -> some View { + CustomIncomingCallView(callInfo: callInfo, viewModel: viewModel) +} +``` + +### Call View + +When the call state change to `.inCall`, the call view slot is shown. The default implementation provides several customizable parts, such as the video participants, the call controls (mute/unmute, hang up) and the top trailing view (which by default displays participants' info). + +In order to swap the default call view, you will need to implement the `makeCallView(viewModel: CallViewModel) -> some View`: + +```swift +public func makeCallView(viewModel: CallViewModel) -> some View { + CustomCallView(viewModel: viewModel) +} +``` + +Apart from the main call view, you can also swap its building blocks. + +#### Call Controls View + +The call controls view by default displays controls for hiding/showing the camera, muting/unmuting the microphone, changing the camera source (front/back) and hanging up. If you want to change these controls, you will need to implement the `makeCallControlsView(viewModel: CallViewModel) -> some View` method: + +```swift +func makeCallControlsView(viewModel: CallViewModel) -> some View { + CustomCallControlsView(viewModel: viewModel) +} +``` + +#### Video Participants View + +The video participants view slot presents the grid of users that are in the call. If you want to provide a different variation of the participants display, you will need to implement the `makeVideoParticipantsView` in the `ViewFactory`: + +```swift +public func makeVideoParticipantsView( + viewModel: CallViewModel, + availableFrame: CGRect, + onChangeTrackVisibility: @escaping @MainActor(CallParticipant, Bool) -> Void +) -> some View { + VideoParticipantsView( + viewFactory: self, + viewModel: viewModel, + availableFrame: availableFrame, + onChangeTrackVisibility: onChangeTrackVisibility + ) +} +``` + +In the method, the following parameters are provided: + +- `viewModel` - the viewModel that manages the call. +- `availableFrame` - the available frame for the participants view. +- `onChangeTrackVisibility` - callback when the track changes its visibility. + +#### Video Participant View + +If you want to customize one particular participant view, you can change it via the method `makeVideoParticipantView`: + +```swift +func makeVideoParticipantView( + participant: CallParticipant, + id: String, + availableFrame: CGRect, + contentMode: UIView.ContentMode, + customData: [String: RawJSON], + call: Call? +) -> some View { + VideoCallParticipantView( + participant: participant, + id: id, + availableFrame: availableFrame, + contentMode: contentMode, + customData: customData, + call: call + ) +} +``` + +Additionally, you can change the modifier applied to the view, by implementing the `makeVideoCallParticipantModifier`: + +```swift +public func makeVideoCallParticipantModifier( + participant: CallParticipant, + call: Call?, + availableFrame: CGRect, + ratio: CGFloat, + showAllInfo: Bool +) -> some ViewModifier { + VideoCallParticipantModifier( + participant: participant, + call: call, + availableFrame: availableFrame, + ratio: ratio, + showAllInfo: showAllInfo + ) +} +``` + +#### Top View + +This is the view presented in the top area of the call view. By default, it displays a back button (to go in minimized mode) and a button that shows the list of participants. You can swap this view with your own implementation, by implementing the `makeCallTopView` in the `ViewFactory`: + +```swift +public func makeCallTopView(viewModel: CallViewModel) -> some View { + CallTopView(viewModel: viewModel) +} +``` \ No newline at end of file diff --git a/docusaurus/docs/iOS/04-ui-components/02-video-renderer.mdx b/docusaurus/docs/iOS/04-ui-components/07-video-renderer.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/02-video-renderer.mdx rename to docusaurus/docs/iOS/04-ui-components/07-video-renderer.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/01-call-container.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/01-call-container.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/01-call-container.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/01-call-container.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/02-outgoing-call.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/02-outgoing-call.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/02-outgoing-call.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/02-outgoing-call.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/03-incoming-call.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/03-incoming-call.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/03-incoming-call.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/03-incoming-call.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/04-active-call.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/04-active-call.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/04-active-call.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/04-active-call.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/05-call-controls.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/05-call-controls.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/05-call-controls.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/05-call-controls.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/06-call-app-bar.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/06-call-app-bar.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/06-call-app-bar.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/06-call-app-bar.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/07-screen-share-content.mdx b/docusaurus/docs/iOS/04-ui-components/08-call/07-screen-share-content.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/07-screen-share-content.mdx rename to docusaurus/docs/iOS/04-ui-components/08-call/07-screen-share-content.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/07-call/_category_.json b/docusaurus/docs/iOS/04-ui-components/08-call/_category_.json similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/07-call/_category_.json rename to docusaurus/docs/iOS/04-ui-components/08-call/_category_.json diff --git a/docusaurus/docs/iOS/04-ui-components/08-participants/01-call-participant.mdx b/docusaurus/docs/iOS/04-ui-components/09-participants/01-call-participant.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/08-participants/01-call-participant.mdx rename to docusaurus/docs/iOS/04-ui-components/09-participants/01-call-participant.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/08-participants/02-call-participants.mdx b/docusaurus/docs/iOS/04-ui-components/09-participants/02-call-participants.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/08-participants/02-call-participants.mdx rename to docusaurus/docs/iOS/04-ui-components/09-participants/02-call-participants.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/08-participants/03-call-participants-info-menu.mdx b/docusaurus/docs/iOS/04-ui-components/09-participants/03-call-participants-info-menu.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/08-participants/03-call-participants-info-menu.mdx rename to docusaurus/docs/iOS/04-ui-components/09-participants/03-call-participants-info-menu.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/08-participants/04-local-video.mdx b/docusaurus/docs/iOS/04-ui-components/09-participants/04-local-video.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/08-participants/04-local-video.mdx rename to docusaurus/docs/iOS/04-ui-components/09-participants/04-local-video.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/08-participants/_category_.json b/docusaurus/docs/iOS/04-ui-components/09-participants/_category_.json similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/08-participants/_category_.json rename to docusaurus/docs/iOS/04-ui-components/09-participants/_category_.json diff --git a/docusaurus/docs/iOS/04-ui-components/09-utility/02-sound-indicator.mdx b/docusaurus/docs/iOS/04-ui-components/10-utility/02-sound-indicator.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/09-utility/02-sound-indicator.mdx rename to docusaurus/docs/iOS/04-ui-components/10-utility/02-sound-indicator.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/09-utility/03-avatars.mdx b/docusaurus/docs/iOS/04-ui-components/10-utility/03-avatars.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/09-utility/03-avatars.mdx rename to docusaurus/docs/iOS/04-ui-components/10-utility/03-avatars.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/09-utility/04-connection-quality-indicator.mdx b/docusaurus/docs/iOS/04-ui-components/10-utility/04-connection-quality-indicator.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/09-utility/04-connection-quality-indicator.mdx rename to docusaurus/docs/iOS/04-ui-components/10-utility/04-connection-quality-indicator.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/09-utility/05-call-background.mdx b/docusaurus/docs/iOS/04-ui-components/10-utility/05-call-background.mdx similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/09-utility/05-call-background.mdx rename to docusaurus/docs/iOS/04-ui-components/10-utility/05-call-background.mdx diff --git a/docusaurus/docs/iOS/04-ui-components/09-utility/_category_.json b/docusaurus/docs/iOS/04-ui-components/10-utility/_category_.json similarity index 100% rename from docusaurus/docs/iOS/04-ui-components/09-utility/_category_.json rename to docusaurus/docs/iOS/04-ui-components/10-utility/_category_.json diff --git a/docusaurus/docs/iOS/06-advanced/15-sdk-size-impact.mdx b/docusaurus/docs/iOS/06-advanced/15-sdk-size-impact.mdx index d08e772d5..fb845a3ca 100644 --- a/docusaurus/docs/iOS/06-advanced/15-sdk-size-impact.mdx +++ b/docusaurus/docs/iOS/06-advanced/15-sdk-size-impact.mdx @@ -2,33 +2,6 @@ When developing a mobile app, one crucial performance metric is app size. An app’s size can be difficult to accurately measure with multiple variants and device spreads. Once measured, it’s even more difficult to understand and identify what’s contributing to size bloat. -This document provides an analysis of the impact of adding StreamVideo and StreamVideoSwiftUI iOS SDKs to an existing mobile application. The analysis includes the size of the SDKs and the impact on the app's binary size. +We track and update the SDK size on every commit to our `develop` branch. The sizes of all SDKs that are part of our video product are shown with badges, at the top of our GitHub [repo](https://github.com/GetStream/stream-video-swift). -## SDK Information - -| | StreamVideo | StreamVideoSwiftUI | -| ------------------- | :---------: | :----------------: | -| **Download Size:** | 4.20 MB | 2.85 MB | -| **Installed Size:** | 4.74 MB | 3.35 MB | - -### Analysis - -The following analysis was conducted using the `mdls -n kMDItemPhysicalSize` command in the Terminal after building the app with and without the SDK. - -1. **SwiftVideo** - - | | w/o framework | with framework | difference | - | ------------------ | :-----------: | :------------: | :--------: | - | App Size (Release) | 13_463_552 | 18_440_192 | 4_976_640 | - -2. **StreamVideoSwiftUI** - - | | w/o framework | with framework | difference | - | ------------------ | :-----------: | :------------: | :--------: | - | App Size (Release) | 13_463_552 | 16_986_112 | 3_522_560 | - -The tables above show the impact of integrating the SDKs on the app's binary size, in bytes. - -## Conclusion - -Based on the analysis conducted, both StreamVideo and StreamVideoSwiftUI SDKs are lightweight and optimized for optimal performance. We are confident that our Video SDKs will enhance the functionality of your app without compromising its performance or increasing its binary size significantly. +Therefore, please check our repo to get the up-to date sizes of our video SDKs. diff --git a/docusaurus/docs/iOS/03-guides/15-migration-from-dolby.mdx b/docusaurus/docs/iOS/06-advanced/16-migration-from-dolby.mdx similarity index 100% rename from docusaurus/docs/iOS/03-guides/15-migration-from-dolby.mdx rename to docusaurus/docs/iOS/06-advanced/16-migration-from-dolby.mdx diff --git a/docusaurus/docs/iOS/assets/call_member-grant-joincall.png b/docusaurus/docs/iOS/assets/call_member-grant-joincall.png new file mode 100644 index 0000000000000000000000000000000000000000..d950f9b309891bc5334350ac3f258b57634794f8 GIT binary patch literal 116550 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 z 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 z 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

&>@F^Gkm_QWn^E)Fyo=|57u9IXo!` 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 Date: Wed, 28 Aug 2024 13:12:15 +0300 Subject: [PATCH 31/38] [Enhancement]Handle participantsCount event (#496) --- CHANGELOG.md | 3 +- Sources/StreamVideo/CallState.swift | 1 + .../Controllers/CallController.swift | 43 ++++++++++++- .../StreamVideo/WebRTC/SfuMiddleware.swift | 2 + Sources/StreamVideo/WebRTC/WebRTCClient.swift | 5 ++ StreamVideo.xcodeproj/project.pbxproj | 4 ++ .../WebRTC/SFUMiddlewareTests.swift | 62 +++++++++++++++++++ 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 StreamVideoTests/WebRTC/SFUMiddlewareTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f87dfe5ed..397b83705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +### ✅ Added +- Participants (regular and anonymous) count, can be accessed - before or after joining a call - from the `Call.state.participantCount` & `Call.state.anonymousParticipantCount` respectively. [#496](https://github.com/GetStream/stream-video-swift/pull/496) # [1.0.9](https://github.com/GetStream/stream-video-swift/releases/tag/1.0.9) _July 19, 2024_ diff --git a/Sources/StreamVideo/CallState.swift b/Sources/StreamVideo/CallState.swift index 754785c1d..3b6797aea 100644 --- a/Sources/StreamVideo/CallState.swift +++ b/Sources/StreamVideo/CallState.swift @@ -117,6 +117,7 @@ public class CallState: ObservableObject { } @Published public internal(set) var reconnectionStatus = ReconnectionStatus.connected + @Published public internal(set) var anonymousParticipantCount: UInt32 = 0 @Published public internal(set) var participantCount: UInt32 = 0 @Published public internal(set) var isInitialized: Bool = false @Published public internal(set) var callSettings = CallSettings() diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index 57f509c08..7d7e50ada 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -13,10 +13,20 @@ class CallController: @unchecked Sendable { didSet { handleParticipantsUpdated() handleParticipantCountUpdated() + handleAnonymousParticipantCountUpdated() + } + } + + weak var call: Call? { + didSet { + participantsCountUpdatesTask?.cancel() + participantsCountUpdatesTask = nil + if let call { + participantsCountUpdatesTask = subscribeToParticipantsCountUpdatesEvent(call) + } } } - weak var call: Call? private let user: User private let callId: String private let callType: String @@ -30,7 +40,8 @@ class CallController: @unchecked Sendable { private var currentSFU: String? private var statsInterval: TimeInterval = 5 private var statsCancellable: AnyCancellable? - + private var participantsCountUpdatesTask: Task? + init( defaultAPI: DefaultAPI, user: User, @@ -478,7 +489,15 @@ class CallController: @unchecked Sendable { } } } - + + private func handleAnonymousParticipantCountUpdated() { + webRTCClient?.onAnonymousParticipantCountUpdated = { [weak self] participantCount in + Task { @MainActor [weak self] in + self?.call?.state.anonymousParticipantCount = participantCount + } + } + } + private func handleSignalChannelConnectionStateChange(_ state: WebSocketConnectionState) { switch state { case let .disconnected(source): @@ -487,6 +506,10 @@ class CallController: @unchecked Sendable { self?.handleSignalChannelDisconnect(source: source) } case .connected(healthCheckInfo: _): + /// Once connected we should stop listening for CallSessionParticipantCountsUpdatedEvent + /// updates and only rely on the healthCheck event. + participantsCountUpdatesTask?.cancel() + participantsCountUpdatesTask = nil log.debug("Signal channel connected") if reconnectionDate != nil { reconnectionDate = nil @@ -691,6 +714,20 @@ class CallController: @unchecked Sendable { } } } + + private func subscribeToParticipantsCountUpdatesEvent(_ call: Call) -> Task { + Task { + for await event in call.subscribe(for: CallSessionParticipantCountsUpdatedEvent.self) { + Task { @MainActor in + call.state.participantCount = event.participantsCountByRole + .values + .map(UInt32.init) + .reduce(0) { $0 + $1 } + call.state.anonymousParticipantCount = UInt32(event.anonymousParticipantCount) + } + } + } + } } extension CallController { diff --git a/Sources/StreamVideo/WebRTC/SfuMiddleware.swift b/Sources/StreamVideo/WebRTC/SfuMiddleware.swift index 388ea0ddb..2864d506b 100644 --- a/Sources/StreamVideo/WebRTC/SfuMiddleware.swift +++ b/Sources/StreamVideo/WebRTC/SfuMiddleware.swift @@ -16,6 +16,7 @@ class SfuMiddleware: EventMiddleware { private var publisher: PeerConnection? var onSocketConnected: ((Bool) -> Void)? var onParticipantCountUpdated: ((UInt32) -> Void)? + var onAnonymousParticipantCountUpdated: ((UInt32) -> Void)? var onSessionMigrationEvent: (() -> Void)? var onPinsChanged: (([Stream_Video_Sfu_Models_Pin]) -> Void)? @@ -76,6 +77,7 @@ class SfuMiddleware: EventMiddleware { await loadParticipants(from: event) case let .healthCheckResponse(event): onParticipantCountUpdated?(event.participantCount.total) + onAnonymousParticipantCountUpdated?(event.participantCount.anonymous) case let .trackPublished(event): await handleTrackPublishedEvent(event) case let .trackUnpublished(event): diff --git a/Sources/StreamVideo/WebRTC/WebRTCClient.swift b/Sources/StreamVideo/WebRTC/WebRTCClient.swift index df5b076f9..3232026a4 100644 --- a/Sources/StreamVideo/WebRTC/WebRTCClient.swift +++ b/Sources/StreamVideo/WebRTC/WebRTCClient.swift @@ -189,6 +189,7 @@ class WebRTCClient: NSObject, @unchecked Sendable { var onParticipantsUpdated: (([String: CallParticipant]) -> Void)? var onSignalConnectionStateChange: ((WebSocketConnectionState) -> Void)? var onParticipantCountUpdated: ((UInt32) -> Void)? + var onAnonymousParticipantCountUpdated: ((UInt32) -> Void)? var onSessionMigrationEvent: (() -> Void)? { didSet { sfuMiddleware.onSessionMigrationEvent = onSessionMigrationEvent @@ -313,6 +314,10 @@ class WebRTCClient: NSObject, @unchecked Sendable { sfuMiddleware.onParticipantCountUpdated = { [weak self] participantCount in self?.onParticipantCountUpdated?(participantCount) } + sfuMiddleware.onAnonymousParticipantCountUpdated = { [weak self] in + self?.onAnonymousParticipantCountUpdated?($0) + } + sfuMiddleware.onPinsChanged = { [weak self] pins in self?.handlePinsChanged(pins) } diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 1af1e3221..135dc354b 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 404C27CC2BF2552900DF2937 /* XCTestCase+PredicateFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */; }; 404CAEE72B8F48F6007087BC /* DemoBackgroundEffectSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E95F2B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift */; }; 4059C3422AAF0CE40006928E /* DemoChatViewModel+Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */; }; + 405EFF112C7F120700AC5CE6 /* SFUMiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405EFF102C7F120700AC5CE6 /* SFUMiddlewareTests.swift */; }; 4063033F2AD847EC0091AE77 /* CallState_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */; }; 406303422AD848000091AE77 /* CallParticipant_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406303412AD848000091AE77 /* CallParticipant_Mock.swift */; }; 406303462AD9432D0091AE77 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 406303442AD942ED0091AE77 /* GoogleSignInSwift */; }; @@ -1304,6 +1305,7 @@ 4049CE832BBBF8EF003D07D2 /* StreamAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamAsyncImage.swift; sourceTree = ""; }; 404A5CFA2AD5648100EF1C62 /* DemoChatModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChatModifier.swift; sourceTree = ""; }; 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DemoChatViewModel+Injection.swift"; sourceTree = ""; }; + 405EFF102C7F120700AC5CE6 /* SFUMiddlewareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFUMiddlewareTests.swift; sourceTree = ""; }; 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallState_Tests.swift; sourceTree = ""; }; 406303412AD848000091AE77 /* CallParticipant_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipant_Mock.swift; sourceTree = ""; }; 406583852B87694B00B4F979 /* BlurBackgroundVideoFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurBackgroundVideoFilter.swift; sourceTree = ""; }; @@ -4081,6 +4083,7 @@ 8414081229F28B5600FF2D7C /* RTCConfiguration_Tests.swift */, 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */, 841FF5042A5D815700809BBB /* VideoCapturerUtils_Tests.swift */, + 405EFF102C7F120700AC5CE6 /* SFUMiddlewareTests.swift */, ); path = WebRTC; sourceTree = ""; @@ -5763,6 +5766,7 @@ 8251E62B2A17BEB400E7257A /* StreamVideoTestResources.swift in Sources */, 403FB14C2BFE14760047A696 /* Publisher_NextTests.swift in Sources */, 84A4DCBB2A41DC6E00B1D1BF /* AsyncAssert.swift in Sources */, + 405EFF112C7F120700AC5CE6 /* SFUMiddlewareTests.swift in Sources */, 84CBBE0B29228BA900D0DA61 /* StreamVideoTestCase.swift in Sources */, 40F017512BBEF00500E89FD1 /* ScreensharingSettings+Dummy.swift in Sources */, 403FB14E2BFE18D10047A696 /* StreamStateMachine_Tests.swift in Sources */, diff --git a/StreamVideoTests/WebRTC/SFUMiddlewareTests.swift b/StreamVideoTests/WebRTC/SFUMiddlewareTests.swift new file mode 100644 index 000000000..e3d03f1a6 --- /dev/null +++ b/StreamVideoTests/WebRTC/SFUMiddlewareTests.swift @@ -0,0 +1,62 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation +@testable import StreamVideo +import XCTest + +final class SFUMiddlewareTests: XCTestCase { + + private lazy var sessionID: String! = .unique + private lazy var user: User! = .init(id: .unique) + private lazy var state: WebRTCClient.State! = .init() + private lazy var signalService: Stream_Video_Sfu_Signal_SignalServer! = .init( + httpClient: HTTPClient_Mock(), + apiKey: .unique, + hostname: .unique, + token: .unique + ) + private lazy var participantThreshold: Int! = 10 + + private lazy var subject: SfuMiddleware! = .init( + sessionID: sessionID, + user: user, + state: state, + signalService: signalService, + participantThreshold: participantThreshold + ) + + override func tearDown() { + subject = nil + user = nil + state = nil + signalService = nil + participantThreshold = nil + super.tearDown() + } + + // MARK: - handle(event:) + + func test_handle_healthCheck_passesCorrectValuesForParticipantsAndAnonymous() async throws { + var participantCount = Stream_Video_Sfu_Models_ParticipantCount() + participantCount.total = 10 + participantCount.anonymous = 3 + var healthCheckInfo = Stream_Video_Sfu_Event_HealthCheckResponse() + healthCheckInfo.participantCount = participantCount + let participantsCountExpectation = expectation(description: "onParticipantCountUpdated was called") + let anonymousCountExpectation = expectation(description: "onAnonymousParticipantCountUpdated was called") + subject.onParticipantCountUpdated = { + XCTAssertEqual($0, participantCount.total) + participantsCountExpectation.fulfill() + } + subject.onAnonymousParticipantCountUpdated = { + XCTAssertEqual($0, participantCount.anonymous) + anonymousCountExpectation.fulfill() + } + + _ = subject.handle(event: .sfuEvent(.healthCheckResponse(healthCheckInfo))) + + await fulfillment(of: [participantsCountExpectation, anonymousCountExpectation]) + } +} From fbfa582a6c971da2e2be6482d22f07c7f5cae2f4 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Thu, 29 Aug 2024 15:06:49 +0300 Subject: [PATCH 32/38] [Fix]Allow callSettings propagation to CallState (#497) --- CHANGELOG.md | 1 + Sources/StreamVideo/Call.swift | 11 ++++++++++- Sources/StreamVideo/StreamVideo.swift | 8 ++++++-- Sources/StreamVideoSwiftUI/CallViewModel.swift | 12 ++++++++++-- StreamVideoTests/Mock/MockStreamVideo.swift | 6 +++++- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 397b83705..ff2be3bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### ✅ Added - Participants (regular and anonymous) count, can be accessed - before or after joining a call - from the `Call.state.participantCount` & `Call.state.anonymousParticipantCount` respectively. [#496](https://github.com/GetStream/stream-video-swift/pull/496) +- You can now provide the `CallSettings` when you start a ringing call [#497](https://github.com/GetStream/stream-video-swift/pull/497) # [1.0.9](https://github.com/GetStream/stream-video-swift/releases/tag/1.0.9) _July 19, 2024_ diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index 54c1e12be..4f4fe3b09 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -43,7 +43,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { callType: String, callId: String, coordinatorClient: DefaultAPI, - callController: CallController + callController: CallController, + callSettings: CallSettings? = nil ) { self.callId = callId self.callType = callType @@ -63,6 +64,14 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { initialSpeakerStatus: .enabled, initialAudioOutputStatus: .enabled ) + + /// If we received a non-nil initial callSettings, we updated them here. + if let callSettings { + Task { @MainActor [weak self] in + self?.state.update(callSettings: callSettings) + } + } + self.callController.call = self // It's important to instantiate the stateMachine as soon as possible // to ensure it's uniqueness. diff --git a/Sources/StreamVideo/StreamVideo.swift b/Sources/StreamVideo/StreamVideo.swift index ee0c2b92e..94da6b3b6 100644 --- a/Sources/StreamVideo/StreamVideo.swift +++ b/Sources/StreamVideo/StreamVideo.swift @@ -215,10 +215,13 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { /// - Parameters: /// - callType: the type of the call. /// - callId: the id of the all. + /// - callSettings: the initial CallSettings to use. If `nil` is provided, the default CallSettings + /// will be used. /// - Returns: `Call` object. public func call( callType: String, - callId: String + callId: String, + callSettings: CallSettings? = nil ) -> Call { callCache.call(for: callCid(from: callId, callType: callType)) { let callController = makeCallController(callType: callType, callId: callId) @@ -226,7 +229,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { callType: callType, callId: callId, coordinatorClient: coordinatorClient, - callController: callController + callController: callController, + callSettings: callSettings ) eventsMiddleware.add(subscriber: call) return call diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 825296f6e..6034593e1 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -351,7 +351,11 @@ open class CallViewModel: ObservableObject { backstage: backstage ) } else { - let call = streamVideo.call(callType: callType, callId: callId) + let call = streamVideo.call( + callType: callType, + callId: callId, + callSettings: callSettings + ) self.call = call Task { do { @@ -572,7 +576,11 @@ open class CallViewModel: ObservableObject { enteringCallTask = Task { do { log.debug("Starting call") - let call = call ?? streamVideo.call(callType: callType, callId: callId) + let call = call ?? streamVideo.call( + callType: callType, + callId: callId, + callSettings: callSettings + ) var settingsRequest: CallSettingsRequest? var limits: LimitsSettingsRequest? if maxDuration != nil || maxParticipants != nil { diff --git a/StreamVideoTests/Mock/MockStreamVideo.swift b/StreamVideoTests/Mock/MockStreamVideo.swift index 51a42837f..463d22e6d 100644 --- a/StreamVideoTests/Mock/MockStreamVideo.swift +++ b/StreamVideoTests/Mock/MockStreamVideo.swift @@ -60,7 +60,11 @@ final class MockStreamVideo: StreamVideo, Mockable { stubbedFunction[function] = value } - override func call(callType: String, callId: String) -> Call { + override func call( + callType: String, + callId: String, + callSettings: CallSettings? = nil + ) -> Call { stubbedFunction[.call] as! Call } From a51b9f0bbf015bb8c953220883c56468f4ede047 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 29 Aug 2024 14:02:21 +0100 Subject: [PATCH 33/38] [CI] Fix fastlane lane syntax (#498) --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 274ddfc66..bf2ae5eff 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -700,7 +700,7 @@ lane :show_frameworks_sizes do |options| update_img_shields_sdk_sizes(sizes: sizes, open_pr: options[:open_pr]) if options[:update_readme] end -lane :update_img_shields_sdk_sizes do +lane :update_img_shields_sdk_sizes do |options| update_sdk_size_in_readme( open_pr: options[:open_pr] || false, readme_path: 'README.md', From e93bcf8a247fac25fec2256624edb7772f0ae660 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 30 Aug 2024 12:23:41 +0100 Subject: [PATCH 34/38] [CI] Do not cache iOS Simulator Runtimes nightly (#500) --- .github/actions/setup-ios-runtime/action.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 2aa2f0454..aeec23e13 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -3,13 +3,6 @@ description: 'Download and Install requested iOS Runtime' runs: using: "composite" steps: - - name: Cache iOS Simulator Runtime - uses: actions/cache@v4 - id: runtime-cache - with: - path: ./*.dmg - key: ipsw-runtime-ios-${{ inputs.version }} - restore-keys: ipsw-runtime-ios-${{ inputs.version }} - name: Setup iOS Simulator Runtime shell: bash run: | From 95792a4a5fcb8048c5f3403eb937b6dbaed7afeb Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Tue, 3 Sep 2024 10:17:47 +0300 Subject: [PATCH 35/38] [Workaround]ParticipantCount event backend fix (#502) --- .../StreamVideo/Controllers/CallController.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index 7d7e50ada..5ece590fc 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -717,13 +717,24 @@ class CallController: @unchecked Sendable { private func subscribeToParticipantsCountUpdatesEvent(_ call: Call) -> Task { Task { + let anonymousUserRoleKey = "anonymous" for await event in call.subscribe(for: CallSessionParticipantCountsUpdatedEvent.self) { Task { @MainActor in - call.state.participantCount = event.participantsCountByRole + call.state.participantCount = event + .participantsCountByRole + .filter { $0.key != anonymousUserRoleKey } // TODO: Workaround. To be removed .values .map(UInt32.init) .reduce(0) { $0 + $1 } - call.state.anonymousParticipantCount = UInt32(event.anonymousParticipantCount) + + // TODO: Workaround. To be removed + if event.anonymousParticipantCount > 0 { + call.state.anonymousParticipantCount = UInt32(event.anonymousParticipantCount) + } else if let anonymousCount = event.participantsCountByRole[anonymousUserRoleKey] { + call.state.anonymousParticipantCount = UInt32(anonymousCount) + } else { + call.state.anonymousParticipantCount = 0 + } } } } From 17fbf0fc1090fca15debff754bc9b34f2da38b20 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Tue, 3 Sep 2024 17:26:16 +0300 Subject: [PATCH 36/38] [Fix]CalSettings propagation to call (#505) --- Sources/StreamVideo/Call.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index 4f4fe3b09..063cd31f8 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -52,17 +52,17 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { self.callController = callController microphone = MicrophoneManager( callController: callController, - initialStatus: .enabled + initialStatus: callSettings?.audioOn == false ? .disabled : .enabled ) camera = CameraManager( callController: callController, - initialStatus: .enabled, + initialStatus: callSettings?.videoOn == false ? .disabled : .enabled, initialDirection: .front ) speaker = SpeakerManager( callController: callController, - initialSpeakerStatus: .enabled, - initialAudioOutputStatus: .enabled + initialSpeakerStatus: callSettings?.speakerOn == false ? .disabled : .enabled, + initialAudioOutputStatus: callSettings?.audioOutputOn == false ? .disabled : .enabled ) /// If we received a non-nil initial callSettings, we updated them here. From b9708bb4c6e7872267c11db0da0cf476b1e36185 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 29 Aug 2024 13:18:08 +0000 Subject: [PATCH 37/38] Bump 1.10.0 --- CHANGELOG.md | 5 +++++ README.md | 8 ++++---- .../StreamVideo/Generated/SystemEnvironment+Version.swift | 2 +- Sources/StreamVideo/Info.plist | 2 +- Sources/StreamVideoSwiftUI/Info.plist | 2 +- Sources/StreamVideoUIKit/Info.plist | 2 +- StreamVideo-XCFramework.podspec | 2 +- StreamVideo.podspec | 2 +- StreamVideoArtifacts.json | 2 +- StreamVideoSwiftUI-XCFramework.podspec | 2 +- StreamVideoSwiftUI.podspec | 2 +- StreamVideoUIKit-XCFramework.podspec | 2 +- StreamVideoUIKit.podspec | 2 +- 13 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2be3bf0..6b02488e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🔄 Changed + +# [1.10.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.10.0) +_August 29, 2024_ + ### ✅ Added - Participants (regular and anonymous) count, can be accessed - before or after joining a call - from the `Call.state.participantCount` & `Call.state.anonymousParticipantCount` respectively. [#496](https://github.com/GetStream/stream-video-swift/pull/496) - You can now provide the `CallSettings` when you start a ringing call [#497](https://github.com/GetStream/stream-video-swift/pull/497) diff --git a/README.md b/README.md index 0fb82306e..76d4d7659 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@

- StreamVideo - StreamVideoSwiftUI - StreamVideoUIKit - StreamWebRTC + StreamVideo + StreamVideoSwiftUI + StreamVideoUIKit + StreamWebRTC

![Stream Video for iOS Header image](https://github.com/GetStream/stream-video-swift/assets/12433593/e4a44ae5-a8eb-4ac7-8910-28187aa011f6) diff --git a/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift b/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift index 97cd5ac23..3b1904962 100644 --- a/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift @@ -7,7 +7,7 @@ import Foundation extension SystemEnvironment { /// A Stream Video version. - public static let version: String = "1.0.9" + public static let version: String = "1.10.0" /// The WebRTC version. public static let webRTCVersion: String = "114.5735.8" } diff --git a/Sources/StreamVideo/Info.plist b/Sources/StreamVideo/Info.plist index b1d7e4550..2a68649f6 100644 --- a/Sources/StreamVideo/Info.plist +++ b/Sources/StreamVideo/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.9 + 1.10.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideoSwiftUI/Info.plist b/Sources/StreamVideoSwiftUI/Info.plist index b1d7e4550..2a68649f6 100644 --- a/Sources/StreamVideoSwiftUI/Info.plist +++ b/Sources/StreamVideoSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.9 + 1.10.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideoUIKit/Info.plist b/Sources/StreamVideoUIKit/Info.plist index b1d7e4550..2a68649f6 100644 --- a/Sources/StreamVideoUIKit/Info.plist +++ b/Sources/StreamVideoUIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.9 + 1.10.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/StreamVideo-XCFramework.podspec b/StreamVideo-XCFramework.podspec index 15387b725..13db3de20 100644 --- a/StreamVideo-XCFramework.podspec +++ b/StreamVideo-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideo-XCFramework' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' diff --git a/StreamVideo.podspec b/StreamVideo.podspec index 2abdefd61..612d77b45 100644 --- a/StreamVideo.podspec +++ b/StreamVideo.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideo' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' diff --git a/StreamVideoArtifacts.json b/StreamVideoArtifacts.json index 821056842..1f156071e 100644 --- a/StreamVideoArtifacts.json +++ b/StreamVideoArtifacts.json @@ -1 +1 @@ -{"0.4.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.4.2/StreamVideo-All.zip","0.5.0":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.0/StreamVideo-All.zip","0.5.1":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.1/StreamVideo-All.zip","0.5.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.2/StreamVideo-All.zip","0.5.3":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.3/StreamVideo-All.zip","1.0.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.0/StreamVideo-All.zip","1.0.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.1/StreamVideo-All.zip","1.0.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.2/StreamVideo-All.zip","1.0.3":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.3/StreamVideo-All.zip","1.0.4":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.4/StreamVideo-All.zip","1.0.5":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.5/StreamVideo-All.zip","1.0.6":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.6/StreamVideo-All.zip","1.0.7":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.7/StreamVideo-All.zip","1.0.8":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.8/StreamVideo-All.zip","1.0.9":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.9/StreamVideo-All.zip"} \ No newline at end of file +{"0.4.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.4.2/StreamVideo-All.zip","0.5.0":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.0/StreamVideo-All.zip","0.5.1":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.1/StreamVideo-All.zip","0.5.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.2/StreamVideo-All.zip","0.5.3":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.3/StreamVideo-All.zip","1.0.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.0/StreamVideo-All.zip","1.0.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.1/StreamVideo-All.zip","1.0.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.2/StreamVideo-All.zip","1.0.3":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.3/StreamVideo-All.zip","1.0.4":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.4/StreamVideo-All.zip","1.0.5":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.5/StreamVideo-All.zip","1.0.6":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.6/StreamVideo-All.zip","1.0.7":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.7/StreamVideo-All.zip","1.0.8":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.8/StreamVideo-All.zip","1.0.9":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.9/StreamVideo-All.zip","1.10.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.10.0/StreamVideo-All.zip"} \ No newline at end of file diff --git a/StreamVideoSwiftUI-XCFramework.podspec b/StreamVideoSwiftUI-XCFramework.podspec index c669109b3..77e0228e5 100644 --- a/StreamVideoSwiftUI-XCFramework.podspec +++ b/StreamVideoSwiftUI-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoSwiftUI-XCFramework' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo SwiftUI Video Components' spec.description = 'StreamVideoSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoSwiftUI.podspec b/StreamVideoSwiftUI.podspec index 0fd735ec8..37d725b08 100644 --- a/StreamVideoSwiftUI.podspec +++ b/StreamVideoSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoSwiftUI' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo SwiftUI Video Components' spec.description = 'StreamVideoSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoUIKit-XCFramework.podspec b/StreamVideoUIKit-XCFramework.podspec index 3fe883c3e..f3a3d8e63 100644 --- a/StreamVideoUIKit-XCFramework.podspec +++ b/StreamVideoUIKit-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoUIKit-XCFramework' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo UIKit Video Components' spec.description = 'StreamVideoUIKit SDK offers flexible UIKit components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoUIKit.podspec b/StreamVideoUIKit.podspec index e85ad060e..b35928e12 100644 --- a/StreamVideoUIKit.podspec +++ b/StreamVideoUIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoUIKit' - spec.version = '1.0.9' + spec.version = '1.10.0' spec.summary = 'StreamVideo UIKit Video Components' spec.description = 'StreamVideoUIKit SDK offers flexible UIKit components able to display data provided by StreamVideo SDK.' From 151c2d3c009aeb58e0a365f3e81b8bfe667fc965 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 3 Sep 2024 16:04:36 +0100 Subject: [PATCH 38/38] [CI] Clean up some disk space on CI before downloading iOS runtimes (#506) --- .github/actions/setup-ios-runtime/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index aeec23e13..89164b238 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -6,8 +6,10 @@ runs: - name: Setup iOS Simulator Runtime shell: bash run: | + sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true brew install blacktop/tap/ipsw bundle exec fastlane install_runtime ios:${{ inputs.version }} + sudo rm -rfv *.dmg || true xcrun simctl list runtimes - name: Create Custom iOS Simulator shell: bash