Signal-iOS/SignalUI/Views/VideoEditor/VideoAttachmentPrepViewController.swift
Evan Hahn 370ff654e7
Change license to AGPL
Change license to AGPL

This commit:

- Updates the `LICENSE` file

- Start every file with something like:

      // Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
      // SPDX-License-Identifier: AGPL-3.0-only

---

First, I removed existing license headers with this Ruby 3.1.2 script:

    require 'set'

    EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']

    same = 0
    different = 0

    all_files = `git ls-files`.lines.map { |line| line.strip }
    all_files.each do |relative_path|
      if relative_path == 'Pods'
        next
      end

      unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
        next
      end

      path = File.expand_path(relative_path)

      contents = File.read(path)
      new_contents = contents.sub(/\/\/\n\/\/  Copyright .*\n\/\/\n\n/, '')

      if contents == new_contents
        same += 1
      else
        different += 1
      end

      File.write(path, new_contents)
    end

    puts "updated #{different} file(s), left #{same} untouched"

I'm sure this script could be improved, but it worked well enough.

Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.

Then I fixed some stragglers and updated the precommit script.

See [a similar change in the Desktop app][0].

[0]: 8bfaf598af
2022-10-13 08:25:37 -05:00

240 lines
7.7 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import UIKit
protocol VideoPlaybackState {
var isPlaying: Bool { get }
var currentTimeSeconds: TimeInterval { get }
}
protocol VideoEditorDataSource: AnyObject {
var untrimmedDurationSeconds: TimeInterval { get }
var trimmedStartSeconds: TimeInterval { get }
var trimmedEndSeconds: TimeInterval { get }
var canBeTrimmed: Bool { get }
var isTrimmed: Bool { get }
}
/**
* Coordinate data transfer between VideoEditorView and VideoTimelineView
*/
class VideoAttachmentPrepViewController: AttachmentPrepViewController {
private let model: VideoEditorModel
private lazy var editorView = VideoEditorView(model: model, delegate: self, dataSource: self, viewControllerProvider: self)
private lazy var timelineView: VideoTimelineView = {
let timelineView = VideoTimelineView()
timelineView.dataSource = self
timelineView.delegate = self
return timelineView
}()
required init?(attachmentApprovalItem: AttachmentApprovalItem) {
guard let videoEditorModel = attachmentApprovalItem.videoEditorModel else {
owsFailDebug("videoEditorModel is empty.")
return nil
}
self.model = videoEditorModel
super.init(attachmentApprovalItem: attachmentApprovalItem)
model.add(observer: self)
}
override var contentView: AttachmentPrepContentView {
editorView
}
override var toolbarSupplementaryView: UIView? {
timelineView
}
override func prepareContentView() {
editorView.configureSubviews()
generateThumbnailsAsync()
}
override func prepareToMoveOffscreen() {
editorView.pauseIfPlaying()
}
override public var canSaveMedia: Bool {
if model.needsRender {
return true
}
return super.canSaveMedia
}
private(set) var videoThumbnails: [UIImage]?
private var shouldResumeVideoPlaybackOnScrubbingEnd = false
}
extension VideoAttachmentPrepViewController: VideoEditorViewDelegate {
func videoEditorViewPlaybackTimeDidChange(_ videoEditorView: VideoEditorView) {
timelineView.updateCursorPosition()
timelineView.updateTimeBubble()
}
}
extension VideoAttachmentPrepViewController: VideoEditorDataSource {
var untrimmedDurationSeconds: TimeInterval {
return model.untrimmedDurationSeconds
}
var trimmedStartSeconds: TimeInterval {
return model.trimmedStartSeconds
}
var trimmedEndSeconds: TimeInterval {
return model.trimmedEndSeconds
}
var canBeTrimmed: Bool {
return model.canBeTrimmed
}
var isTrimmed: Bool {
return model.isTrimmed
}
}
extension VideoAttachmentPrepViewController: VideoPlaybackState {
var isPlaying: Bool {
return editorView.isPlaying
}
var currentTimeSeconds: TimeInterval {
return editorView.currentTimeSeconds
}
}
extension VideoAttachmentPrepViewController: VideoTimelineViewDataSource {
var videoAspectRatio: CGSize {
return model.displaySize
}
private func generateThumbnailsAsync() {
let model = self.model
let videoAspectRatio = videoAspectRatio
let untrimmedDurationSeconds = self.untrimmedDurationSeconds
firstly {
VideoAttachmentPrepViewController.thumbnails(forVideoAtPath: model.srcVideoPath,
aspectRatio: videoAspectRatio,
thumbnailHeight: VideoTimelineView.preferredHeight,
untrimmedDurationSeconds: untrimmedDurationSeconds)
}.done(on: .main) { [weak self] (thumbnails: [UIImage]) -> Void in
guard let self = self else {
return
}
self.videoThumbnails = thumbnails
self.timelineView.updateThumbnailView()
}.catch { error in
owsFailDebug("Error: \(error)")
}
}
private class func thumbnails(forVideoAtPath videoPath: String,
aspectRatio: CGSize,
thumbnailHeight: CGFloat,
untrimmedDurationSeconds: TimeInterval) -> Promise<[UIImage]> {
AssertIsOnMainThread()
let contextSize = CurrentAppContext().frame.size
let screenScale = UIScreen.main.scale
return DispatchQueue.global().async(.promise) {
// We generate enough thumbnails for the worst case (full-screen landscape)
// to avoid the complexity of regeneration.
let contextMaxDimension = max(contextSize.width, contextSize.height)
let thumbnailWidth = floor(thumbnailHeight * aspectRatio.width / aspectRatio.height)
let thumbnailCount = UInt(ceil(contextMaxDimension / thumbnailWidth))
let maxThumbnailSize = max(thumbnailWidth, thumbnailHeight) * screenScale
let url = URL(fileURLWithPath: videoPath)
let asset = AVURLAsset(url: url, options: nil)
let generator = AVAssetImageGenerator(asset: asset)
generator.maximumSize = CGSize(square: maxThumbnailSize)
generator.appliesPreferredTrackTransform = true
var thumbnails = [UIImage]()
for index in 0..<thumbnailCount {
let thumbnailAlpha = Double(index) / Double(thumbnailCount - 1)
let thumbnailTimeSeconds = thumbnailAlpha * untrimmedDurationSeconds
let thumbnailCMTime = CMTime(seconds: thumbnailTimeSeconds, preferredTimescale: 1000)
let cgImage = try generator.copyCGImage(at: thumbnailCMTime, actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage, scale: 1, orientation: .up)
thumbnails.append(thumbnail)
}
return thumbnails
}
}
}
extension VideoAttachmentPrepViewController: VideoTimelineViewDelegate {
func videoTimelineViewDidBeginTrimming(_ view: VideoTimelineView) {
editorView.pauseIfPlaying()
editorView.isTrimmingVideo = true
}
func videoTimelineView(_ view: VideoTimelineView, didTrimBeginningTo seconds: TimeInterval) {
model.trimToStartSeconds(seconds)
editorView.seek(toSeconds: seconds)
}
func videoTimelineView(_ view: VideoTimelineView, didTrimEndTo seconds: TimeInterval) {
model.trimToEndSeconds(seconds)
editorView.seek(toSeconds: seconds)
}
func videoTimelineViewDidEndTrimming(_ view: VideoTimelineView) {
editorView.isTrimmingVideo = false
editorView.ensureSeekReflectsTrimming()
if model.needsRender {
_ = model.ensureCurrentRender()
}
}
func videoTimelineViewWillBeginScrubbing(_ view: VideoTimelineView) {
// Pause playback during scrubbing.
shouldResumeVideoPlaybackOnScrubbingEnd = editorView.pauseIfPlaying()
}
func videoTimelineView(_ view: VideoTimelineView, didScrubTo seconds: TimeInterval) {
editorView.seek(toSeconds: seconds)
}
func videoTimelineViewDidEndScrubbing(_ view: VideoTimelineView) {
if shouldResumeVideoPlaybackOnScrubbingEnd {
editorView.playVideo()
}
}
}
extension VideoAttachmentPrepViewController: VideoEditorModelObserver {
func videoEditorModelDidChange(_ model: VideoEditorModel) {
timelineView.updateContents()
}
}
extension VideoAttachmentPrepViewController: VideoEditorViewControllerProviding {
func viewController(forVideoEditorView videoEditorView: VideoEditorView) -> UIViewController {
return self
}
}