Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 4 additions & 59 deletions App/ttaccessible/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var userAccountsWindowController: NSWindowController?
private var bannedUsersWindowController: NSWindowController?
private var userInfoWindowController: UserInfoWindowController?
private var mediaStreamingPlayerWindowController: MediaStreamingPlayerWindowController?
private weak var mediaStreamingPlayerViewController: MediaStreamingPlayerViewController?
private weak var savedServersViewController: SavedServersViewController?
private weak var connectedServerViewController: ConnectedServerViewController?
private weak var privateMessagesViewController: PrivateMessagesViewController?
Expand Down Expand Up @@ -1259,7 +1257,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
panel.title = L10n.text("mediaStream.panel.title")
panel.message = L10n.text("mediaStream.panel.message")
panel.prompt = L10n.text("mediaStream.panel.choose")
panel.allowedContentTypes = [.audio, .mp3, .mpeg4Audio, .wav, .aiff]
panel.allowedContentTypes = [.audio, .mp3, .mpeg4Audio, .wav, .aiff, .movie, .mpeg4Movie, .video, .avi, .quickTimeMovie]
guard let parentWindow = NSApp.keyWindow ?? NSApp.mainWindow ?? NSApp.windows.first else { return }
panel.beginSheetModal(for: parentWindow) { [weak self] response in
guard response == .OK, let url = panel.url, let self else { return }
Expand Down Expand Up @@ -2095,64 +2093,11 @@ extension AppDelegate: TeamTalkConnectionControllerDelegate {

func teamTalkConnectionController(_ controller: TeamTalkConnectionController, didUpdateMediaStreamingProgress progress: MediaStreamingProgress) {
menuState.setMediaStreamingActive(progress.isActive)
if progress.isActive {
showMediaStreamingPlayerWindow(with: progress)
} else {
closeMediaStreamingPlayerWindow()
}
}
}

extension AppDelegate: MediaStreamingPlayerActions {
func mediaStreamingPlayerDidTogglePlayPause() {
connectionController.toggleMediaStreamingPaused()
}

func mediaStreamingPlayerDidStop() {
connectionController.stopStreamingMediaFile()
}

func mediaStreamingPlayerDidSeek(toMSec offsetMSec: UInt32) {
connectionController.seekMediaStreaming(toMSec: offsetMSec)
}

func mediaStreamingPlayerDidChangeBroadcastGainPercent(_ percent: Int) {
connectionController.setMediaStreamingBroadcastGainPercent(percent)
}
}

private extension AppDelegate {
func showMediaStreamingPlayerWindow(with progress: MediaStreamingProgress) {
let viewController: MediaStreamingPlayerViewController
if let existing = mediaStreamingPlayerViewController {
viewController = existing
} else {
let newViewController = MediaStreamingPlayerViewController()
newViewController.actions = self
mediaStreamingPlayerViewController = newViewController
viewController = newViewController
}

if mediaStreamingPlayerWindowController == nil {
let controller = MediaStreamingPlayerWindowController(contentViewController: viewController)
controller.onCloseRequested = { [weak self] in
self?.connectionController.stopStreamingMediaFile()
}
mediaStreamingPlayerWindowController = controller
}

viewController.update(with: progress)

if let window = mediaStreamingPlayerWindowController?.window, !window.isVisible {
mediaStreamingPlayerWindowController?.showWindow(nil)
window.makeKeyAndOrderFront(nil)
}
connectedServerViewController?.applyMediaStreamingProgress(progress)
}

func closeMediaStreamingPlayerWindow() {
mediaStreamingPlayerWindowController?.close()
mediaStreamingPlayerWindowController = nil
mediaStreamingPlayerViewController = nil
func teamTalkConnectionController(_ controller: TeamTalkConnectionController, didUpdateVideoDisplay state: VideoDisplayState) {
connectedServerViewController?.applyVideoDisplay(state)
}
}

Expand Down
3 changes: 2 additions & 1 deletion App/ttaccessible/AppKit/ChannelFilesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ final class ChannelFilesViewController: NSViewController {
outputGainDB: session.outputGainDB,
recordingActive: session.recordingActive,
mediaStreamingActive: session.mediaStreamingActive,
mediaStreamingFileName: session.mediaStreamingFileName
mediaStreamingFileName: session.mediaStreamingFileName,
mediaStreamingHasVideo: session.mediaStreamingHasVideo
)
announceNewTransfers(transfers)
if oldUploads != activeUploadTransfers {
Expand Down
105 changes: 105 additions & 0 deletions App/ttaccessible/AppKit/CollapsibleVideoPanelView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// CollapsibleVideoPanelView.swift
// ttaccessible
//

import AppKit

@MainActor
protocol CollapsibleVideoPanelViewDelegate: AnyObject {
func collapsibleVideoPanelViewDidToggleExpanded(_ view: CollapsibleVideoPanelView, expanded: Bool)
}

final class CollapsibleVideoPanelView: NSView {
weak var delegate: CollapsibleVideoPanelViewDelegate?

private(set) var isExpanded = true
private let toggleButton = NSButton()
private let videoFrameView = VideoFrameView()
private var expandedHeightConstraint: NSLayoutConstraint?
private var collapsedHeightConstraint: NSLayoutConstraint?

var videoView: VideoFrameView { videoFrameView }

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
configureUI()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
nil
}

func setExpanded(_ expanded: Bool, notifyDelegate: Bool) {
guard isExpanded != expanded else { return }
isExpanded = expanded
applyExpandedState()
if notifyDelegate {
delegate?.collapsibleVideoPanelViewDidToggleExpanded(self, expanded: expanded)
}
}

func updateVideoState(_ state: VideoDisplayState) {
if state.userID == 0 || state.frame == nil {
videoFrameView.update(frame: nil)
videoFrameView.setAccessibilitySourceLabel("")
} else {
videoFrameView.update(frame: state.frame)
videoFrameView.setAccessibilitySourceLabel(
L10n.format("video.panel.source.mediaFile", state.displayName)
)
}
}

private func configureUI() {
toggleButton.bezelStyle = .disclosure
toggleButton.setButtonType(.switch)
toggleButton.title = L10n.text("video.panel.toggle.title")
toggleButton.state = .on
toggleButton.target = self
toggleButton.action = #selector(toggleExpanded)
toggleButton.setAccessibilityLabel(L10n.text("video.panel.toggle.accessibilityLabel"))

videoFrameView.translatesAutoresizingMaskIntoConstraints = false
toggleButton.translatesAutoresizingMaskIntoConstraints = false
translatesAutoresizingMaskIntoConstraints = false

addSubview(toggleButton)
addSubview(videoFrameView)

expandedHeightConstraint = videoFrameView.heightAnchor.constraint(equalToConstant: 220)
collapsedHeightConstraint = videoFrameView.heightAnchor.constraint(equalToConstant: 0)
collapsedHeightConstraint?.priority = .defaultHigh

NSLayoutConstraint.activate([
toggleButton.topAnchor.constraint(equalTo: topAnchor),
toggleButton.leadingAnchor.constraint(equalTo: leadingAnchor),
toggleButton.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),

videoFrameView.topAnchor.constraint(equalTo: toggleButton.bottomAnchor, constant: 6),
videoFrameView.leadingAnchor.constraint(equalTo: leadingAnchor),
videoFrameView.trailingAnchor.constraint(equalTo: trailingAnchor),
videoFrameView.bottomAnchor.constraint(equalTo: bottomAnchor),
expandedHeightConstraint!
])

applyExpandedState()
}

private func applyExpandedState() {
toggleButton.state = isExpanded ? .on : .off
videoFrameView.isHidden = !isExpanded
if isExpanded {
collapsedHeightConstraint?.isActive = false
expandedHeightConstraint?.isActive = true
} else {
expandedHeightConstraint?.isActive = false
collapsedHeightConstraint?.isActive = true
}
}

@objc private func toggleExpanded() {
setExpanded(toggleButton.state == .on, notifyDelegate: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ extension ConnectedServerViewController: NSOutlineViewDelegate {
func outlineViewSelectionDidChange(_ notification: Notification) {
selectedKey = currentSelectionKey()
updateMenuState()
updateVideoSelectionFromTree()
}

func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
Expand Down
95 changes: 88 additions & 7 deletions App/ttaccessible/AppKit/ConnectedServerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ final class ConnectedServerViewController: NSViewController {
let messageField = NSTextField(frame: .zero)
let sendButton = NSButton(title: "", target: nil, action: nil)
let microphoneButton = NSButton(title: "", target: nil, action: nil)
let collapsibleVideoPanel = CollapsibleVideoPanelView()
let embeddedMediaStreamingControls = MediaStreamingPlayerViewController()
var lastVideoDisplayState = VideoDisplayState.empty
lazy var inputGainControl = AudioGainControlView(
title: L10n.text("connectedServer.audio.inputGain.label"),
accessibilityLabel: L10n.text("connectedServer.audio.inputGain.accessibilityLabel")
Expand Down Expand Up @@ -101,6 +104,8 @@ final class ConnectedServerViewController: NSViewController {
self.menuState = menuState
self.appDelegate = appDelegate
super.init(nibName: nil, bundle: nil)
embeddedMediaStreamingControls.actions = self
collapsibleVideoPanel.delegate = self
}

@available(*, unavailable)
Expand Down Expand Up @@ -405,6 +410,12 @@ final class ConnectedServerViewController: NSViewController {
microphoneButton.action = #selector(toggleMicrophone)
microphoneButton.bezelStyle = .rounded

collapsibleVideoPanel.setExpanded(preferencesStore.preferences.videoPanelExpanded, notifyDelegate: false)
collapsibleVideoPanel.translatesAutoresizingMaskIntoConstraints = false

addChild(embeddedMediaStreamingControls)
embeddedMediaStreamingControls.view.translatesAutoresizingMaskIntoConstraints = false

// -- Layout en colonne unique --
// Ordre : titre, statut, recherche, liste canaux, gains, audio, chat, message, historique

Expand All @@ -418,14 +429,24 @@ final class ConnectedServerViewController: NSViewController {
chatScrollView.translatesAutoresizingMaskIntoConstraints = false
historyScrollView.translatesAutoresizingMaskIntoConstraints = false

let audioControlsStack = NSStackView(views: [
outputGainControl,
inputGainControl,
embeddedMediaStreamingControls.view
])
audioControlsStack.orientation = .vertical
audioControlsStack.alignment = .leading
audioControlsStack.spacing = 8
audioControlsStack.translatesAutoresizingMaskIntoConstraints = false

let mainStack = NSStackView(views: [
titleLabel,
statusLabel,
audioStatusLabel,
microphoneButton,
channelsScrollView,
outputGainControl,
inputGainControl,
collapsibleVideoPanel,
audioControlsStack,
chatTitleLabel,
chatScrollView,
inputStack,
Expand All @@ -446,8 +467,11 @@ final class ConnectedServerViewController: NSViewController {
mainStack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20),
channelsScrollView.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
channelsScrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200),
outputGainControl.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
inputGainControl.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
collapsibleVideoPanel.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
audioControlsStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
outputGainControl.widthAnchor.constraint(equalTo: audioControlsStack.widthAnchor),
inputGainControl.widthAnchor.constraint(equalTo: audioControlsStack.widthAnchor),
embeddedMediaStreamingControls.view.widthAnchor.constraint(equalTo: audioControlsStack.widthAnchor),
chatScrollView.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
chatScrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160),
inputStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor),
Expand Down Expand Up @@ -493,6 +517,7 @@ final class ConnectedServerViewController: NSViewController {
}
updateChatInputState()
updateAudioControls()
updateVideoSelectionFromTree()

// Only reload the outline when the channel tree or user list changed.
let treeChanged = previousSession.rootChannels != session.rootChannels
Expand Down Expand Up @@ -561,14 +586,42 @@ final class ConnectedServerViewController: NSViewController {
outputGainDB: session.outputGainDB,
recordingActive: session.recordingActive,
mediaStreamingActive: session.mediaStreamingActive,
mediaStreamingFileName: session.mediaStreamingFileName
mediaStreamingFileName: session.mediaStreamingFileName,
mediaStreamingHasVideo: session.mediaStreamingHasVideo
)

updateAudioControls()
reloadVisibleUserRows(for: changedUserIDs)
updateMenuState()
}

func applyVideoDisplay(_ state: VideoDisplayState) {
lastVideoDisplayState = state
collapsibleVideoPanel.updateVideoState(state)
}

func applyMediaStreamingProgress(_ progress: MediaStreamingProgress) {
embeddedMediaStreamingControls.update(with: progress)
}

func updateVideoSelectionFromTree() {
guard case .user(let user)? = selectedNode else {
if session.mediaStreamingActive, session.mediaStreamingHasVideo, let me = session.currentUser {
connectionController.setActiveVideoDisplayFromSelection(
userID: me.id,
hasMediaVideo: true
)
} else {
connectionController.setActiveVideoDisplayFromSelection(userID: 0, hasMediaVideo: false)
}
return
}
connectionController.setActiveVideoDisplayFromSelection(
userID: user.id,
hasMediaVideo: user.isStreamingMediaFileVideo
)
}

func updateMenuState() {
let selectedUsers = selectedUserNodes()
.filter { $0.isCurrentUser == false }
Expand Down Expand Up @@ -673,6 +726,7 @@ final class ConnectedServerViewController: NSViewController {
isTalking: update.isTalking,
isMuted: update.isMuted,
isMediaFileMuted: update.isMediaFileMuted,
isStreamingMediaFileVideo: user.isStreamingMediaFileVideo,
isAway: user.isAway,
isQuestion: user.isQuestion,
ipAddress: user.ipAddress,
Expand Down Expand Up @@ -751,7 +805,8 @@ final class ConnectedServerViewController: NSViewController {
outputGainDB: session.outputGainDB,
recordingActive: session.recordingActive,
mediaStreamingActive: session.mediaStreamingActive,
mediaStreamingFileName: session.mediaStreamingFileName
mediaStreamingFileName: session.mediaStreamingFileName,
mediaStreamingHasVideo: session.mediaStreamingHasVideo
)
updateAudioControls()
}
Expand Down Expand Up @@ -788,7 +843,8 @@ final class ConnectedServerViewController: NSViewController {
outputGainDB: normalized,
recordingActive: session.recordingActive,
mediaStreamingActive: session.mediaStreamingActive,
mediaStreamingFileName: session.mediaStreamingFileName
mediaStreamingFileName: session.mediaStreamingFileName,
mediaStreamingHasVideo: session.mediaStreamingHasVideo
)
updateAudioControls()
}
Expand Down Expand Up @@ -1330,6 +1386,31 @@ extension ConnectedServerViewController: ConnectedServerOutlineViewActionDelegat
}
}

extension ConnectedServerViewController: MediaStreamingPlayerActions {
func mediaStreamingPlayerDidTogglePlayPause() {
connectionController.toggleMediaStreamingPaused()
}

func mediaStreamingPlayerDidStop() {
connectionController.stopStreamingMediaFile()
announce(L10n.text("mediaStream.announced.finished"))
}

func mediaStreamingPlayerDidSeek(toMSec offsetMSec: UInt32) {
connectionController.seekMediaStreaming(toMSec: offsetMSec)
}

func mediaStreamingPlayerDidChangeBroadcastGainPercent(_ percent: Int) {
connectionController.setMediaStreamingBroadcastGainPercent(percent)
}
}

extension ConnectedServerViewController: CollapsibleVideoPanelViewDelegate {
func collapsibleVideoPanelViewDidToggleExpanded(_ view: CollapsibleVideoPanelView, expanded: Bool) {
preferencesStore.updateVideoPanelExpanded(expanded)
}
}

extension ConnectedServerViewController: NSUserInterfaceValidations {
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
switch item.action {
Expand Down
Loading