Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for media recordings #140

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c2dd174
Refactor error handling in create-stream.js
Mar 23, 2022
71e6a95
Create stream document in create-stream route
Mar 23, 2022
a20aa69
Refactor create-stream a little more
Mar 23, 2022
059aa90
Refactor errors in join-stream; add permissions to stream document
Mar 23, 2022
d161ee3
Refactor error handling in join-stream; add permissions to stream doc
Mar 23, 2022
39051bf
Fix bug in record stream
Mar 24, 2022
a0caf7b
Display recording status
timrozum Mar 25, 2022
30f2190
Fix proxy
Mar 30, 2022
3d9bd8a
Add checkbox to enable recordings
Mar 30, 2022
63ed6f4
Update useIsRecording to listen to StreamDocument updates
Mar 30, 2022
816d80d
Add mock media-processor-webhook endpoint
Mar 30, 2022
b719dcc
Fix if statement
Mar 30, 2022
e9bcb78
Merge branch 'update-functions-to-support-recording' into add-recordi…
Mar 30, 2022
22dd9f6
Minor fixes
timrozum Mar 31, 2022
a1c4cae
Add recording indicator to the Player component
Apr 12, 2022
b2a490e
Add recording error to useIsRecording hook
Apr 12, 2022
1511e03
Update usage of useIsRecording Hook
Apr 12, 2022
5485fc9
Add useRecordingNotifications hook
Apr 12, 2022
5744626
Fix positioning of the Recording bagde
Apr 12, 2022
6aef4c4
Remove old RecordingNotifications component
Apr 12, 2022
803d6fe
Update useRecordingNotifications
Apr 12, 2022
66d61c6
Refactor delete-stream function
Apr 12, 2022
6fccd4e
Refactor raise-hand endpoint
Apr 12, 2022
67ce197
Refactor viewer-connected-to-player endpoint
Apr 12, 2022
a0c832b
Refactor sync-webhook
Apr 12, 2022
8d287aa
Refactor rooms-webhook
Apr 12, 2022
df7f3b0
Merge branch 'main' into task/recording
timrozum Apr 12, 2022
e9f2473
Merge branch 'update-functions-to-support-recording' into add-recordi…
Apr 13, 2022
42600c3
Fix typos
May 3, 2022
441b114
Remove sync_object_names
May 3, 2022
c3f223f
Merge pull request #111 from twilio/update-functions-to-support-recor…
Jun 9, 2022
1793ed2
Merge pull request #112 from twilio/add-recording-ui-web
Jun 9, 2022
88f1f41
Merge pull request #108 from twilio/task/recording
timrozum Jun 9, 2022
b4f6fec
Merge branch 'main' into feature/recording-web
timrozum Jun 9, 2022
8d0715e
Configure recording when creating media processor
timrozum Jun 13, 2022
164e636
Handle recording error in media processor callback
timrozum Jun 14, 2022
a439600
Update readme
timrozum Jun 14, 2022
cd80428
Add recordings script
timrozum Jun 17, 2022
ed09d19
Add date updated to the recordings script output
timrozum Jun 21, 2022
4601d52
Recording script improvements
timrozum Jun 22, 2022
b85ac73
Use multiple lines to output each recording
timrozum Jun 22, 2022
cbc04bf
Tiny code format change
timrozum Jun 22, 2022
e2881b2
Merge pull request #133 from twilio/connect-recording-to-api
timrozum Jun 22, 2022
c76acf3
Update readme
timrozum Jun 29, 2022
6987c59
Merge pull request #139 from twilio/task/update-readme
timrozum Jun 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ If you want to edit the functions that have been deployed to Twilio Serverless,
1. Enter passcode from the [backend deploy](#deploy-the-app-to-twilio) and tap `Continue`.
1. Tap `Create Event` to host a new stream or `Join Event` to join a stream as a viewer or a speaker.

## Recordings

The event host has the option to enable recording in the app UI when they create the event. A recording will be available shortly after the event ends. To view a list of all recordings, run `npm run recordings`.

## Reference Backend

The API for the reference backend used by the clients is specified [here](ReferenceBackendAPI.md).
Expand Down
3 changes: 2 additions & 1 deletion ReferenceBackendAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Request parameters:
```json
{
"user_identity": "Bob",
"stream_name": "demo"
"stream_name": "demo",
"record_stream": true
}
```

Expand Down
8 changes: 8 additions & 0 deletions apps/ios/LiveVideo/LiveVideo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
DCB1BE0B27581C11006CE9D1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = DCB1BE0A27581C11006CE9D1 /* Nimble */; };
DCB4497426F5496400B52774 /* SpeakerSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB4497326F5496400B52774 /* SpeakerSettingsManager.swift */; };
DCB4497626F65A7000B52774 /* SpeakerVideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB4497526F65A7000B52774 /* SpeakerVideoViewModel.swift */; };
DCB8FA7B27EE17E00001EEB1 /* SyncStreamDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB8FA7A27EE17DF0001EEB1 /* SyncStreamDocument.swift */; };
DCB8FA8127EE51630001EEB1 /* RecordingBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB8FA8027EE51630001EEB1 /* RecordingBadge.swift */; };
DCC1D62626D9396400892038 /* StreamManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC1D62526D9396400892038 /* StreamManager.swift */; };
DCC1D62826D9568700892038 /* StreamConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC1D62726D9568700892038 /* StreamConfig.swift */; };
DCC1D62A26D9643400892038 /* FormStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC1D62926D9643400892038 /* FormStack.swift */; };
Expand Down Expand Up @@ -184,6 +186,8 @@
DCB1BE042756D954006CE9D1 /* EnterPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPasscodeView.swift; sourceTree = "<group>"; };
DCB4497326F5496400B52774 /* SpeakerSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerSettingsManager.swift; sourceTree = "<group>"; };
DCB4497526F65A7000B52774 /* SpeakerVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerVideoViewModel.swift; sourceTree = "<group>"; };
DCB8FA7A27EE17DF0001EEB1 /* SyncStreamDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStreamDocument.swift; sourceTree = "<group>"; };
DCB8FA8027EE51630001EEB1 /* RecordingBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingBadge.swift; sourceTree = "<group>"; };
DCC1D62526D9396400892038 /* StreamManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamManager.swift; sourceTree = "<group>"; };
DCC1D62726D9568700892038 /* StreamConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamConfig.swift; sourceTree = "<group>"; };
DCC1D62926D9643400892038 /* FormStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormStack.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -259,6 +263,7 @@
children = (
DC175F2C270E232F00D9D1FE /* SyncManager.swift */,
DC175F3D2717530D00D9D1FE /* SyncObjectConnecting.swift */,
DCB8FA7A27EE17DF0001EEB1 /* SyncStreamDocument.swift */,
DC175F31270E23F900D9D1FE /* SyncUserDocument.swift */,
DC175F33270E3FD200D9D1FE /* SyncUsersMap.swift */,
);
Expand Down Expand Up @@ -472,6 +477,7 @@
DCCA726E27B590F6006B0441 /* DisplayNameFactory.swift */,
DC1ED0EF26DFCD3C00FFA769 /* LiveBadge.swift */,
DC9E18382729DC740070F53F /* OffscreenSpeakersView.swift */,
DCB8FA8027EE51630001EEB1 /* RecordingBadge.swift */,
DC1ED12F26E960C600FFA769 /* SpeakerGridView.swift */,
DC504A7426F0F18600C37EC9 /* SpeakerGridViewModel.swift */,
DC504A7626F1468E00C37EC9 /* SpeakerVideoView.swift */,
Expand Down Expand Up @@ -720,11 +726,13 @@
DC1ED0FD26E14D0B00FFA769 /* StreamToolbar.swift in Sources */,
DC1ED0F926E1440200FFA769 /* StreamStatusView.swift in Sources */,
DC504A7726F1468E00C37EC9 /* SpeakerVideoView.swift in Sources */,
DCB8FA7B27EE17E00001EEB1 /* SyncStreamDocument.swift in Sources */,
DC175F34270E3FD200D9D1FE /* SyncUsersMap.swift in Sources */,
DCC48FA326D8322900EE49EF /* SwiftUIPlayerView.swift in Sources */,
DC4F45E92723135500BE730B /* CardButtonLabel.swift in Sources */,
DCADF33927FCA7780093A9FE /* TitleValueView.swift in Sources */,
DC1ED0F726E12C7400FFA769 /* ProgressHUD.swift in Sources */,
DCB8FA8127EE51630001EEB1 /* RecordingBadge.swift in Sources */,
DC175F32270E23F900D9D1FE /* SyncUserDocument.swift in Sources */,
DCC1D62626D9396400892038 /* StreamManager.swift in Sources */,
DCCA727127B5BFE8006B0441 /* PresentationStatusView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
"repositoryURL": "https://github.com/twilio/twilio-live-player-ios",
"state": {
"branch": null,
"revision": "7c4827e0bdb3b935aecdce9fa3c1b3c42ba60dd5",
"version": "1.0.1"
"revision": "97dff98041229cfe90c9d2e5b930c836279a9a48",
"version": "1.1.0"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.663"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
4 changes: 4 additions & 0 deletions apps/ios/LiveVideo/LiveVideo/Helpers/LiveVideoError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import Foundation
enum LiveVideoError: Error {
case backendError(message: String)
case passcodeIncorrect
case recordError(message: String)
case speakerMovedToViewersByHost
case streamEndedByHost
case syncClientConnectionFatalError
case syncObjectDecodeError
case syncTokenExpired
}

Expand All @@ -18,9 +20,11 @@ extension LiveVideoError: LocalizedError {
switch self {
case let .backendError(message): return message
case .passcodeIncorrect: return "Passcode incorrect."
case let .recordError(message): return message
case .speakerMovedToViewersByHost: return "Speaker moved to viewers by host."
case .streamEndedByHost: return "Event ended by host."
case .syncClientConnectionFatalError: return "Sync client connection fatal error."
case .syncObjectDecodeError: return "Sync object decode error."
case .syncTokenExpired: return "Sync token expired."
}
}
Expand Down
12 changes: 8 additions & 4 deletions apps/ios/LiveVideo/LiveVideo/Launch/LiveVideoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct LiveVideoApp: App {
@StateObject private var speakerGridViewModel = SpeakerGridViewModel()
@StateObject private var presentationLayoutViewModel = PresentationLayoutViewModel()
@StateObject private var api = API()
@StateObject private var streamDocument = SyncStreamDocument()
@StateObject private var appSettingsManager = AppSettingsManager()

var body: some Scene {
Expand All @@ -30,22 +31,24 @@ struct LiveVideoApp: App {
.environmentObject(speakerSettingsManager)
.environmentObject(hostControlsManager)
.environmentObject(api)
.environmentObject(streamDocument)
.environmentObject(appSettingsManager)
.onAppear {
authManager.configure(api: api, appSettingsManager: appSettingsManager)
let localParticipant = LocalParticipantManager(authManager: authManager)
let roomManager = RoomManager()
roomManager.configure(localParticipant: localParticipant)
let userDocument = SyncUserDocument()
let speakersMap = SyncUsersMap()
let raisedHandsMap = SyncUsersMap()
let viewersMap = SyncUsersMap()
let speakersMap = SyncUsersMap(uniqueName: "speakers")
let raisedHandsMap = SyncUsersMap(uniqueName: "raised_hands")
let viewersMap = SyncUsersMap(uniqueName: "viewers")
let speakerVideoViewModelFactory = SpeakerVideoViewModelFactory()
let syncManager = SyncManager(
speakersMap: speakersMap,
viewersMap: viewersMap,
raisedHandsMap: raisedHandsMap,
userDocument: userDocument,
streamDocument: streamDocument,
appSettingsManager: appSettingsManager
)
streamManager.configure(
Expand All @@ -59,7 +62,8 @@ struct LiveVideoApp: App {
streamManager: streamManager,
speakerSettingsManager: speakerSettingsManager,
api: api,
userDocument: userDocument
userDocument: userDocument,
streamDocument: streamDocument
)
participantsViewModel.configure(
streamManager: streamManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,19 @@ struct CreateOrJoinStreamRequest: APIRequest {
struct Parameters: Encodable {
let userIdentity: String
let streamName: String
let recordStream: Bool?
}

struct Response: Decodable {
struct SyncObjectNames: Decodable {
let speakersMap: String
let viewersMap: String
let raisedHandsMap: String
let userDocument: String?
}

let token: String
let syncObjectNames: SyncObjectNames
}

let path: String
let parameters: Parameters
let responseType = Response.self

init(userIdentity: String, streamName: String, role: StreamConfig.Role) {
parameters = Parameters(userIdentity: userIdentity, streamName: streamName)
init(userIdentity: String, streamName: String, role: StreamConfig.Role, recordStream: Bool?) {
parameters = Parameters(userIdentity: userIdentity, streamName: streamName, recordStream: recordStream)
path = role.path
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ class LocalParticipantManager: NSObject {
private(set) var micTrack: LocalAudioTrack?
private(set) var cameraTrack: LocalVideoTrack?
private let app = UIApplication.shared
private let authManager: AuthManager
private var cameraSource: CameraSource?
private var authManager: AuthManager

init(authManager: AuthManager) {
self.authManager = authManager
Expand Down
10 changes: 10 additions & 0 deletions apps/ios/LiveVideo/LiveVideo/Twilio/Stream/StreamConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,15 @@ struct StreamConfig {

let streamName: String
let userIdentity: String
let shouldRecord: Bool?
var role: Role

var hasUserDocument: Bool {
switch role {
case .host:
return false
case .speaker, .viewer:
return true
}
}
}
20 changes: 9 additions & 11 deletions apps/ios/LiveVideo/LiveVideo/Twilio/Stream/StreamManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,33 +109,31 @@ class StreamManager: ObservableObject {
let request = CreateOrJoinStreamRequest(
userIdentity: config.userIdentity,
streamName: config.streamName,
role: config.role
role: config.role,
recordStream: config.shouldRecord
)

api.request(request) { [weak self] result in
switch result {
case let .success(response):
let objectNames = SyncManager.ObjectNames(
speakersMap: response.syncObjectNames.speakersMap,
viewersMap: response.syncObjectNames.viewersMap,
raisedHandsMap: response.syncObjectNames.raisedHandsMap,
userDocument: response.syncObjectNames.userDocument
)

self?.connectSync(accessToken: response.token, objectNames: objectNames)
self?.connectSync(accessToken: response.token)
case let .failure(error):
self?.handleError(error)
}
}
}

private func connectSync(accessToken: String, objectNames: SyncManager.ObjectNames) {
private func connectSync(accessToken: String) {
guard !syncManager.isConnected else {
connectRoomOrPlayer(accessToken: accessToken)
return
}

syncManager.connect(token: accessToken, objectNames: objectNames) { [weak self] error in
syncManager.connect(
token: accessToken,
userIdentity: config.userIdentity,
hasUserDocument: config.hasUserDocument
) { [weak self] error in
if let error = error {
self?.handleError(error)
return
Expand Down
Loading