Commit 65b05c9e authored by shigemi miura's avatar shigemi miura

動画処理が出来ないバージョン

parent 8cec0939
...@@ -149,6 +149,7 @@ ...@@ -149,6 +149,7 @@
D5E008762B2ADD5900C4070A /* MenuManualRADARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E008752B2ADD5900C4070A /* MenuManualRADARView.swift */; }; D5E008762B2ADD5900C4070A /* MenuManualRADARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E008752B2ADD5900C4070A /* MenuManualRADARView.swift */; };
D5E008782B2B022200C4070A /* MenuAboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E008772B2B022200C4070A /* MenuAboutAppView.swift */; }; D5E008782B2B022200C4070A /* MenuAboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E008772B2B022200C4070A /* MenuAboutAppView.swift */; };
D5E03A672B04484D00D65FCE /* SessionTaskList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E03A662B04484D00D65FCE /* SessionTaskList.swift */; }; D5E03A672B04484D00D65FCE /* SessionTaskList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E03A662B04484D00D65FCE /* SessionTaskList.swift */; };
D5F969BD2E8AAD4F005662B0 /* VideoQualityPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F969BC2E8AAD3C005662B0 /* VideoQualityPreset.swift */; };
D5FCEF552B478985009A81D0 /* ResChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF542B478985009A81D0 /* ResChatMessage.swift */; }; D5FCEF552B478985009A81D0 /* ResChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF542B478985009A81D0 /* ResChatMessage.swift */; };
D5FCEF572B4789A8009A81D0 /* ResAckMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF562B4789A8009A81D0 /* ResAckMessage.swift */; }; D5FCEF572B4789A8009A81D0 /* ResAckMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF562B4789A8009A81D0 /* ResAckMessage.swift */; };
D5FCEF592B4789D2009A81D0 /* ResChatMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF582B4789D2009A81D0 /* ResChatMode.swift */; }; D5FCEF592B4789D2009A81D0 /* ResChatMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FCEF582B4789D2009A81D0 /* ResChatMode.swift */; };
...@@ -339,6 +340,7 @@ ...@@ -339,6 +340,7 @@
D5E008752B2ADD5900C4070A /* MenuManualRADARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuManualRADARView.swift; path = Sailassist/Menu/View/MenuManualRADARView.swift; sourceTree = SOURCE_ROOT; }; D5E008752B2ADD5900C4070A /* MenuManualRADARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuManualRADARView.swift; path = Sailassist/Menu/View/MenuManualRADARView.swift; sourceTree = SOURCE_ROOT; };
D5E008772B2B022200C4070A /* MenuAboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuAboutAppView.swift; path = Sailassist/Menu/View/MenuAboutAppView.swift; sourceTree = SOURCE_ROOT; }; D5E008772B2B022200C4070A /* MenuAboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuAboutAppView.swift; path = Sailassist/Menu/View/MenuAboutAppView.swift; sourceTree = SOURCE_ROOT; };
D5E03A662B04484D00D65FCE /* SessionTaskList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionTaskList.swift; path = Sailassist/ServerSession/SessionTaskList.swift; sourceTree = SOURCE_ROOT; }; D5E03A662B04484D00D65FCE /* SessionTaskList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionTaskList.swift; path = Sailassist/ServerSession/SessionTaskList.swift; sourceTree = SOURCE_ROOT; };
D5F969BC2E8AAD3C005662B0 /* VideoQualityPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoQualityPreset.swift; sourceTree = "<group>"; };
D5FCEF542B478985009A81D0 /* ResChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResChatMessage.swift; path = Sailassist/Json/ResChatMessage.swift; sourceTree = SOURCE_ROOT; }; D5FCEF542B478985009A81D0 /* ResChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResChatMessage.swift; path = Sailassist/Json/ResChatMessage.swift; sourceTree = SOURCE_ROOT; };
D5FCEF562B4789A8009A81D0 /* ResAckMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResAckMessage.swift; path = Sailassist/Json/ResAckMessage.swift; sourceTree = SOURCE_ROOT; }; D5FCEF562B4789A8009A81D0 /* ResAckMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResAckMessage.swift; path = Sailassist/Json/ResAckMessage.swift; sourceTree = SOURCE_ROOT; };
D5FCEF582B4789D2009A81D0 /* ResChatMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResChatMode.swift; path = Sailassist/Json/ResChatMode.swift; sourceTree = SOURCE_ROOT; }; D5FCEF582B4789D2009A81D0 /* ResChatMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ResChatMode.swift; path = Sailassist/Json/ResChatMode.swift; sourceTree = SOURCE_ROOT; };
...@@ -541,6 +543,7 @@ ...@@ -541,6 +543,7 @@
D5AF8A4B2E88CDF600BECA22 /* ViewModel */, D5AF8A4B2E88CDF600BECA22 /* ViewModel */,
D5258CA42B036F0700365276 /* GetMessage.swift */, D5258CA42B036F0700365276 /* GetMessage.swift */,
D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */, D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */,
D5F969BC2E8AAD3C005662B0 /* VideoQualityPreset.swift */,
D524DAF72B5A6D3600A399DD /* Imagepicker.swift */, D524DAF72B5A6D3600A399DD /* Imagepicker.swift */,
); );
path = Chat; path = Chat;
...@@ -665,8 +668,6 @@ ...@@ -665,8 +668,6 @@
children = ( children = (
020B98622ADD14E40029DE4C /* ChatView.swift */, 020B98622ADD14E40029DE4C /* ChatView.swift */,
02A1DE2E2AFB4AA0005BCF55 /* ChatInputView.swift */, 02A1DE2E2AFB4AA0005BCF55 /* ChatInputView.swift */,
D57405052E827C35001C74DF /* ChatUrlVideoView.swift */,
D5A4C2522E7AB46B00642D7D /* SpeechRecognizer.swift */,
02C3E5D02AFCC16800AF7837 /* ChatTitleView.swift */, 02C3E5D02AFCC16800AF7837 /* ChatTitleView.swift */,
02A1DE302AFB61D8005BCF55 /* MyChatContentView.swift */, 02A1DE302AFB61D8005BCF55 /* MyChatContentView.swift */,
02C3E5CD2AFCA04700AF7837 /* OtherChatContentView.swift */, 02C3E5CD2AFCA04700AF7837 /* OtherChatContentView.swift */,
...@@ -674,7 +675,9 @@ ...@@ -674,7 +675,9 @@
02C3E6082AFDF30000AF7837 /* ChatMemberView.swift */, 02C3E6082AFDF30000AF7837 /* ChatMemberView.swift */,
D524DAF92B5A6F5F00A399DD /* CameraView.swift */, D524DAF92B5A6F5F00A399DD /* CameraView.swift */,
D5598B772C435A5C00611AE0 /* ChatUrlImageView.swift */, D5598B772C435A5C00611AE0 /* ChatUrlImageView.swift */,
D57405052E827C35001C74DF /* ChatUrlVideoView.swift */,
D5598B792C435C4500611AE0 /* ChatUrlRawImageView.swift */, D5598B792C435C4500611AE0 /* ChatUrlRawImageView.swift */,
D5A4C2522E7AB46B00642D7D /* SpeechRecognizer.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1148,6 +1151,7 @@ ...@@ -1148,6 +1151,7 @@
020B98512AD9189A0029DE4C /* InputIdPassWordView.swift in Sources */, 020B98512AD9189A0029DE4C /* InputIdPassWordView.swift in Sources */,
D52C2C0A2B91BF24003B286C /* InformationView.swift in Sources */, D52C2C0A2B91BF24003B286C /* InformationView.swift in Sources */,
D52D213A2AEBAC0500324D58 /* HttpRequestType.swift in Sources */, D52D213A2AEBAC0500324D58 /* HttpRequestType.swift in Sources */,
D5F969BD2E8AAD4F005662B0 /* VideoQualityPreset.swift in Sources */,
D53B97262B341867000B3D29 /* AboutAppView.swift in Sources */, D53B97262B341867000B3D29 /* AboutAppView.swift in Sources */,
D5258CA12B03593500365276 /* SessionMonitoringRoute.swift in Sources */, D5258CA12B03593500365276 /* SessionMonitoringRoute.swift in Sources */,
D5E03A672B04484D00D65FCE /* SessionTaskList.swift in Sources */, D5E03A672B04484D00D65FCE /* SessionTaskList.swift in Sources */,
......
//
// Imagepicker.swift
// Sailassist
//
// Created by 三浦薫巳 on 2024/01/19.
//
import SwiftUI import SwiftUI
import AVFoundation
struct Imagepicker : UIViewControllerRepresentable { struct Imagepicker : UIViewControllerRepresentable {
@Binding var show: Bool @Binding var show: Bool
...@@ -30,6 +25,7 @@ struct Imagepicker : UIViewControllerRepresentable { ...@@ -30,6 +25,7 @@ struct Imagepicker : UIViewControllerRepresentable {
class Coordinator: NSObject,UIImagePickerControllerDelegate,UINavigationControllerDelegate { class Coordinator: NSObject,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
var parent : Imagepicker var parent : Imagepicker
var selectedPreset : VideoQualityPreset = .medium
init(parent : Imagepicker){ init(parent : Imagepicker){
self.parent = parent self.parent = parent
...@@ -40,7 +36,7 @@ struct Imagepicker : UIViewControllerRepresentable { ...@@ -40,7 +36,7 @@ struct Imagepicker : UIViewControllerRepresentable {
self.parent.show.toggle() self.parent.show.toggle()
} }
//MARK: - Use Photo //MARK: - Use Photo / Movie
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage { if let image = info[.originalImage] as? UIImage {
//MARK: - 画像データのリサイズ //MARK: - 画像データのリサイズ
...@@ -55,29 +51,26 @@ struct Imagepicker : UIViewControllerRepresentable { ...@@ -55,29 +51,26 @@ struct Imagepicker : UIViewControllerRepresentable {
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
print("Failed to resize or compress image") print(debug: "Failed to resize or compress image")
self.parent.show.toggle() self.parent.show.toggle()
} }
} }
} }
} else if let videoURL = info[.mediaURL] as? URL { } else if let videoURL = info[.mediaURL] as? URL {
//MARK: - 動画データのリサイズ //MARK: - 動画データのリサイズ
DispatchQueue.global(qos: .userInitiated).async { compressVideo(inputURL: videoURL, preset: selectedPreset) { compressedData in
if let videoData = try? Data(contentsOf: videoURL) { DispatchQueue.main.async {
DispatchQueue.main.async { if let data = compressedData {
self.parent.image = videoData self.parent.image = data
self.parent.show.toggle() } else {
} print(debug: "Failed to compress video")
} else {
DispatchQueue.main.async {
print("Failed to process video data")
self.parent.show.toggle()
} }
self.parent.show.toggle()
} }
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
print("No valid media selected") print(debug: "No valid media selected")
self.parent.show.toggle() self.parent.show.toggle()
} }
} }
......
import Network
import AVFoundation
enum VideoQualityPreset {
case low, medium, high
var exportPresetName: String {
switch self {
case .low: return AVAssetExportPresetLowQuality
case .medium: return AVAssetExportPresetMediumQuality
case .high: return AVAssetExportPresetHighestQuality
}
}
var fileExtension: String {
return "mp4"
}
var mimeType: String {
return "video/mp4"
}
}
func isVideoSizeAcceptable(_ data: Data, maxSizeMB: Double = 28.0) -> Bool {
let sizeMB = Double(data.count) / (1024.0 * 1024.0)
return sizeMB <= maxSizeMB
}
// MARK: - 動画圧縮処理
func compressVideo(inputURL: URL, preset: VideoQualityPreset, completion: @escaping (Data?) -> Void) {
let asset = AVAsset(url: inputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: preset.exportPresetName) else {
completion(nil)
return
}
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".mp4")
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously {
if exportSession.status == .completed {
if let data = try? Data(contentsOf: outputURL), isVideoSizeAcceptable(data) {
completion(data)
} else {
completion(nil) // サイズオーバー
}
} else {
completion(nil)
}
}
}
func selectPresetBasedOnNetwork() -> VideoQualityPreset {
let monitor = NWPathMonitor()
var selectedPreset: VideoQualityPreset = .medium
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
if path.isExpensive {
selectedPreset = .low // モバイル通信
} else {
selectedPreset = .high // Wi-Fi
}
} else {
selectedPreset = .low // オフラインまたは不安定
}
}
let queue = DispatchQueue(label: "NetworkMonitor")
monitor.start(queue: queue)
return selectedPreset
}
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
// //
// Created by 三浦薫巳 on 2024/01/19. // Created by 三浦薫巳 on 2024/01/19.
// //
import SwiftUI import SwiftUI
struct CameraView: View { struct CameraView: View {
......
...@@ -136,6 +136,8 @@ struct ChatInputView: View { ...@@ -136,6 +136,8 @@ struct ChatInputView: View {
if let image = UIImage(data: imageData) { if let image = UIImage(data: imageData) {
viewModel.sendImageToTemporary(image) viewModel.sendImageToTemporary(image)
} else if !imageData.isEmpty {
viewModel.sendVideoToTemporary(imageData)
} }
}) { }) {
CameraView(imageData: $imageData, source: $source, isActionSheet: .constant(false), isImagePicker: $viewModel.isImagePickerPresented) CameraView(imageData: $imageData, source: $source, isActionSheet: .constant(false), isImagePicker: $viewModel.isImagePickerPresented)
...@@ -206,6 +208,21 @@ struct ChatInputView: View { ...@@ -206,6 +208,21 @@ struct ChatInputView: View {
} message: { } message: {
Text("Image upload failed. Would you like to try again?") Text("Image upload failed. Would you like to try again?")
} }
.alert("Upload failed", isPresented: $viewModel.isRetryDialogPresented) {
Button("retry") {
if let videoToRetry = viewModel.failedUploadImage {
viewModel.sendChatVideo(videoToRetry)
}
}
Button("Cancel", role: .cancel) {
viewModel.failedUploadImage = nil
viewModel.isRetryDialogPresented = false
SharingData.message.messages.removeAll { $0.messageId.caseInsensitiveCompare(viewModel.tempId) == .orderedSame }
}
} message: {
Text("Video upload failed. Would you like to try again?")
}
} }
} }
......
...@@ -6,67 +6,79 @@ ...@@ -6,67 +6,79 @@
// //
import SwiftUI import SwiftUI
import AVKit
struct MyChatContentView: View { struct MyChatContentView: View {
@State private var isVideoPlayerPresented = false
var message : ChatMessage var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil var onMediaLoaded: (() -> Void)? = nil
var body: some View { var body: some View {
HStack { HStack {
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 6) { VStack(alignment: .trailing, spacing: 6) {
Group { Group {
if let msg = message.message { if let msg = message.message {
if msg.contains(".jpg") || msg.contains(".png") { if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded) ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") { // .onTapGesture {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded) // if let videoUrl = message.videoUrl,
} else { // let url = URL(string: videoUrl) {
Text(msg) // isVideoPlayerPresented = true
.font(FontStyle.DefaultText.font) // }
.foregroundColor(ColorSet.BodyChat.color) // }
.padding(15) } else {
.background(ColorSet.ChatBaloon.color) Text(msg)
.border(Color.red.gradient.opacity(0.8), width: (message.mode == ChatMode.warningProgress.rawValue ? 2 : 0)) .font(FontStyle.DefaultText.font)
.clipShape( .foregroundColor(ColorSet.BodyChat.color)
.rect( .padding(15)
topLeadingRadius: 10, .background(ColorSet.ChatBaloon.color)
bottomLeadingRadius: 0, .border(Color.red.gradient.opacity(0.8), width: (message.mode == ChatMode.warningProgress.rawValue ? 2 : 0))
bottomTrailingRadius: 10, .clipShape(
topTrailingRadius: 10 .rect(
)) topLeadingRadius: 10,
} bottomLeadingRadius: 0,
bottomTrailingRadius: 10,
topTrailingRadius: 10
))
} }
} }
}
HStack(spacing: 5){ HStack(spacing: 5){
//MARK: - 既読マーク //MARK: - 既読マーク
Text(DateTextLib.ISO86012FormatText(message.time, format: "yyyy/MM/dd HH:mm", errFormat: "")) Text(DateTextLib.ISO86012FormatText(message.time, format: "yyyy/MM/dd HH:mm", errFormat: ""))
.padding(.trailing, 8) .padding(.trailing, 8)
let viewerCnt = viewerCnt() let viewerCnt = viewerCnt()
Image("chat_company") Image("chat_company")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 12, height: 12) .frame(width: 12, height: 12)
.padding(.trailing, 4) .padding(.trailing, 4)
Text(String(viewerCnt.1)) Text(String(viewerCnt.1))
Image("chat_ship") Image("chat_ship")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 12, height: 12) .frame(width: 12, height: 12)
.padding(.trailing, 4) .padding(.trailing, 4)
Text(String(viewerCnt.0)) Text(String(viewerCnt.0))
}
.font(FontStyle.DateText.font)
.foregroundColor(ColorSet.ChatDate.color)
} }
.padding(.trailing, 20) .font(FontStyle.DateText.font)
.foregroundColor(ColorSet.ChatDate.color)
}
.padding(.trailing, 20)
} }
// .fullScreenCover(isPresented: $isVideoPlayerPresented) {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// VideoPlayerView(videoURL: url)
// }
// }
} }
func viewerCnt() -> (Int, Int) { func viewerCnt() -> (Int, Int) {
...@@ -83,6 +95,16 @@ struct MyChatContentView: View { ...@@ -83,6 +95,16 @@ struct MyChatContentView: View {
} }
} }
//struct VideoPlayerView: View {
// let videoURL: URL
//
// var body: some View {
// VideoPlayer(player: AVPlayer(url: videoURL))
// .navigationTitle("動画再生")
// .navigationBarTitleDisplayMode(.inline)
// }
//}
#Preview { #Preview {
MyChatContentView(message: ChatMessage( MyChatContentView(message: ChatMessage(
shipId: 10000003, shipId: 10000003,
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
// //
// Created by Mamoru Sugita on 2023/11/09. // Created by Mamoru Sugita on 2023/11/09.
// //
import SwiftUI import SwiftUI
import AVKit
struct OtherChatContentView: View { struct OtherChatContentView: View {
@State private var isVideoPlayerPresented = false
var message : ChatMessage var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil var onMediaLoaded: (() -> Void)? = nil
...@@ -24,8 +25,13 @@ struct OtherChatContentView: View { ...@@ -24,8 +25,13 @@ struct OtherChatContentView: View {
if let msg = message.message { if let msg = message.message {
if msg.contains(".jpg") || msg.contains(".png") { if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded) ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") { // .onTapGesture {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded) // if message.type == 3,
// let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// isVideoPlayerPresented = true
// }
// }
} else { } else {
Text(msg) Text(msg)
.font(FontStyle.DefaultText.font) .font(FontStyle.DefaultText.font)
...@@ -54,6 +60,12 @@ struct OtherChatContentView: View { ...@@ -54,6 +60,12 @@ struct OtherChatContentView: View {
.padding(.leading, 20) .padding(.leading, 20)
Spacer() Spacer()
} }
// .fullScreenCover(isPresented: $isVideoPlayerPresented) {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// VideoPlayerView(videoURL: url)
// }
// }
} }
} }
......
import SwiftUI import SwiftUI
import Combine import Combine
import Speech import Speech
import AVFoundation
import UIKit
enum MediaInputType { enum MediaInputType {
case none case none
...@@ -38,30 +40,6 @@ class ChatInputViewModel: ObservableObject { ...@@ -38,30 +40,6 @@ class ChatInputViewModel: ObservableObject {
} }
} }
// MARK: - Send Chat Image
func sendChatImage(_ uploadImage: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await sessionUploadImage.requestUploadImage(uploadImage)
print(debug: "Upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
sessionUploadImage.progress = 0.0 // 完了後リセット
isUploadingDialogPresented = false
} catch {
print(debug: "Upload failed: \(error)")
sessionUploadImage.progress = 0.0 // エラー時もリセット
isUploadingDialogPresented = false
failedUploadImage = uploadImage
isRetryDialogPresented = true
}
}
}
// MARK: - Send Chat Message // MARK: - Send Chat Message
func sendChatMessage() { func sendChatMessage() {
guard !SharingData.message.sendInf else { return } guard !SharingData.message.sendInf else { return }
...@@ -109,14 +87,62 @@ class ChatInputViewModel: ObservableObject { ...@@ -109,14 +87,62 @@ class ChatInputViewModel: ObservableObject {
textViewHeight = max(newSize.height, 40) // 最低高さを40に設定 textViewHeight = max(newSize.height, 40) // 最低高さを40に設定
} }
// MARK: - ローカル画像を一時保存してURLを取得 // MARK: - Send Chat Image
func sendChatImage(_ uploadImage: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await sessionUploadImage.requestUploadImage(uploadImage)
print(debug: "Upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
sessionUploadImage.progress = 0.0 // 完了後リセット
isUploadingDialogPresented = false
} catch {
print(debug: "Upload failed: \(error)")
sessionUploadImage.progress = 0.0 // エラー時もリセット
isUploadingDialogPresented = false
failedUploadImage = uploadImage
isRetryDialogPresented = true
}
}
}
//MARK: - Send Chat Video
func sendChatVideo(_ uploadVideo: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await SessionUploadImage.OnlyOne.requestUploadVideo(uploadVideo)
print(debug: "Video upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
SessionUploadImage.OnlyOne.progress = 0.0
isUploadingDialogPresented = false
} catch {
print(debug: "Video upload failed: \(error)")
SessionUploadImage.OnlyOne.progress = 0.0
isUploadingDialogPresented = false
failedUploadImage = uploadVideo
isRetryDialogPresented = true
}
}
}
// MARK: - ローカル画像を一時保存してURLを取得 (静止画)
func saveImageToTemporaryDirectory(_ image: UIImage) -> String? { func saveImageToTemporaryDirectory(_ image: UIImage) -> String? {
let fileName = UUID().uuidString + ".jpg" let fileName = UUID().uuidString + ".jpg"
let tempDir = FileManager.default.temporaryDirectory let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent(fileName) let fileURL = tempDir.appendingPathComponent(fileName)
guard let data = image.jpegData(compressionQuality: 0.8) else { guard let data = image.jpegData(compressionQuality: 0.8) else {
print("Image JPEG conversion failed") print(debug: "Image JPEG conversion failed")
return nil return nil
} }
...@@ -124,7 +150,7 @@ class ChatInputViewModel: ObservableObject { ...@@ -124,7 +150,7 @@ class ChatInputViewModel: ObservableObject {
try data.write(to: fileURL) try data.write(to: fileURL)
return fileURL.absoluteString return fileURL.absoluteString
} catch { } catch {
print("Image JPEG conversion failed: \(error.localizedDescription)") print(debug: "Image JPEG conversion failed: \(error.localizedDescription)")
return nil return nil
} }
} }
...@@ -167,4 +193,75 @@ class ChatInputViewModel: ObservableObject { ...@@ -167,4 +193,75 @@ class ChatInputViewModel: ObservableObject {
sendChatImage(uploadImage) sendChatImage(uploadImage)
} }
} }
// MARK: - ローカル画像を一時保存してURLを取得 (動画)
func saveVideoToTemporaryDirectory(_ data: Data) -> URL? {
let fileName = UUID().uuidString + ".mp4"
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent(fileName)
do {
try data.write(to: fileURL)
return fileURL
} catch {
print(debug: "Video save failed: \(error.localizedDescription)")
return nil
}
}
//MARK: - 動画データからサムネイル画像を生成する
func generateThumbnail(from videoURL: URL, at time: CMTime = CMTime(seconds: 1.0, preferredTimescale: 600)) -> UIImage? {
let asset = AVAsset(url: videoURL)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
do {
let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
return UIImage(cgImage: cgImage)
} catch {
print("サムネイル生成失敗: \(error.localizedDescription)")
return nil
}
}
//MARK: - 送信前に送信動画をテンポラリに入れる
func sendVideoToTemporary(_ data: Data) {
if let videoURL = saveVideoToTemporaryDirectory(data),
let image = generateThumbnail(from: videoURL),
let localURL = saveImageToTemporaryDirectory(image) {
tempId = UUID().uuidString
let viewer = Viewer(
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
id: "",
name: ""
)
let tempMessage = ChatMessage(
shipId: Preferences.shipId,
messageId: tempId,
type: 3, // 動画タイプ
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
mode: SharingData.message.mode ? 1 : 0,
message: localURL,
// videoUrl: videoURL.absoluteString,
stampId: 0,
viewer: [viewer]
)
SharingData.message.messages.append(tempMessage)
let uploadVideo = ReqUploadImage(
shipId: Preferences.shipId,
messageId: tempId,
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
files: data
)
sendChatVideo(uploadVideo)
}
}
} }
...@@ -28,7 +28,10 @@ struct ChatMessage: Codable { ...@@ -28,7 +28,10 @@ struct ChatMessage: Codable {
var fromId: String? //ユーザーID var fromId: String? //ユーザーID
var mode: Int // 0:通常 , 1:Warning中 var mode: Int // 0:通常 , 1:Warning中
var message: String? //テキスト時:テキスト , 画像時:サムネイルのUri var message: String? //テキスト時:テキスト , 画像時:サムネイルのUri
// var videoUrl: String? //TODO: - テスト
var stampId: Int //スタンプ番号 0:Fire~ var stampId: Int //スタンプ番号 0:Fire~
// var Latitude: Double? //TODO: - テスト
// var Longitude: Double? //TODO: - テスト
var viewer: [Viewer] = [] //閲覧者情報 var viewer: [Viewer] = [] //閲覧者情報
} }
......
...@@ -218,7 +218,6 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M ...@@ -218,7 +218,6 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
if existingMsg.message!.hasPrefix("file://") { if existingMsg.message!.hasPrefix("file://") {
updatedMsg.message = existingMsg.message updatedMsg.message = existingMsg.message
} }
self.msg.messages[index] = updatedMsg self.msg.messages[index] = updatedMsg
} else { } else {
self.msg.messages.append(newMsg) self.msg.messages.append(newMsg)
......
...@@ -28,9 +28,7 @@ class SessionUploadImage : ObservableObject { ...@@ -28,9 +28,7 @@ class SessionUploadImage : ObservableObject {
//MARK: - 画像のアップロード //MARK: - 画像のアップロード
func requestUploadImage(_ uploadImage: ReqUploadImage) async throws -> Data { func requestUploadImage(_ uploadImage: ReqUploadImage) async throws -> Data {
print(debug: "called") print(debug: "called")
guard !Calling else { guard !Calling else { throw APIError.busy }
throw APIError.busy
}
Calling = true Calling = true
defer { Calling = false } defer { Calling = false }
...@@ -92,6 +90,74 @@ class SessionUploadImage : ObservableObject { ...@@ -92,6 +90,74 @@ class SessionUploadImage : ObservableObject {
} }
} }
// MARK: - 動画のアップロード
func requestUploadVideo(_ uploadVideo: ReqUploadImage) async throws -> Data {
print("Video upload called")
guard !Calling else {
throw APIError.busy
}
Calling = true
defer { Calling = false }
guard let req_url = URL(string: HttpRequestType.UploadImage.rawValue) else {
throw APIError.invalidURL
}
let boundary = "----------\(UUID().uuidString)"
var httpBody = Data()
func appendFormField(name: String, value: String) {
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".data(using: .utf8)!)
httpBody.append("\(value)\r\n".data(using: .utf8)!)
}
appendFormField(name: "ShipId", value: String(uploadVideo.shipId))
appendFormField(name: "MessageId", value: uploadVideo.messageId)
appendFormField(name: "Location", value: String(uploadVideo.location))
appendFormField(name: "From", value: uploadVideo.from)
appendFormField(name: "FromId", value: uploadVideo.fromId)
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"files\"; filename=\"itemp.mp4\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Type: video/mp4\r\n\r\n".data(using: .utf8)!)
httpBody.append(uploadVideo.files)
httpBody.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
var request = URLRequest(url: req_url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let session = URLSession(configuration: .default, delegate: UploadProgressDelegate(parent: self), delegateQueue: nil)
isUploading = true
return try await withCheckedThrowingContinuation { continuation in
self.uploadTask = session.uploadTask(with: request, from: httpBody) { data, response, error in
DispatchQueue.main.async {
self.isUploading = false
self.progress = 0.0
}
if let error = error {
continuation.resume(throwing: error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
continuation.resume(throwing: APIError.serverError)
return
}
continuation.resume(returning: data)
}
self.uploadTask?.resume()
}
}
func postFormAsync(boundary: String, url: URL, body: Data) async throws -> Data { func postFormAsync(boundary: String, url: URL, body: Data) async throws -> Data {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = "POST" request.httpMethod = "POST"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment