Various improvements

This commit is contained in:
Ilya Laktyushin 2025-11-07 14:13:09 +04:00
parent a05ef5e022
commit 22945ebb31
22 changed files with 662 additions and 363 deletions

View file

@ -339,43 +339,34 @@ private final class CameraContext {
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self, let mainDeviceContext = self.mainDeviceContext else {
return
}
}
var front = false
if #available(iOS 13.0, *) {
front = connection.inputPorts.first?.sourceDevicePosition == .front
}
self.mainVideoOutput?.push(sampleBuffer, mirror: front)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording || !self.savedSnapshot {
var front = false
if #available(iOS 13.0, *) {
front = connection.inputPorts.first?.sourceDevicePosition == .front
}
if sampleBuffer.type == kCMMediaType_Video {
Queue.mainQueue().async {
self.mainVideoOutput?.push(sampleBuffer, mirror: front)
}
}
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
self.lastSnapshotTimestamp = timestamp
self.savedSnapshot = true
}
}
self.mainDeviceContext?.output.processAudioBuffer = { [weak self] sampleBuffer in
let _ = self
}
self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self, let additionalDeviceContext = self.additionalDeviceContext else {
return
}
var front = false
if #available(iOS 13.0, *) {
front = connection.inputPorts.first?.sourceDevicePosition == .front
}
self.additionalVideoOutput?.push(sampleBuffer, mirror: front)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastAdditionalSnapshotTimestamp + 2.5, !additionalDeviceContext.output.isRecording || !self.savedAdditionalSnapshot {
var front = false
if #available(iOS 13.0, *) {
front = connection.inputPorts.first?.sourceDevicePosition == .front
}
Queue.mainQueue().async {
self.additionalVideoOutput?.push(sampleBuffer, mirror: front)
}
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
self.lastAdditionalSnapshotTimestamp = timestamp
self.savedAdditionalSnapshot = true
@ -402,10 +393,7 @@ private final class CameraContext {
if #available(iOS 13.0, *) {
front = connection.inputPorts.first?.sourceDevicePosition == .front
}
Queue.mainQueue().async {
self.mainVideoOutput?.push(sampleBuffer, mirror: front)
}
self.mainVideoOutput?.push(sampleBuffer, mirror: front)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording || !self.savedSnapshot {
@ -414,10 +402,6 @@ private final class CameraContext {
self.savedSnapshot = true
}
}
self.mainDeviceContext?.output.processAudioBuffer = { [weak self] sampleBuffer in
let _ = self
}
// if self.initialConfiguration.reportAudioLevel {
// self.mainDeviceContext?.output.processAudioBuffer = { [weak self] sampleBuffer in
// guard let self else {

View file

