Playing audio records

This commit is contained in:
Selim Mustafaev 2025-03-31 17:23:10 +03:00
parent 59530b62ba
commit 94e4605af3
9 changed files with 143 additions and 7 deletions

View File

@ -146,6 +146,8 @@
7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */; }; 7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */; };
7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */; }; 7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */; };
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */; }; 7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */; };
7AA515D02D9ABCC800EB3418 /* RecordPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */; };
7AA515D22D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA515D12D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift */; };
7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; }; 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; };
7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; }; 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; };
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFAD22C4D0FD00050410D /* ACImageSliderView.swift */; }; 7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFAD22C4D0FD00050410D /* ACImageSliderView.swift */; };
@ -444,6 +446,8 @@
7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNoteCell.swift; sourceTree = "<group>"; }; 7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNoteCell.swift; sourceTree = "<group>"; };
7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxRealmDataSource.swift; sourceTree = "<group>"; }; 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxRealmDataSource.swift; sourceTree = "<group>"; };
7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+Events.swift"; sourceTree = "<group>"; }; 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+Events.swift"; sourceTree = "<group>"; };
7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordPlayerService.swift; sourceTree = "<group>"; };
7AA515D12D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordPlayerServiceProtocol.swift; sourceTree = "<group>"; };
7AAAFAD22C4D0FD00050410D /* ACImageSliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderView.swift; sourceTree = "<group>"; }; 7AAAFAD22C4D0FD00050410D /* ACImageSliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderView.swift; sourceTree = "<group>"; };
7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoomable.swift; sourceTree = "<group>"; }; 7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoomable.swift; sourceTree = "<group>"; };
7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = "<group>"; }; 7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = "<group>"; };
@ -823,6 +827,7 @@
7A45FB362C2706D000618694 /* Services */ = { 7A45FB362C2706D000618694 /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7AA515CE2D9ABC9B00EB3418 /* RecordPlayerService */,
7ABDA8012D8704C90083C715 /* VehicleRecordService */, 7ABDA8012D8704C90083C715 /* VehicleRecordService */,
7A9519772D80B3B200E69883 /* AudioRecordService */, 7A9519772D80B3B200E69883 /* AudioRecordService */,
7AB4E4392D3D3F390006D052 /* VehicleService */, 7AB4E4392D3D3F390006D052 /* VehicleService */,
@ -1043,6 +1048,15 @@
path = RecordsScreen; path = RecordsScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7AA515CE2D9ABC9B00EB3418 /* RecordPlayerService */ = {
isa = PBXGroup;
children = (
7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */,
7AA515D12D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift */,
);
path = RecordPlayerService;
sourceTree = "<group>";
};
7AAAFAD12C4D0FB00050410D /* ACImageSlider */ = { 7AAAFAD12C4D0FB00050410D /* ACImageSlider */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1630,6 +1644,7 @@
7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */, 7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */,
7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */, 7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */,
7AF6D21F2677C1680086EA64 /* Response.swift in Sources */, 7AF6D21F2677C1680086EA64 /* Response.swift in Sources */,
7AA515D02D9ABCC800EB3418 /* RecordPlayerService.swift in Sources */,
7A60D24D2C5A9D4900D13F7B /* LocationService.swift in Sources */, 7A60D24D2C5A9D4900D13F7B /* LocationService.swift in Sources */,
7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */, 7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */,
7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */, 7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */,
@ -1647,6 +1662,7 @@
7A60D2512C5A9E4200D13F7B /* GeocoderProtocol.swift in Sources */, 7A60D2512C5A9E4200D13F7B /* GeocoderProtocol.swift in Sources */,
7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */, 7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */,
7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */, 7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */,
7AA515D22D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift in Sources */,
7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */, 7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */,
7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */, 7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */,
7A1CF81629A42117007962DA /* Realm.swift in Sources */, 7A1CF81629A42117007962DA /* Realm.swift in Sources */,

View File

@ -70,6 +70,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
settingsService: settingsService settingsService: settingsService
) )
container.register(VehicleRecordServiceProtocol.self, instance: vehicleRecordService) container.register(VehicleRecordServiceProtocol.self, instance: vehicleRecordService)
container.register(RecordPlayerServiceProtocol.self, instance: RecordPlayerService())
} }
func setupRootController(scene: UIScene, openReport number: String?) { func setupRootController(scene: UIScene, openReport number: String?) {

View File

@ -18,7 +18,7 @@ struct AudioRecordViewModel: Identifiable {
var date: String var date: String
var onPlay: () -> Void var onPlay: () -> Void
init(dto: AudioRecordDto, onPlay: @escaping () -> Void) { init(dto: AudioRecordDto, isPlaying: Bool = false, onPlay: @escaping () -> Void) {
self.id = dto.id self.id = dto.id
self.duration = Formatters.time.string(from: dto.duration) self.duration = Formatters.time.string(from: dto.duration)
@ -26,7 +26,7 @@ struct AudioRecordViewModel: Identifiable {
self.date = Formatters.short.string( self.date = Formatters.short.string(
from: Date(timeIntervalSince1970: dto.addedDate) from: Date(timeIntervalSince1970: dto.addedDate)
) )
self.isPlaying = false self.isPlaying = isPlaying
self.onPlay = onPlay self.onPlay = onPlay
} }
} }

View File

@ -20,7 +20,8 @@ final class RecordsCoordinator {
let resolver = ServiceContainer.shared let resolver = ServiceContainer.shared
let viewModel = RecordsViewModel( let viewModel = RecordsViewModel(
recordService: resolver.resolve(VehicleRecordServiceProtocol.self), recordService: resolver.resolve(VehicleRecordServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self) storageService: resolver.resolve(StorageServiceProtocol.self),
recordPlayer: resolver.resolve(RecordPlayerServiceProtocol.self)
) )
let view = RecordsScreen(viewModel: viewModel) let view = RecordsScreen(viewModel: viewModel)

View File

@ -15,24 +15,31 @@ final class RecordsViewModel: ACHudContainer {
let recordService: VehicleRecordServiceProtocol let recordService: VehicleRecordServiceProtocol
let storageService: StorageServiceProtocol let storageService: StorageServiceProtocol
let recordPlayer: RecordPlayerServiceProtocol
var hud: ACHud? var hud: ACHud?
var showRecordingAlert: Bool = false var showRecordingAlert: Bool = false
var records: [AudioRecordDto] = [] var records: [AudioRecordDto] = []
var playingRecord: AudioRecordDto?
var recordModels: [AudioRecordViewModel] { var recordModels: [AudioRecordViewModel] {
records.map { record in return records.map { record in
AudioRecordViewModel(dto: record) { [weak self] in AudioRecordViewModel(
dto: record,
isPlaying: record.id == playingRecord?.id
) { [weak self] in
self?.onPlayTapped(record: record) self?.onPlayTapped(record: record)
} }
} }
} }
init(recordService: VehicleRecordServiceProtocol, init(recordService: VehicleRecordServiceProtocol,
storageService: StorageServiceProtocol) { storageService: StorageServiceProtocol,
recordPlayer: RecordPlayerServiceProtocol) {
self.recordService = recordService self.recordService = recordService
self.storageService = storageService self.storageService = storageService
self.recordPlayer = recordPlayer
} }
func onAppear() async { func onAppear() async {
@ -65,5 +72,17 @@ final class RecordsViewModel: ACHudContainer {
} }
func onPlayTapped(record: AudioRecordDto) { func onPlayTapped(record: AudioRecordDto) {
do {
playingRecord = record
try recordPlayer.play(record: record) { [weak self] dto, error in
self?.playingRecord = nil
if let error {
self?.hud = .error(error)
}
}
} catch {
playingRecord = nil
hud = .error(error)
}
} }
} }

View File

@ -0,0 +1,80 @@
//
// RecordPlayerService.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 31.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import AVFoundation
public final class RecordPlayerService: NSObject {
var player: AVAudioPlayer?
var record: AudioRecordDto?
var onStop: ((AudioRecordDto, Error?) -> Void)?
}
extension RecordPlayerService: RecordPlayerServiceProtocol {
public func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws {
player?.stop()
let url = try FileManager.default.url(for: record.path, in: Constants.audioRecordsFolder)
player = try AVAudioPlayer(contentsOf: url)
player?.delegate = self
self.record = record
self.onStop = onStop
// try AVAudioSession.sharedInstance().setCategory(.playback)
// try AVAudioSession.sharedInstance().setActive(true)
print("==== playing started ====")
player?.play()
}
public func pause() {
player?.pause()
}
public var currentPlayingId: TimeInterval? {
guard player?.isPlaying == true else {
return nil
}
return record?.id
}
}
extension RecordPlayerService: AVAudioPlayerDelegate {
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let record else {
return
}
print("==== playing stopped ====")
onStop?(record, nil)
reset()
}
public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: (any Error)?) {
guard let record else {
return
}
print("==== playing error ====")
onStop?(record, error)
reset()
}
func reset() {
onStop = nil
player = nil
record = nil
}
}

View File

@ -0,0 +1,17 @@
//
// RecordPlayerServiceProtocol.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 31.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
public protocol RecordPlayerServiceProtocol {
var currentPlayingId: TimeInterval? { get }
func play(record: AudioRecordDto, onStop: ((AudioRecordDto, Error?) -> Void)?) throws
func pause()
}

View File

@ -95,7 +95,7 @@ extension VehicleRecordService: VehicleRecordServiceProtocol {
date = Date() date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a" let fileName = "recording-\(date.timeIntervalSince1970).m4a"
let url = try FileManager.default.url(for: fileName, in: "recordings") let url = try FileManager.default.url(for: fileName, in: Constants.audioRecordsFolder)
self.url = url self.url = url
try await recordService.startRecording(to: url) try await recordService.startRecording(to: url)

View File

@ -50,4 +50,6 @@ public enum Constants {
public static let reportLinkTokenSecret = "#TheTruthIsOutThere" public static let reportLinkTokenSecret = "#TheTruthIsOutThere"
public static let reportLinkBaseURL = "https://auto.aliencat.pro/report.html" public static let reportLinkBaseURL = "https://auto.aliencat.pro/report.html"
public static let audioRecordsFolder = "recordings"
} }