@ -139,7 +139,7 @@ final class CameraOutput: NSObject {
}
self.videoOutput.alwaysDiscardsLateVideoFrames = false
self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: use32BGRA ? kCVPixelFormatType_32BGRA : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any]
self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: use32BGRA ? kCVPixelFormatType_32BGRA : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] as [String : Any]
}
deinit {
@ -471,90 +471,91 @@ final class CameraOutput: NSObject {
private var videoSwitchSampleTimeOffset: CMTime?
func processVideoRecording(_ sampleBuffer: CMSampleBuffer, fromAdditionalOutput: Bool) {
guard let videoRecorder = self.videoRecorder, videoRecorder.isRecording else {
return
}
guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else {
return
}
let type = CMFormatDescriptionGetMediaType(formatDescriptor)
if let videoRecorder = self.videoRecorder, videoRecorder.isRecording {
if case .roundVideo = self.currentMode, type == kCMMediaType_Video {
let currentTimestamp = CACurrentMediaTime()
let duration: Double = 0.2
if !self.exclusive {
var transitionFactor: CGFloat = 0.0
if case .front = self.currentPosition {
transitionFactor = 1.0
if self.lastSwitchTimestamp > 0.0, currentTimestamp - self.lastSwitchTimestamp < duration {
transitionFactor = max(0.0, (currentTimestamp - self.lastSwitchTimestamp) / duration)
}
} else {
transitionFactor = 0.0
if self.lastSwitchTimestamp > 0.0, currentTimestamp - self.lastSwitchTimestamp < duration {
transitionFactor = 1.0 - max(0.0, (currentTimestamp - self.lastSwitchTimestamp) / duration)
}
}
if (transitionFactor == 1.0 && fromAdditionalOutput)
|| (transitionFactor == 0.0 && !fromAdditionalOutput)
|| (transitionFactor > 0.0 && transitionFactor < 1.0) {
if let processedSampleBuffer = self.processRoundVideoSampleBuffer(sampleBuffer, additional: fromAdditionalOutput, transitionFactor: transitionFactor) {
let presentationTime = CMSampleBufferGetPresentationTimeStamp(processedSampleBuffer)
if let lastSampleTimestamp = self.lastSampleTimestamp, lastSampleTimestamp > presentationTime {
} else {
videoRecorder.appendSampleBuffer(processedSampleBuffer)
self.lastSampleTimestamp = presentationTime
}
}
if case .roundVideo = self.currentMode, type == kCMMediaType_Video {
let currentTimestamp = CACurrentMediaTime()
let duration: Double = 0.2
if !self.exclusive {
var transitionFactor: CGFloat = 0.0
if case .front = self.currentPosition {
transitionFactor = 1.0
if self.lastSwitchTimestamp > 0.0, currentTimestamp - self.lastSwitchTimestamp < duration {
transitionFactor = max(0.0, (currentTimestamp - self.lastSwitchTimestamp) / duration)
}
} else {
var additional = self.currentPosition == .front
var transitionFactor = self.currentPosition == .front ? 1.0 : 0.0
if self.lastSwitchTimestamp > 0.0 {
if self.needsCrossfadeTransition {
self.needsCrossfadeTransition = false
self.crossfadeTransitionStart = currentTimestamp + 0.03
self.needsSwitchSampleOffset = true
}
if self.crossfadeTransitionStart > 0.0, currentTimestamp - self.crossfadeTransitionStart < duration {
if case .front = self.currentPosition {
transitionFactor = max(0.0, (currentTimestamp - self.crossfadeTransitionStart) / duration)
} else {
transitionFactor = 1.0 - max(0.0, (currentTimestamp - self.crossfadeTransitionStart) / duration)
}
} else if currentTimestamp - self.lastSwitchTimestamp < 0.05 {
additional = !additional
transitionFactor = 1.0 - transitionFactor
self.needsCrossfadeTransition = true
}
transitionFactor = 0.0
if self.lastSwitchTimestamp > 0.0, currentTimestamp - self.lastSwitchTimestamp < duration {
transitionFactor = 1.0 - max(0.0, (currentTimestamp - self.lastSwitchTimestamp) / duration)
}
if let processedSampleBuffer = self.processRoundVideoSampleBuffer(sampleBuffer, additional: additional, transitionFactor: transitionFactor) {
videoRecorder.appendSampleBuffer(processedSampleBuffer)
} else {
videoRecorder.appendSampleBuffer(sampleBuffer)
}
if (transitionFactor == 1.0 && fromAdditionalOutput)
|| (transitionFactor == 0.0 && !fromAdditionalOutput)
|| (transitionFactor > 0.0 && transitionFactor < 1.0) {
if let processedSampleBuffer = self.processRoundVideoSampleBuffer(sampleBuffer, additional: fromAdditionalOutput, transitionFactor: transitionFactor) {
let presentationTime = CMSampleBufferGetPresentationTimeStamp(processedSampleBuffer)
if let lastSampleTimestamp = self.lastSampleTimestamp, lastSampleTimestamp > presentationTime {
} else {
videoRecorder.appendSampleBuffer(processedSampleBuffer)
self.lastSampleTimestamp = presentationTime
}
}
}
} else {
if type == kCMMediaType_Audio {
if self.needsSwitchSampleOffset {
self.needsSwitchSampleOffset = false
if let lastAudioSampleTime = self.lastAudioSampleTime {
let videoSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let offset = videoSampleTime - lastAudioSampleTime
if let current = self.videoSwitchSampleTimeOffset {
self.videoSwitchSampleTimeOffset = current + offset
} else {
self.videoSwitchSampleTimeOffset = offset
}
self.lastAudioSampleTime = nil
}
var additional = self.currentPosition == .front
var transitionFactor = self.currentPosition == .front ? 1.0 : 0.0
if self.lastSwitchTimestamp > 0.0 {
if self.needsCrossfadeTransition {
self.needsCrossfadeTransition = false
self.crossfadeTransitionStart = currentTimestamp + 0.03
self.needsSwitchSampleOffset = true
}
if self.crossfadeTransitionStart > 0.0, currentTimestamp - self.crossfadeTransitionStart < duration {
if case .front = self.currentPosition {
transitionFactor = max(0.0, (currentTimestamp - self.crossfadeTransitionStart) / duration)
} else {
transitionFactor = 1.0 - max(0.0, (currentTimestamp - self.crossfadeTransitionStart) / duration)
}
} else if currentTimestamp - self.lastSwitchTimestamp < 0.05 {
additional = !additional
transitionFactor = 1.0 - transitionFactor
self.needsCrossfadeTransition = true
}
self.lastAudioSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + CMSampleBufferGetDuration(sampleBuffer)
}
videoRecorder.appendSampleBuffer(sampleBuffer)
if let processedSampleBuffer = self.processRoundVideoSampleBuffer(sampleBuffer, additional: additional, transitionFactor: transitionFactor) {
videoRecorder.appendSampleBuffer(processedSampleBuffer)
} else {
videoRecorder.appendSampleBuffer(sampleBuffer)
}
}
} else {
if type == kCMMediaType_Audio {
if self.needsSwitchSampleOffset {
self.needsSwitchSampleOffset = false
if let lastAudioSampleTime = self.lastAudioSampleTime {
let videoSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let offset = videoSampleTime - lastAudioSampleTime
if let current = self.videoSwitchSampleTimeOffset {
self.videoSwitchSampleTimeOffset = current + offset
} else {
self.videoSwitchSampleTimeOffset = offset
}
self.lastAudioSampleTime = nil
}
}
self.lastAudioSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + CMSampleBufferGetDuration(sampleBuffer)
}
videoRecorder.appendSampleBuffer(sampleBuffer)
}
}

View file

@ -634,7 +634,7 @@ public final class ButtonComponent: Component {
chromeView.layer.compositingFilter = "overlayBlendMode"
chromeView.alpha = 0.8
chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.4, fillColor: .clear)
chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.36, fillColor: .clear)
}
chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: availableSize))
} else if let chromeView = self.chromeView {

View file

@ -86,6 +86,7 @@ swift_library(
"//submodules/UndoUI",
"//submodules/ContextUI",
"//submodules/AvatarNode",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/Utils/AnimatableProperty",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",

View file

@ -51,33 +51,36 @@ fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
constant uint2 &resolution[[buffer(0)]],
constant float4 &primaryParameters[[buffer(1)]],
constant float2 &primaryOffset[[buffer(2)]],
constant float3 &secondaryParameters[[buffer(3)]],
constant float2 &secondaryOffset[[buffer(4)]])
constant float3 &primaryColor[[buffer(3)]],
constant float3 &secondaryParameters[[buffer(4)]],
constant float2 &secondaryOffset[[buffer(5)]])
{
float2 R = float2(resolution.x, resolution.y);
float2 R = float2(resolution);
float2 uv;
float offset;
float axis;
if (R.x > R.y) {
uv = (2.0 * in.position.xy - R.xy) / R.y;
offset = uv.x;
uv = (2.0 * in.position.xy - R) / R.y;
axis = uv.x;
} else {
uv = (2.0 * in.position.xy - R.xy) / R.x;
offset = uv.y;
uv = (2.0 * in.position.xy - R) / R.x;
axis = uv.y;
}
float t = AARadius / resolution.y;
float cAlpha = min(1.0, 1.0 - primaryParameters.z);
float minColor = min(1.0, 1.0 + primaryParameters.z);
float bound = max(primaryParameters.x, primaryParameters.y) + 0.05;
if (abs(offset) > bound) {
cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4));
}
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
float coverage = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset,
secondaryParameters, secondaryOffset));
return half4(min(minColor, c), min(minColor, max(cAlpha, 0.231)), min(minColor, max(cAlpha, 0.188)), c);
float bound = max(primaryParameters.x, primaryParameters.y) + 0.05;
if (abs(axis) > bound) {
float extra = min(1.0, (abs(axis) - bound) * 2.4);
coverage = mix(0.0, coverage, extra);
}
float alpha = coverage;
float3 rgb = clamp(primaryColor, 0.0, 1.0) * alpha;
return half4(half3(rgb), half(alpha));
}
struct Rectangle {

View file

@ -24,6 +24,9 @@ final class CameraLiveStreamComponent: Component {
let safeInsets: UIEdgeInsets
let metrics: LayoutMetrics
let deviceMetrics: DeviceMetrics
let presentController: (ViewController, Any?) -> Void
let presentInGlobalOverlay: (ViewController, Any?) -> Void
let getController: () -> ViewController?
let didSetupMediaStream: (PresentationGroupCall) -> Void
init(
@ -37,6 +40,9 @@ final class CameraLiveStreamComponent: Component {
safeInsets: UIEdgeInsets,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
presentController: @escaping (ViewController, Any?) -> Void,
presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void,
getController: @escaping () -> ViewController?,
didSetupMediaStream: @escaping (PresentationGroupCall) -> Void
) {
self.context = context
@ -49,6 +55,9 @@ final class CameraLiveStreamComponent: Component {
self.safeInsets = safeInsets
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
self.getController = getController
self.didSetupMediaStream = didSetupMediaStream
}
@ -121,6 +130,14 @@ final class CameraLiveStreamComponent: Component {
}
return nil
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
return result
}
func update(component: CameraLiveStreamComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
@ -180,21 +197,17 @@ final class CameraLiveStreamComponent: Component {
isPanning: false,
isCentral: true,
pinchState: nil,
presentController: { c, a in
// guard let self, let environment = self.environment else {
// return
// }
// if c is UndoOverlayController || c is TooltipScreen {
// environment.controller()?.present(c, in: .current)
// } else {
// environment.controller()?.present(c, in: .window(.root), with: a)
// }
presentController: { [weak self] c, a in
guard let self, let component = self.component else {
return
}
component.presentController(c, a)
},
presentInGlobalOverlay: { c, a in
// guard let self, let environment = self.environment else {
// return
// }
// environment.controller()?.presentInGlobalOverlay(c, with: a)
presentInGlobalOverlay: { [weak self] c, a in
guard let self, let component = self.component else {
return
}
component.presentInGlobalOverlay(c, a)
},
close: {
},
@ -210,8 +223,11 @@ final class CameraLiveStreamComponent: Component {
},
addToFolder: { _ in
},
controller: {
return nil //self?.environment?.controller()
controller: { [weak self] in
guard let self, let component = self.component else {
return nil
}
return component.getController()
},
toggleAmbientMode: {
},
@ -312,6 +328,58 @@ public final class StreamAsComponent: Component {
fatalError("init(coder:) has not been implemented")
}
var scheduledAnimateIn: ComponentTransition?
func animateIn(transition: ComponentTransition) {
if self.peer == nil {
self.scheduledAnimateIn = transition
self.alpha = 0.0
return
}
self.alpha = 1.0
transition.animateAlpha(view: self.avatarNode.view, from: 0.0, to: 1.0)
transition.animateScale(view: self.avatarNode.view, from: 0.01, to: 1.0)
let offset: CGFloat = 24.0
if let titleView = self.title.view {
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
transition.animatePosition(view: titleView, from: CGPoint(x: -titleView.bounds.width / 2.0 - offset, y: self.bounds.height / 2.0 - titleView.center.y), to: .zero, additive: true)
transition.animateScale(view: titleView, from: 0.01, to: 1.0)
}
if let subtitleView = self.subtitle.view {
transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0)
transition.animatePosition(view: subtitleView, from: CGPoint(x: -subtitleView.bounds.width / 2.0 - offset, y: self.bounds.height / 2.0 - subtitleView.center.y), to: .zero, additive: true)
transition.animateScale(view: subtitleView, from: 0.01, to: 1.0)
transition.animateAlpha(view: self.arrow, from: 0.0, to: 1.0)
transition.animatePosition(view: self.arrow, from: CGPoint(x: -subtitleView.bounds.width / 2.0 - offset - 16.0, y: self.bounds.height / 2.0 - self.arrow.center.y), to: .zero, additive: true)
transition.animateScale(view: self.arrow, from: 0.01, to: 1.0)
}
}
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
transition.setAlpha(view: self.avatarNode.view, alpha: 0.0, completion: { _ in
completion()
})
transition.setScale(view: self.avatarNode.view, scale: 0.01)
let offset: CGFloat = 24.0
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: 0.0)
transition.setPosition(view: titleView, position: titleView.center.offsetBy(dx: -titleView.bounds.width / 2.0 - offset, dy: self.bounds.height / 2.0 - titleView.center.y))
transition.setScale(view: titleView, scale: 0.01)
}
if let subtitleView = self.subtitle.view {
transition.setAlpha(view: subtitleView, alpha: 0.0)
transition.setPosition(view: subtitleView, position: subtitleView.center.offsetBy(dx: -subtitleView.bounds.width / 2.0 - offset, dy: self.bounds.height / 2.0 - subtitleView.center.y))
transition.setScale(view: subtitleView, scale: 0.01)
transition.setAlpha(view: self.arrow, alpha: 0.0)
transition.setPosition(view: self.arrow, position: self.arrow.center.offsetBy(dx: -subtitleView.bounds.width / 2.0 - offset - 16.0, dy: self.bounds.height / 2.0 - self.arrow.center.y))
transition.setScale(view: self.arrow, scale: 0.01)
}
}
public func update(component: StreamAsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
@ -324,6 +392,11 @@ public final class StreamAsComponent: Component {
}
self.peer = peer
self.state?.updated()
if let scheduledAnimateIn = self.scheduledAnimateIn {
self.scheduledAnimateIn = nil
self.animateIn(transition: scheduledAnimateIn)
}
})
}

View file

@ -30,6 +30,7 @@ import TelegramVoip
import TelegramCallsUI
import GlassBarButtonComponent
import PlainButtonComponent
import StoryContainerScreen
let videoRedColor = UIColor(rgb: 0xff3b30)
let collageGrids: [Camera.CollageGrid] = [
@ -87,53 +88,58 @@ struct CameraState: Equatable {
let collageGrid: Camera.CollageGrid
let collageProgress: Float
let isStreaming: Bool
let isWaitingForStream: Bool
func updatedMode(_ mode: CameraMode) -> CameraState {
return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedPosition(_ position: Camera.Position) -> CameraState {
return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedFlashTint(_ flashTint: FlashTint) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedFlashTintSize(_ size: CGFloat) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: size, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: size, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedRecording(_ recording: Recording) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedDuration(_ duration: Double) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedIsDualCameraEnabled(_ isDualCameraEnabled: Bool) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedIsCollageEnabled(_ isCollageEnabled: Bool) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedCollageGrid(_ collageGrid: Camera.CollageGrid) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedCollageProgress(_ collageProgress: Float) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: collageProgress, isStreaming: self.isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: collageProgress, isStreaming: self.isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedIsStreaming(_ isStreaming: Bool) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: isStreaming)
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: isStreaming, isWaitingForStream: self.isWaitingForStream)
}
func updatedIsWaitingForStream(_ isWaitingForStream: Bool) -> CameraState {
return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isCollageEnabled: self.isCollageEnabled, collageGrid: self.collageGrid, collageProgress: self.collageProgress, isStreaming: self.isStreaming, isWaitingForStream: isWaitingForStream)
}
}
@ -314,6 +320,7 @@ private final class CameraScreenComponent: CombinedComponent {
private var paidMessageStars: Int64 = 0
private(set) var liveStreamStory: EngineStoryItem?
private(set) var liveStreamContent: StoryContentContext?
private weak var liveStreamCall: PresentationGroupCall?
private var liveStreamVideoCapturer: OngoingCallVideoCapturer?
private var liveStreamVideoDisposable: Disposable?
@ -371,6 +378,13 @@ private final class CameraScreenComponent: CombinedComponent {
self.sendAsPeerId = customTarget
self.isCustomTarget = true
}
let _ = (mediaEditorStoredState(engine: self.context.engine)
|> deliverOnMainQueue).start(next: { [weak self] state in
if let self, let privacy = state?.privacy {
self.privacy = privacy.privacy
}
})
}
}
@ -1082,13 +1096,23 @@ private final class CameraScreenComponent: CombinedComponent {
controller.node.pauseCameraCapture()
}
let _ = (self.context.engine.messages.beginStoryLivestream(peerId: peerId, rtmp: rtmp, privacy: self.privacy, isForwardingDisabled: self.isForwardingDisabled, messagesEnabled: self.allowComments, sendPaidMessageStars: self.paidMessageStars)
controller.updateCameraState({ $0.updatedIsWaitingForStream(true) }, transition: .spring(duration: 0.4))
let _ = (self.context.engine.messages.beginStoryLivestream(
peerId: peerId,
rtmp: rtmp,
privacy: self.privacy,
isForwardingDisabled: self.isForwardingDisabled,
messagesEnabled: self.allowComments,
sendPaidMessageStars: self.paidMessageStars
)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] story in
guard let self else {
return
}
self.liveStreamStory = story
controller?.updateCameraState({ $0.updatedIsStreaming(true) }, transition: .spring(duration: 0.4))
controller?.updateCameraState({ $0.updatedIsStreaming(true).updatedIsWaitingForStream(false) }, transition: .spring(duration: 0.4))
self.updated(transition: .immediate)
})
}
@ -1148,13 +1172,13 @@ private final class CameraScreenComponent: CombinedComponent {
return
}
let _ = self.liveStreamCall?.leave(terminateIfPossible: true).startStandalone()
controller.dismiss(animated: true)
controller.requestDismiss(animated: true)
}),
TextAlertAction(type: .genericAction, title: "Leave", action: { [weak controller] in
guard let controller else {
return
}
controller.dismiss(animated: true)
controller.requestDismiss(animated: true)
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
],
@ -1175,7 +1199,7 @@ private final class CameraScreenComponent: CombinedComponent {
return
}
let _ = self.liveStreamCall?.leave(terminateIfPossible: true).startStandalone()
controller.dismiss(animated: true)
controller.requestDismiss(animated: true)
})
]
)
@ -1184,14 +1208,18 @@ private final class CameraScreenComponent: CombinedComponent {
}
func setupLiveStreamCamera(call: PresentationGroupCall) {
guard self.liveStreamVideoCapturer == nil, let call = call as? PresentationGroupCallImpl, let controller = self.getController() else {
guard self.liveStreamVideoCapturer == nil, let call = call as? PresentationGroupCallImpl, let controller = self.getController(), let cameraState = self.cameraState else {
return
}
self.liveStreamCall = call
call.setIsMuted(action: .unmuted)
let liveStreamMediaSource = controller.node.liveStreamMediaSource
liveStreamMediaSource.setup(
isDualCameraEnabled: cameraState.isDualCameraEnabled,
dualCameraPosition: controller.node.pipPosition,
position: cameraState.position
)
let videoCapturer = OngoingCallVideoCapturer(keepLandscape: false, isCustom: true)
self.liveStreamVideoCapturer = videoCapturer
@ -1203,8 +1231,9 @@ private final class CameraScreenComponent: CombinedComponent {
videoCapturer.injectSampleBuffer(sampleBuffer, rotation: .up, completion: {})
}
}
Queue.mainQueue().after(1.0) {
Queue.mainQueue().after(0.1) {
call.requestVideo(capturer: videoCapturer, useFrontCamera: false)
call.setIsMuted(action: .unmuted)
}
}
@ -1390,7 +1419,7 @@ private final class CameraScreenComponent: CombinedComponent {
case .video:
shutterState = .video
case .live:
shutterState = .live(active: component.cameraState.isStreaming)
shutterState = .live(active: component.cameraState.isStreaming, progress: component.cameraState.isWaitingForStream)
}
}
}
@ -1527,6 +1556,24 @@ private final class CameraScreenComponent: CombinedComponent {
safeInsets: environment.safeInsets,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
presentController: { c, a in
if let controller = controller() {
if c is UndoOverlayController || c is TooltipScreen {
controller.present(c, in: .current, with: a)
} else {
controller.present(c, in: .window(.root), with: a)
}
}
},
presentInGlobalOverlay: { c, a in
if let controller = controller() {
controller.presentInGlobalOverlay(c, with: a)
}
},
getController: {
return nil
//return controller()
},
didSetupMediaStream: { [weak state] call in
state?.setupLiveStreamCamera(call: call)
}
@ -1568,8 +1615,18 @@ private final class CameraScreenComponent: CombinedComponent {
)
context.add(streamAsButton
.position(CGPoint(x: topControlSideInset + streamAsButton.size.width / 2.0 + 7.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlVerticalInset) + streamAsButton.size.height / 2.0 + 4.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.appear(ComponentTransition.Appear({ _, view, transition in
if let buttonView = view as? PlainButtonComponent.View, let view = buttonView.contentView as? StreamAsComponent.View, !transition.animation.isImmediate {
view.animateIn(transition: transition)
}
}))
.disappear(ComponentTransition.Disappear({ view, transition, completion in
if let buttonView = view as? PlainButtonComponent.View, let view = buttonView.contentView as? StreamAsComponent.View, !transition.animation.isImmediate {
view.animateOut(transition: transition, completion: completion)
} else {
completion()
}
}))
)
}
@ -2211,7 +2268,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
fileprivate var collage: CameraCollage?
private var collageStateDisposable: Disposable?
private var pipPosition: PIPPosition = .topRight
fileprivate var pipPosition: PIPPosition = .topRight
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
private let animateFlipAction = ActionSlot<Void>()
@ -2294,6 +2351,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if isDualCameraEnabled && previousPosition != currentPosition {
self.animateDualCameraPositionSwitch()
self._livestreamMediaSource?.markToggleCamera(position: currentPosition)
} else if dualCamWasEnabled != isDualCameraEnabled {
self.requestUpdateLayout(transition: .spring(duration: 0.4))
@ -2395,7 +2453,8 @@ public class CameraScreenImpl: ViewController, CameraScreen {
isCollageEnabled: false,
collageGrid: collageGrids[6],
collageProgress: 0.0,
isStreaming: false
isStreaming: false,
isWaitingForStream: false
)
self.previewFrameLeftDimView = UIView()
@ -2620,7 +2679,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
preset: .hd1920x1080,
position: self.cameraState.position,
isDualEnabled: self.cameraState.isDualCameraEnabled,
audio: true,
audio: false,
photo: true,
metadata: true
),
@ -2820,7 +2879,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
case .began:
break
case .changed:
if case .none = self.cameraState.recording {
if case .none = self.cameraState.recording, !self.cameraState.isStreaming && !self.cameraState.isWaitingForStream {
if case .compact = layout.metrics.widthClass {
switch controller.mode {
case .story:
@ -2887,7 +2946,9 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
let location = gestureRecognizer.location(in: gestureRecognizer.view)
if self.cameraState.isDualCameraEnabled && self.additionalPreviewContainerView.frame.contains(location) {
if let layout = self.validLayout, let inputHeight = layout.inputHeight, inputHeight > 10.0 {
self.view.endEditing(true)
} else if self.cameraState.isDualCameraEnabled && self.additionalPreviewContainerView.frame.contains(location) {
self.toggleCameraPositionAction.invoke(Void())
} else {
let location = gestureRecognizer.location(in: self.mainPreviewView)
@ -2921,6 +2982,8 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.pipPosition = pipPositionForLocation(layout: layout, position: location, velocity: velocity)
self.containerLayoutUpdated(layout: layout, transition: .spring(duration: 0.4))
self._livestreamMediaSource?.setDualCameraPosition(self.pipPosition)
UserDefaults.standard.set(self.pipPosition.rawValue as NSNumber, forKey: "TelegramStoryCameraDualPosition")
default:
break

View file

@ -83,28 +83,29 @@ final class CameraVideoSource: VideoSource {
}
}
private let dimensions = CGSize(width: 1080.0, height: 1920.0)
final class LiveStreamMediaSource {
private let queue = Queue()
private let pool: CVPixelBufferPool?
private(set) var mainVideoOutput: CameraVideoOutput!
private(set) var additionalVideoOutput: CameraVideoOutput!
private let composer: MediaEditorComposer
private var currentMainSampleBuffer: CMSampleBuffer?
private var currentAdditionalSampleBuffer: CMSampleBuffer?
private var additionalSampleBuffer: CMSampleBuffer?
public private(set) var currentVideoOutput: CVPixelBuffer?
private var onVideoUpdatedListeners = Bag<() -> Void>()
public private(set) var currentAudioOutput: Data?
private var onAudioUpdatedListeners = Bag<() -> Void>()
private var values: MediaEditorValues
private var cameraPosition: Camera.Position = .back
public init() {
let width: Int32 = 720
let height: Int32 = 1280
let dimensions = CGSize(width: CGFloat(width), height: CGFloat(height))
let dimensions = CGSize(width: CGFloat(1080), height: CGFloat(1920))
let bufferOptions: [String: Any] = [
kCVPixelBufferPoolMinimumBufferCountKey as String: 3 as NSNumber
@ -120,52 +121,51 @@ final class LiveStreamMediaSource {
CVPixelBufferPoolCreate(nil, bufferOptions as CFDictionary, pixelBufferOptions as CFDictionary, &pool)
self.pool = pool
let topOffset = CGPoint(x: 267.0, y: 438.0)
let additionalVideoPosition = CGPoint(x: dimensions.width - topOffset.x, y: topOffset.y)
self.values = MediaEditorValues(
peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)),
originalDimensions: PixelDimensions(dimensions),
cropOffset: .zero,
cropRect: CGRect(origin: .zero, size: dimensions),
cropScale: 1.0,
cropRotation: 0.0,
cropMirroring: false,
cropOrientation: nil,
gradientColors: nil,
videoTrimRange: nil,
videoIsMuted: false,
videoIsFullHd: false,
videoIsMirrored: false,
videoVolume: nil,
additionalVideoPath: nil,
additionalVideoIsDual: true,
additionalVideoPosition: nil,
additionalVideoScale: 1.625,
additionalVideoRotation: 0.0,
additionalVideoPositionChanges: [],
additionalVideoTrimRange: nil,
additionalVideoOffset: nil,
additionalVideoVolume: nil,
collage: [],
nightTheme: false,
drawing: nil,
maskDrawing: nil,
entities: [],
toolValues: [:],
audioTrack: nil,
audioTrackTrimRange: nil,
audioTrackOffset: nil,
audioTrackVolume: nil,
audioTrackSamples: nil,
collageTrackSamples: nil,
coverImageTimestamp: nil,
coverDimensions: nil,
qualityPreset: nil
)
self.composer = MediaEditorComposer(
postbox: nil,
values: MediaEditorValues(
peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)),
originalDimensions: PixelDimensions(dimensions),
cropOffset: .zero,
cropRect: CGRect(origin: .zero, size: dimensions),
cropScale: 1.0,
cropRotation: 0.0,
cropMirroring: false,
cropOrientation: nil,
gradientColors: nil,
videoTrimRange: nil,
videoIsMuted: false,
videoIsFullHd: false,
videoIsMirrored: false,
videoVolume: nil,
additionalVideoPath: nil,
additionalVideoIsDual: true,
additionalVideoPosition: additionalVideoPosition,
additionalVideoScale: 1.625,
additionalVideoRotation: 0.0,
additionalVideoPositionChanges: [],
additionalVideoTrimRange: nil,
additionalVideoOffset: nil,
additionalVideoVolume: nil,
collage: [],
nightTheme: false,
drawing: nil,
maskDrawing: nil,
entities: [],
toolValues: [:],
audioTrack: nil,
audioTrackTrimRange: nil,
audioTrackOffset: nil,
audioTrackVolume: nil,
audioTrackSamples: nil,
collageTrackSamples: nil,
coverImageTimestamp: nil,
coverDimensions: nil,
qualityPreset: nil
),
dimensions: CGSize(width: 1080.0, height: 1920.0),
values: self.values,
dimensions: dimensions,
outputDimensions: CGSize(width: 720.0, height: 1280.0),
textScale: 1.0,
videoDuration: nil,
@ -177,19 +177,77 @@ final class LiveStreamMediaSource {
guard let self else {
return
}
self.currentMainSampleBuffer = try? CMSampleBuffer(copying: buffer)
self.push(mainSampleBuffer: buffer, additionalSampleBuffer: self.currentAdditionalSampleBuffer)
self.queue.async {
self.push(mainSampleBuffer: buffer)
}
})
self.additionalVideoOutput = CameraVideoOutput(sink: { [weak self] buffer, mirror in
guard let self else {
return
}
self.currentAdditionalSampleBuffer = try? CMSampleBuffer(copying: buffer)
self.queue.async {
self.additionalSampleBuffer = buffer
}
})
}
public func addOnVideoUpdated(_ f: @escaping () -> Void) -> Disposable {
func setup(isDualCameraEnabled: Bool, dualCameraPosition: CameraScreenImpl.PIPPosition, position: Camera.Position) {
var additionalVideoPositionChanges: [VideoPositionChange] = []
if isDualCameraEnabled && position == .front {
additionalVideoPositionChanges.append(VideoPositionChange(additional: true, timestamp: CACurrentMediaTime()))
}
var values = self.values
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
values = values.withUpdatedAdditionalVideo(position: self.getAdditionalVideoPosition(dualCameraPosition), scale: 1.625, rotation: 0.0)
self.values = values
self.cameraPosition = position
self.composer.values = values
}
func markToggleCamera(position: Camera.Position) {
let timestamp = self.additionalSampleBuffer?.presentationTimeStamp.seconds ?? CACurrentMediaTime()
var values = self.values
var additionalVideoPositionChanges = values.additionalVideoPositionChanges
additionalVideoPositionChanges.append(VideoPositionChange(additional: position == .front, timestamp: timestamp))
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
self.values = values
self.cameraPosition = position
self.composer.values = self.values
}
func setDualCameraPosition(_ pipPosition: CameraScreenImpl.PIPPosition) {
let timestamp = self.additionalSampleBuffer?.presentationTimeStamp.seconds ?? CACurrentMediaTime()
var values = self.values
var additionalVideoPositionChanges = values.additionalVideoPositionChanges
additionalVideoPositionChanges.append(VideoPositionChange(additional: self.cameraPosition == .front, translationFrom: values.additionalVideoPosition ?? .zero, timestamp: timestamp))
values = values.withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: additionalVideoPositionChanges)
values = values.withUpdatedAdditionalVideo(position: self.getAdditionalVideoPosition(pipPosition), scale: 1.625, rotation: 0.0)
self.values = values
self.composer.values = values
}
func getAdditionalVideoPosition(_ pipPosition: CameraScreenImpl.PIPPosition) -> CGPoint {
let topOffset = CGPoint(x: 267.0, y: 438.0)
let bottomOffset = CGPoint(x: 267.0, y: 438.0)
let position: CGPoint
switch pipPosition {
case .topLeft:
position = CGPoint(x: topOffset.x, y: topOffset.y)
case .topRight:
position = CGPoint(x: dimensions.width - topOffset.x, y: topOffset.y)
case .bottomLeft:
position = CGPoint(x: bottomOffset.x, y: dimensions.height - bottomOffset.y)
case .bottomRight:
position = CGPoint(x: dimensions.width - bottomOffset.x, y: dimensions.height - bottomOffset.y)
}
return position
}
func addOnVideoUpdated(_ f: @escaping () -> Void) -> Disposable {
let index = self.onVideoUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
@ -202,19 +260,7 @@ final class LiveStreamMediaSource {
}
}
public func addOnAudioUpdated(_ f: @escaping () -> Void) -> Disposable {
let index = self.onAudioUpdatedListeners.add(f)
return ActionDisposable { [weak self] in
DispatchQueue.main.async {
guard let self else {
return
}
self.onAudioUpdatedListeners.remove(index)
}
}
}
private func push(mainSampleBuffer: CMSampleBuffer, additionalSampleBuffer: CMSampleBuffer?) {
private func push(mainSampleBuffer: CMSampleBuffer) {
let timestamp = mainSampleBuffer.presentationTimeStamp
guard let mainPixelBuffer = CMSampleBufferGetImageBuffer(mainSampleBuffer) else {
@ -222,7 +268,7 @@ final class LiveStreamMediaSource {
}
let main: MediaEditorComposer.Input = .videoBuffer(VideoPixelBuffer(pixelBuffer: mainPixelBuffer, rotation: .rotate90Degrees, timestamp: timestamp), nil, 1.0, .zero)
var additional: [MediaEditorComposer.Input?] = []
if let additionalPixelBuffer = additionalSampleBuffer.flatMap({ CMSampleBufferGetImageBuffer($0) }) {
if let additionalPixelBuffer = self.additionalSampleBuffer.flatMap({ CMSampleBufferGetImageBuffer($0) }) {
additional.append(
.videoBuffer(VideoPixelBuffer(pixelBuffer: additionalPixelBuffer, rotation: .rotate90DegreesMirrored, timestamp: timestamp), nil, 1.0, .zero)
)
@ -236,7 +282,7 @@ final class LiveStreamMediaSource {
guard let self else {
return
}
Queue.mainQueue().async {
self.queue.async {
self.currentVideoOutput = pixelBuffer
for onUpdated in self.onVideoUpdatedListeners.copyItems() {
onUpdated()

View file

@ -12,6 +12,7 @@ import AccountContext
import GlassBackgroundComponent
import GlassBarButtonComponent
import BundleIconComponent
import ActivityIndicator
enum ShutterButtonState: Equatable {
case disabled
@ -20,7 +21,7 @@ enum ShutterButtonState: Equatable {
case stopRecording
case holdRecording(progress: Float)
case transition
case live(active: Bool)
case live(active: Bool, progress: Bool)
}
private let maximumShutterSize = CGSize(width: 96.0, height: 96.0)
@ -109,6 +110,7 @@ private final class ShutterButtonContentComponent: Component {
private let chromeView = UIImageView()
private let label = ComponentView<Empty>()
private var activityIndicator: ActivityIndicator?
private let checkLayer = SimpleLayer()
private let checkLayerMask = SimpleShapeLayer()
@ -274,6 +276,7 @@ private final class ShutterButtonContentComponent: Component {
var chromeAlpha: CGFloat = 0.0
var chromeSize = CGSize(width: 60.0, height: 60.0)
var labelAlpha: CGFloat = 0.0
var hasProgress = false
switch component.shutterState {
case .generic, .disabled:
innerColor = component.tintColor
@ -297,14 +300,15 @@ private final class ShutterButtonContentComponent: Component {
innerSize = CGSize(width: 60.0, height: 60.0)
ringSize = CGSize(width: 68.0, height: 68.0)
recordingProgress = 0.0
case .live:
case let .live(_, progress):
innerColor = UIColor(rgb: 0xff375f)
innerSize = CGSize(width: 52.0, height: 52.0)
ringSize = CGSize(width: 60.0, height: 60.0)
glassAlpha = 0.0
chromeAlpha = 0.65
labelAlpha = 1.0
labelAlpha = progress ? 0.0 : 1.0
chromeSize = CGSize(width: 326.0, height: 53.0 - UIScreenPixel)
hasProgress = progress
}
if component.collageProgress > 1.0 - .ulpOfOne {
@ -330,6 +334,30 @@ private final class ShutterButtonContentComponent: Component {
transition.setAlpha(view: labelView, alpha: labelAlpha)
}
if hasProgress {
let activityIndicator: ActivityIndicator
var activityIndicatorTransition = transition
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicatorTransition = .immediate
activityIndicator = ActivityIndicator(type: .custom(.white, 22.0, 2.0, true))
activityIndicator.view.alpha = 0.0
self.activityIndicator = activityIndicator
self.addSubview(activityIndicator.view)
}
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
let indicatorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((maximumShutterSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((maximumShutterSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: indicatorFrame)
} else if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil
transition.setAlpha(view: activityIndicator.view, alpha: 0.0, completion: { [weak activityIndicator] _ in
activityIndicator?.view.removeFromSuperview()
})
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((maximumShutterSize.width - chromeSize.width) / 2.0), y: floorToScreenPixels((maximumShutterSize.height - chromeSize.height) / 2.0)), size: chromeSize)
transition.setFrame(view: self.chromeView, frame: buttonFrame)
@ -1182,7 +1210,7 @@ final class CaptureControlsComponent: Component {
isHolding = true
} else if case .transition = component.shutterState {
isTransitioning = true
} else if case let .live(active) = component.shutterState {
} else if case let .live(active, _) = component.shutterState {
isLiveStream = true
isLiveActive = active
}

View file

@ -49,19 +49,27 @@ final class ShutterBlobView: UIView {
}
}
func primaryRedness(tintColor: UIColor) -> CGFloat {
func primaryColor(tintColor: UIColor) -> CGRect {
var color: UIColor
switch self {
case .generic:
if tintColor.rgb == 0x000000 {
return -1.0
color = UIColor(rgb: 0x000000)
} else {
return 0.0
color = UIColor(rgb: 0xffffff)
}
case .live:
return 0.7
color = UIColor(rgb: 0xfa325a)
default:
return 1.0
color = UIColor(rgb: 0xff0b18)
}
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
return CGRect(x: r, y: g, width: b, height: 1.0)
}
return CGRect(x: 0, y: 0, width: 0, height: 1.0)
}
var primaryCornerRadius: CGFloat {
@ -105,7 +113,7 @@ final class ShutterBlobView: UIView {
private var primaryHeight = AnimatableProperty<CGFloat>(value: 0.63)
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryColor = AnimatableProperty<CGRect>(value: CGRect(x: 1.0, y: 1.0, width: 1.0, height: 1.0))
private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.34)
@ -145,13 +153,22 @@ final class ShutterBlobView: UIView {
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
// pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
// pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
// pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
// pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
// pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
@ -184,7 +201,7 @@ final class ShutterBlobView: UIView {
self.primaryWidth.update(value: state.primarySize.width, transition: transition)
self.primaryHeight.update(value: state.primarySize.height, transition: transition)
self.primaryRedness.update(value: state.primaryRedness(tintColor: tintColor), transition: transition)
self.primaryColor.update(value: state.primaryColor(tintColor: tintColor), transition: transition)
self.primaryCornerRadius.update(value: state.primaryCornerRadius, transition: transition)
self.secondarySize.update(value: state.secondarySize, transition: transition)
self.secondaryRedness.update(value: state.secondaryRedness, transition: transition)
@ -238,7 +255,6 @@ final class ShutterBlobView: UIView {
self.primaryHeight,
self.primaryOffsetX,
self.primaryOffsetY,
self.primaryRedness,
self.primaryCornerRadius,
self.secondarySize,
self.secondaryOffsetX,
@ -253,6 +269,9 @@ final class ShutterBlobView: UIView {
hasAnimations = true
}
}
if self.primaryColor.tick(timestamp: timestamp) {
hasAnimations = true
}
self.displayLink?.isPaused = !hasAnimations
}
@ -316,7 +335,7 @@ final class ShutterBlobView: UIView {
var primaryParameters = simd_float4(
Float(self.primaryWidth.presentationValue),
Float(self.primaryHeight.presentationValue),
Float(self.primaryRedness.presentationValue),
Float(0.0),
Float(self.primaryCornerRadius.presentationValue)
)
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float3>.size, index: 1)
@ -327,17 +346,20 @@ final class ShutterBlobView: UIView {
)
renderEncoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 2)
var primaryColor = simd_float3(Float(self.primaryColor.presentationValue.minX), Float(self.primaryColor.presentationValue.minY), Float(self.primaryColor.presentationValue.width))
renderEncoder.setFragmentBytes(&primaryColor, length: MemoryLayout<simd_float3>.stride, index: 3)
var secondaryParameters = simd_float2(
Float(self.secondarySize.presentationValue),
Float(self.secondaryRedness.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float4>.size, index: 3)
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float4>.size, index: 4)
var secondaryOffset = simd_float2(
Float(self.secondaryOffsetX.presentationValue),
Float(self.secondaryOffsetY.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 4)
renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 5)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()

View file

@ -57,6 +57,7 @@ public final class ChatTextInputPanelComponent: Component {
public final class LeftAction: Equatable {
public enum Kind: Equatable {
case empty
case attach
case toggleExpanded(isVisible: Bool, isExpanded: Bool, hasUnseen: Bool)
}
@ -79,6 +80,7 @@ public final class ChatTextInputPanelComponent: Component {
public final class RightAction: Equatable {
public enum Kind: Equatable {
case empty
case stars(count: Int, isFilled: Bool)
}
@ -839,6 +841,8 @@ public final class ChatTextInputPanelComponent: Component {
if let leftAction = component.leftAction {
switch leftAction.kind {
case .empty:
panelNode.customLeftAction = .empty
case .attach:
panelNode.customLeftAction = nil
case let .toggleExpanded(isVisible, isExpanded, hasUnseen):
@ -854,6 +858,8 @@ public final class ChatTextInputPanelComponent: Component {
if let rightAction = component.rightAction {
switch rightAction.kind {
case .empty:
panelNode.customRightAction = .empty
case let .stars(count, isFilled):
panelNode.customRightAction = .stars(count: count, isFilled: isFilled, action: { sourceView in
rightAction.action(sourceView)

View file

@ -386,10 +386,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
public enum LeftAction {
case empty
case toggleExpanded(isVisible: Bool, isExpanded: Bool, hasUnseen: Bool)
}
public enum RightAction {
case empty
case stars(count: Int, isFilled: Bool, action: (UIView) -> Void, longPressAction: ((UIView) -> Void)?)
}
@ -1751,7 +1753,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let customLeftAction = self.customLeftAction {
switch customLeftAction {
case .toggleExpanded:
case .empty, .toggleExpanded:
self.attachmentButtonIcon.image = nil
self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
}
@ -1789,10 +1791,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
if wasEditingMedia != isEditingMedia || hadMediaDraft != hasMediaDraft || isFirstTime {
if let customLeftAction = self.customLeftAction {
switch customLeftAction {
case .toggleExpanded:
case .empty, .toggleExpanded:
self.attachmentButtonIcon.image = nil
self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
}
@ -1945,6 +1946,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let customLeftAction = self.customLeftAction {
switch customLeftAction {
case .empty:
break
case let .toggleExpanded(_, isExpanded, hasUnseen):
let commentsButtonIcon: RasterizedCompositionMonochromeLayer
if let current = self.commentsButtonIcon {
@ -2260,7 +2263,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
} else {
if let starReactionButtonSize {
if let customRightAction = self.customRightAction, case .empty = customRightAction {
textFieldInsets.right = 8.0
} else if let starReactionButtonSize {
textFieldInsets.right = 14.0 + starReactionButtonSize.width
} else {
textFieldInsets.right = 54.0
@ -2269,8 +2274,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if mediaRecordingState != nil {
textFieldInsets.left = 8.0
}
if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _, _) = customLeftAction, !isVisible {
textFieldInsets.left = 8.0
if let customLeftAction = self.customLeftAction {
switch customLeftAction {
case .empty, .toggleExpanded(false, _, _):
textFieldInsets.left = 8.0
default:
break
}
}
var audioRecordingItemsAlpha: CGFloat = 1.0

View file

@ -4,25 +4,25 @@
using namespace metal;
static inline float sRGBnonLinearNormToLinear(float normV) {
if (normV <= 0.04045f) {
normV *= (1.0f / 12.92f);
} else {
const float a = 0.055f;
const float gamma = 2.4f;
normV = (normV + a) * (1.0f / (1.0f + a));
normV = pow(normV, gamma);
}
return normV;
}
//static inline float sRGBnonLinearNormToLinear(float normV) {
// if (normV <= 0.04045f) {
// normV *= (1.0f / 12.92f);
// } else {
// const float a = 0.055f;
// const float gamma = 2.4f;
// normV = (normV + a) * (1.0f / (1.0f + a));
// normV = pow(normV, gamma);
// }
// return normV;
//}
static inline float4 sRGBGammaDecode(const float4 rgba) {
float4 result = rgba;
result.r = sRGBnonLinearNormToLinear(rgba.r);
result.g = sRGBnonLinearNormToLinear(rgba.g);
result.b = sRGBnonLinearNormToLinear(rgba.b);
return rgba;
}
//static inline float4 sRGBGammaDecode(const float4 rgba) {
// float4 result = rgba;
// result.r = sRGBnonLinearNormToLinear(rgba.r);
// result.g = sRGBnonLinearNormToLinear(rgba.g);
// result.b = sRGBnonLinearNormToLinear(rgba.b);
// return result;
//}
static inline float4 BT709Decode(const float Y, const float Cb, const float Cr) {
float Yn = Y;
@ -57,6 +57,6 @@ fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]],
float Cr = float(uvSamples[1]);
float4 pixel = BT709Decode(Y, Cb, Cr);
pixel = sRGBGammaDecode(pixel);
//pixel = sRGBGammaDecode(pixel);
return pixel;
}

View file

@ -92,7 +92,12 @@ public final class MediaEditorComposer {
let ciContext: CIContext?
private var textureCache: CVMetalTextureCache?
private let values: MediaEditorValues
public var values: MediaEditorValues {
didSet {
self.renderChain.update(values: self.values)
self.renderer.videoFinishPass.update(values: self.values, videoDuration: nil, additionalVideoDuration: nil)
}
}
private let dimensions: CGSize
private let outputDimensions: CGSize
private let textScale: CGFloat

View file

@ -21,19 +21,6 @@ public final class VideoPixelBuffer {
}
}
final class RenderingContext {
let device: MTLDevice
let commandBuffer: MTLCommandBuffer
init(
device: MTLDevice,
commandBuffer: MTLCommandBuffer
) {
self.device = device
self.commandBuffer = commandBuffer
}
}
protocol RenderPass: AnyObject {
func setup(device: MTLDevice, library: MTLLibrary)
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture?

View file

@ -3,7 +3,6 @@ import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import MediaEditor
public final class MediaEditorStoredTextSettings: Codable {
private enum CodingKeys: String, CodingKey {
@ -111,7 +110,7 @@ public final class MediaEditorStoredState: Codable {
}
}
func mediaEditorStoredState(engine: TelegramEngine) -> Signal<MediaEditorStoredState?, NoError> {
public func mediaEditorStoredState(engine: TelegramEngine) -> Signal<MediaEditorStoredState?, NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: 0)
@ -121,7 +120,7 @@ func mediaEditorStoredState(engine: TelegramEngine) -> Signal<MediaEditorStoredS
}
}
func updateMediaEditorStoredStateInteractively(engine: TelegramEngine, _ f: @escaping (MediaEditorStoredState?) -> MediaEditorStoredState?) -> Signal<Never, NoError> {
public func updateMediaEditorStoredStateInteractively(engine: TelegramEngine, _ f: @escaping (MediaEditorStoredState?) -> MediaEditorStoredState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: 0)

View file

@ -41,17 +41,21 @@ public enum EditorToolKey: Int32, CaseIterable {
public struct VideoPositionChange: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case additional
case translationFrom
case timestamp
}
public let additional: Bool
public let translationFrom: CGPoint?
public let timestamp: Double
public init(
additional: Bool,
translationFrom: CGPoint? = nil,
timestamp: Double
) {
self.additional = additional
self.translationFrom = translationFrom
self.timestamp = timestamp
}
}
@ -888,10 +892,14 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}
func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues {
public func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}
public func withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: [VideoPositionChange]) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}
func withUpdatedAdditionalVideoTrimRange(_ additionalVideoTrimRange: Range<Double>?) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}

View file

@ -506,6 +506,10 @@ final class VideoFinishPass: RenderPass {
let mirroring: Bool
let baseScale: CGFloat
func with(position: CGPoint) -> VideoPosition {
return VideoPosition(position: position, size: self.size, scale: self.scale, rotation: self.rotation, mirroring: self.mirroring, baseScale: baseScale)
}
func with(size: CGSize, baseScale: CGFloat) -> VideoPosition {
return VideoPosition(position: self.position, size: size, scale: self.scale, rotation: self.rotation, mirroring: self.mirroring, baseScale: baseScale)
}
@ -568,6 +572,9 @@ final class VideoFinishPass: RenderPass {
if let additionalInput {
var previousChange: VideoPositionChange?
for change in self.videoPositionChanges {
if let _ = change.translationFrom {
continue
}
if timestamp >= change.timestamp {
previousChange = change
}
@ -595,6 +602,29 @@ final class VideoFinishPass: RenderPass {
}
}
var translationTransitionFraction = 1.0
var previousAdditionalPosition = additionalPosition
if let _ = additionalInput {
var previousChange: VideoPositionChange?
for change in self.videoPositionChanges {
guard let _ = change.translationFrom else {
continue
}
if timestamp >= change.timestamp {
previousChange = change
}
if timestamp < change.timestamp {
break
}
}
if let previousChange, let translationFrom = previousChange.translationFrom {
previousAdditionalPosition = previousAdditionalPosition.with(position: translationFrom)
if previousChange.timestamp > 0.0 && timestamp < previousChange.timestamp + transitionDuration {
translationTransitionFraction = (timestamp - previousChange.timestamp) / transitionDuration
}
}
}
var backgroundVideoState = VideoState(texture: backgroundTexture, textureRotation: backgroundTextureRotation, position: mainPosition, roundness: 0.0, alpha: 1.0)
var foregroundVideoState: VideoState?
var disappearingVideoState: VideoState?
@ -615,6 +645,10 @@ final class VideoFinishPass: RenderPass {
foregroundAlpha = min(1.0, max(0.0, Float(transitionFraction) * 2.5))
}
if translationTransitionFraction < 1.0 {
let springFraction = lookupSpringValue(translationTransitionFraction)
foregroundPosition = previousAdditionalPosition.mixed(with: foregroundPosition, fraction: springFraction)
}
var isVisible = true
var trimRangeLowerBound: Double?

View file

@ -746,38 +746,38 @@ final class LiveStreamSettingsScreenComponent: Component {
action: nil
))))
var pinTitle = "Post to My Profile"
var pinInfo = "Keep this live on your profile."
if screenState.sendAsPeerId?.namespace == Namespaces.Peer.CloudChannel {
pinTitle = "Post to Channel Profile"
pinInfo = "Keep this live on channel profile."
}
settingsSectionItems.append(AnyComponentWithIdentity(id: "pin", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: pinTitle,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: screenState.pin, action: { [weak self] _ in
guard let self else {
return
}
component.stateContext.pin = !component.stateContext.pin
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
let settingsSectionFooterComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: pinInfo, font: footerTextFont, textColor: footerTextColor)),
maximumNumberOfLines: 0
))
// var pinTitle = "Post to My Profile"
// var pinInfo = "Keep this live on your profile."
// if screenState.sendAsPeerId?.namespace == Namespaces.Peer.CloudChannel {
// pinTitle = "Post to Channel Profile"
// pinInfo = "Keep this live on channel profile."
// }
//
// settingsSectionItems.append(AnyComponentWithIdentity(id: "pin", component: AnyComponent(ListActionItemComponent(
// theme: theme,
// style: .glass,
// title: AnyComponent(MultilineTextComponent(
// text: .plain(NSAttributedString(
// string: pinTitle,
// font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
// textColor: theme.list.itemPrimaryTextColor
// )),
// maximumNumberOfLines: 1
// )),
// accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: screenState.pin, action: { [weak self] _ in
// guard let self else {
// return
// }
// component.stateContext.pin = !component.stateContext.pin
// self.state?.updated(transition: .spring(duration: 0.4))
// })),
// action: nil
// ))))
//
// let settingsSectionFooterComponent = AnyComponent(MultilineTextComponent(
// text: .plain(NSAttributedString(string: pinInfo, font: footerTextFont, textColor: footerTextColor)),
// maximumNumberOfLines: 0
// ))
let settingsSectionSize = self.settingsSection.update(
transition: transition,
@ -785,7 +785,7 @@ final class LiveStreamSettingsScreenComponent: Component {
theme: theme,
style: .glass,
header: nil,
footer: settingsSectionFooterComponent,
footer: nil,
items: settingsSectionItems
)),
environment: {},

View file

@ -35,6 +35,7 @@ final class StoryContentLiveChatComponent: Component {
let call: PresentationGroupCall
let storyPeerId: EnginePeer.Id
let insets: UIEdgeInsets
let isEmbeddedInCamera: Bool
let controller: () -> ViewController?
init(
@ -45,6 +46,7 @@ final class StoryContentLiveChatComponent: Component {
call: PresentationGroupCall,
storyPeerId: EnginePeer.Id,
insets: UIEdgeInsets,
isEmbeddedInCamera: Bool,
controller: @escaping () -> ViewController?
) {
self.external = external
@ -54,6 +56,7 @@ final class StoryContentLiveChatComponent: Component {
self.call = call
self.storyPeerId = storyPeerId
self.insets = insets
self.isEmbeddedInCamera = isEmbeddedInCamera
self.controller = controller
}
@ -79,6 +82,9 @@ final class StoryContentLiveChatComponent: Component {
if lhs.insets != rhs.insets {
return false
}
if lhs.isEmbeddedInCamera != rhs.isEmbeddedInCamera {
return false
}
return true
}
@ -255,6 +261,10 @@ final class StoryContentLiveChatComponent: Component {
}
}
if let component = self.component, component.isEmbeddedInCamera && result === self.listContainer {
return nil
}
return result
}

View file

@ -446,7 +446,7 @@ final class StoryItemContentComponent: Component {
isChannel: false,
invite: nil,
joinAsPeerId: nil,
isStream: true,
isStream: !(component.isEmbeddedInCamera && liveStream.kind == .rtc),
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
@ -943,6 +943,7 @@ final class StoryItemContentComponent: Component {
call: mediaStreamCall,
storyPeerId: component.peer.id,
insets: environment.containerInsets,
isEmbeddedInCamera: component.isEmbeddedInCamera,
controller: { [weak self] in
guard let self, let component = self.component else {
return nil

View file

@ -1342,6 +1342,14 @@ public final class StoryItemSetContainerComponent: Component {
return self.itemsContainerView
}
if let component = self.component, component.isEmbeddedInCamera {
for (_, visibleItem) in self.visibleItems {
if result === visibleItem.contentContainerView {
return nil
}
}
}
return result
}
@ -4285,30 +4293,35 @@ public final class StoryItemSetContainerComponent: Component {
let centerInfoItemSize = currentCenterInfoItem.view.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(content: currentCenterInfoItem.component, effectAlignment: .center, action: { [weak self] in
guard let self, let component = self.component else {
return
}
if let forwardInfo = component.slice.item.storyItem.forwardInfo, case let .known(peer, _, _) = forwardInfo {
if peer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: peer, chat: false)
component: AnyComponent(PlainButtonComponent(
content: currentCenterInfoItem.component,
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
} else if let author = component.slice.item.storyItem.author {
if author.id == component.context.account.peerId {
self.navigateToMyStories()
if let forwardInfo = component.slice.item.storyItem.forwardInfo, case let .known(peer, _, _) = forwardInfo {
if peer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: peer, chat: false)
}
} else if let author = component.slice.item.storyItem.author {
if author.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: author, chat: false)
}
} else {
self.navigateToPeer(peer: author, chat: false)
if component.slice.effectivePeer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.effectivePeer, chat: false)
}
}
} else {
if component.slice.effectivePeer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.effectivePeer, chat: false)
}
}
})),
},
isEnabled: !component.isEmbeddedInCamera
)),
environment: {},
containerSize: CGSize(width: headerRightOffset - 34.0, height: 44.0)
)
@ -4333,16 +4346,21 @@ public final class StoryItemSetContainerComponent: Component {
let leftInfoItemSize = currentLeftInfoItem.view.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(content: currentLeftInfoItem.component, effectAlignment: .center, action: { [weak self] in
guard let self, let component = self.component else {
return
}
if component.slice.effectivePeer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.effectivePeer, chat: false)
}
})),
component: AnyComponent(PlainButtonComponent(
content: currentLeftInfoItem.component,
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if component.slice.effectivePeer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.effectivePeer, chat: false)
}
},
isEnabled: !component.isEmbeddedInCamera
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)