Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
Sailassist
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
54
Merge Requests
54
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
gr-ssv
Sailassist
Commits
65b05c9e
Commit
65b05c9e
authored
Oct 01, 2025
by
shigemi miura
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
動画処理が出来ないバージョン
parent
8cec0939
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
388 additions
and
100 deletions
+388
-100
project.pbxproj
Seilassist/Sailassist.xcodeproj/project.pbxproj
+6
-2
Imagepicker.swift
Seilassist/Sailassist/Chat/Imagepicker.swift
+12
-19
VideoQualityPreset.swift
Seilassist/Sailassist/Chat/VideoQualityPreset.swift
+76
-0
CameraView.swift
Seilassist/Sailassist/Chat/View/CameraView.swift
+0
-1
ChatInputView.swift
Seilassist/Sailassist/Chat/View/ChatInputView.swift
+17
-0
MyChatContentView.swift
Seilassist/Sailassist/Chat/View/MyChatContentView.swift
+66
-44
OtherChatContentView.swift
Seilassist/Sailassist/Chat/View/OtherChatContentView.swift
+15
-3
ChatInputViewModel.swift
...assist/Sailassist/Chat/ViewModel/ChatInputViewModel.swift
+124
-27
ResGetMessages.swift
Seilassist/Sailassist/Json/ResGetMessages.swift
+3
-0
SailassistApp.swift
Seilassist/Sailassist/SailassistApp.swift
+0
-1
SessionUploadImage.swift
Seilassist/Sailassist/ServerSession/SessionUploadImage.swift
+69
-3
No files found.
Seilassist/Sailassist.xcodeproj/project.pbxproj
View file @
65b05c9e
...
...
@@ -149,6 +149,7 @@
D5E008762B2ADD5900C4070A
/* MenuManualRADARView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008752B2ADD5900C4070A
/* MenuManualRADARView.swift */
;
};
D5E008782B2B022200C4070A
/* MenuAboutAppView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008772B2B022200C4070A
/* MenuAboutAppView.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 */
;
};
D5FCEF572B4789A8009A81D0
/* ResAckMessage.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF562B4789A8009A81D0
/* ResAckMessage.swift */
;
};
D5FCEF592B4789D2009A81D0
/* ResChatMode.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF582B4789D2009A81D0
/* ResChatMode.swift */
;
};
...
...
@@ -339,6 +340,7 @@
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
;
};
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
;
};
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
;
};
...
...
@@ -541,6 +543,7 @@
D5AF8A4B2E88CDF600BECA22
/* ViewModel */
,
D5258CA42B036F0700365276
/* GetMessage.swift */
,
D55186122E7E914C004CD8BD
/* ImageQualityPreset.swift */
,
D5F969BC2E8AAD3C005662B0
/* VideoQualityPreset.swift */
,
D524DAF72B5A6D3600A399DD
/* Imagepicker.swift */
,
);
path
=
Chat
;
...
...
@@ -665,8 +668,6 @@
children
=
(
020B98622ADD14E40029DE4C
/* ChatView.swift */
,
02A1DE2E2AFB4AA0005BCF55
/* ChatInputView.swift */
,
D57405052E827C35001C74DF
/* ChatUrlVideoView.swift */
,
D5A4C2522E7AB46B00642D7D
/* SpeechRecognizer.swift */
,
02C3E5D02AFCC16800AF7837
/* ChatTitleView.swift */
,
02A1DE302AFB61D8005BCF55
/* MyChatContentView.swift */
,
02C3E5CD2AFCA04700AF7837
/* OtherChatContentView.swift */
,
...
...
@@ -674,7 +675,9 @@
02C3E6082AFDF30000AF7837
/* ChatMemberView.swift */
,
D524DAF92B5A6F5F00A399DD
/* CameraView.swift */
,
D5598B772C435A5C00611AE0
/* ChatUrlImageView.swift */
,
D57405052E827C35001C74DF
/* ChatUrlVideoView.swift */
,
D5598B792C435C4500611AE0
/* ChatUrlRawImageView.swift */
,
D5A4C2522E7AB46B00642D7D
/* SpeechRecognizer.swift */
,
);
path
=
View
;
sourceTree
=
"<group>"
;
...
...
@@ -1148,6 +1151,7 @@
020B98512AD9189A0029DE4C
/* InputIdPassWordView.swift in Sources */
,
D52C2C0A2B91BF24003B286C
/* InformationView.swift in Sources */
,
D52D213A2AEBAC0500324D58
/* HttpRequestType.swift in Sources */
,
D5F969BD2E8AAD4F005662B0
/* VideoQualityPreset.swift in Sources */
,
D53B97262B341867000B3D29
/* AboutAppView.swift in Sources */
,
D5258CA12B03593500365276
/* SessionMonitoringRoute.swift in Sources */
,
D5E03A672B04484D00D65FCE
/* SessionTaskList.swift in Sources */
,
...
...
Seilassist/Sailassist/Chat/Imagepicker.swift
View file @
65b05c9e
//
// Imagepicker.swift
// Sailassist
//
// Created by 三浦薫巳 on 2024/01/19.
//
import
SwiftUI
import
AVFoundation
struct
Imagepicker
:
UIViewControllerRepresentable
{
@Binding
var
show
:
Bool
...
...
@@ -30,6 +25,7 @@ struct Imagepicker : UIViewControllerRepresentable {
class
Coordinator
:
NSObject
,
UIImagePickerControllerDelegate
,
UINavigationControllerDelegate
{
var
parent
:
Imagepicker
var
selectedPreset
:
VideoQualityPreset
=
.
medium
init
(
parent
:
Imagepicker
){
self
.
parent
=
parent
...
...
@@ -40,7 +36,7 @@ struct Imagepicker : UIViewControllerRepresentable {
self
.
parent
.
show
.
toggle
()
}
//MARK: - Use Photo
//MARK: - Use Photo
/ Movie
func
imagePickerController
(
_
picker
:
UIImagePickerController
,
didFinishPickingMediaWithInfo
info
:
[
UIImagePickerController
.
InfoKey
:
Any
])
{
if
let
image
=
info
[
.
originalImage
]
as?
UIImage
{
//MARK: - 画像データのリサイズ
...
...
@@ -55,29 +51,26 @@ struct Imagepicker : UIViewControllerRepresentable {
}
}
else
{
DispatchQueue
.
main
.
async
{
print
(
"Failed to resize or compress image"
)
print
(
debug
:
"Failed to resize or compress image"
)
self
.
parent
.
show
.
toggle
()
}
}
}
}
else
if
let
videoURL
=
info
[
.
mediaURL
]
as?
URL
{
//MARK: - 動画データのリサイズ
DispatchQueue
.
global
(
qos
:
.
userInitiated
)
.
async
{
if
let
videoData
=
try
?
Data
(
contentsOf
:
videoURL
)
{
DispatchQueue
.
main
.
async
{
self
.
parent
.
image
=
videoData
self
.
parent
.
show
.
toggle
()
}
}
else
{
DispatchQueue
.
main
.
async
{
print
(
"Failed to process video data"
)
self
.
parent
.
show
.
toggle
()
compressVideo
(
inputURL
:
videoURL
,
preset
:
selectedPreset
)
{
compressedData
in
DispatchQueue
.
main
.
async
{
if
let
data
=
compressedData
{
self
.
parent
.
image
=
data
}
else
{
print
(
debug
:
"Failed to compress video"
)
}
self
.
parent
.
show
.
toggle
()
}
}
}
else
{
DispatchQueue
.
main
.
async
{
print
(
"No valid media selected"
)
print
(
debug
:
"No valid media selected"
)
self
.
parent
.
show
.
toggle
()
}
}
...
...
Seilassist/Sailassist/Chat/VideoQualityPreset.swift
0 → 100644
View file @
65b05c9e
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
}
Seilassist/Sailassist/Chat/View/CameraView.swift
View file @
65b05c9e
...
...
@@ -4,7 +4,6 @@
//
// Created by 三浦薫巳 on 2024/01/19.
//
import
SwiftUI
struct
CameraView
:
View
{
...
...
Seilassist/Sailassist/Chat/View/ChatInputView.swift
View file @
65b05c9e
...
...
@@ -136,6 +136,8 @@ struct ChatInputView: View {
if
let
image
=
UIImage
(
data
:
imageData
)
{
viewModel
.
sendImageToTemporary
(
image
)
}
else
if
!
imageData
.
isEmpty
{
viewModel
.
sendVideoToTemporary
(
imageData
)
}
})
{
CameraView
(
imageData
:
$
imageData
,
source
:
$
source
,
isActionSheet
:
.
constant
(
false
),
isImagePicker
:
$
viewModel
.
isImagePickerPresented
)
...
...
@@ -206,6 +208,21 @@ struct ChatInputView: View {
}
message
:
{
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?"
)
}
}
}
...
...
Seilassist/Sailassist/Chat/View/MyChatContentView.swift
View file @
65b05c9e
...
...
@@ -6,67 +6,79 @@
//
import
SwiftUI
import
AVKit
struct
MyChatContentView
:
View
{
@State
private
var
isVideoPlayerPresented
=
false
var
message
:
ChatMessage
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
var
body
:
some
View
{
HStack
{
Spacer
()
VStack
(
alignment
:
.
trailing
,
spacing
:
6
)
{
Group
{
if
let
msg
=
message
.
message
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
}
else
if
msg
.
contains
(
".mp4"
)
||
msg
.
contains
(
".mov"
)
{
ChatUrlVideoView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
}
else
{
Text
(
msg
)
.
font
(
FontStyle
.
DefaultText
.
font
)
.
foregroundColor
(
ColorSet
.
BodyChat
.
color
)
.
padding
(
15
)
.
background
(
ColorSet
.
ChatBaloon
.
color
)
.
border
(
Color
.
red
.
gradient
.
opacity
(
0.8
),
width
:
(
message
.
mode
==
ChatMode
.
warningProgress
.
rawValue
?
2
:
0
))
.
clipShape
(
.
rect
(
topLeadingRadius
:
10
,
bottomLeadingRadius
:
0
,
bottomTrailingRadius
:
10
,
topTrailingRadius
:
10
))
}
Spacer
()
VStack
(
alignment
:
.
trailing
,
spacing
:
6
)
{
Group
{
if
let
msg
=
message
.
message
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
// .onTapGesture {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// isVideoPlayerPresented = true
// }
// }
}
else
{
Text
(
msg
)
.
font
(
FontStyle
.
DefaultText
.
font
)
.
foregroundColor
(
ColorSet
.
BodyChat
.
color
)
.
padding
(
15
)
.
background
(
ColorSet
.
ChatBaloon
.
color
)
.
border
(
Color
.
red
.
gradient
.
opacity
(
0.8
),
width
:
(
message
.
mode
==
ChatMode
.
warningProgress
.
rawValue
?
2
:
0
))
.
clipShape
(
.
rect
(
topLeadingRadius
:
10
,
bottomLeadingRadius
:
0
,
bottomTrailingRadius
:
10
,
topTrailingRadius
:
10
))
}
}
}
HStack
(
spacing
:
5
){
//MARK: - 既読マーク
Text
(
DateTextLib
.
ISO86012FormatText
(
message
.
time
,
format
:
"yyyy/MM/dd HH:mm"
,
errFormat
:
""
))
.
padding
(
.
trailing
,
8
)
HStack
(
spacing
:
5
){
//MARK: - 既読マーク
Text
(
DateTextLib
.
ISO86012FormatText
(
message
.
time
,
format
:
"yyyy/MM/dd HH:mm"
,
errFormat
:
""
))
.
padding
(
.
trailing
,
8
)
let
viewerCnt
=
viewerCnt
()
let
viewerCnt
=
viewerCnt
()
Image
(
"chat_company"
)
.
resizable
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
Image
(
"chat_company"
)
.
resizable
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
Text
(
String
(
viewerCnt
.
1
))
Text
(
String
(
viewerCnt
.
1
))
Image
(
"chat_ship"
)
.
resizable
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
Image
(
"chat_ship"
)
.
resizable
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
Text
(
String
(
viewerCnt
.
0
))
}
.
font
(
FontStyle
.
DateText
.
font
)
.
foregroundColor
(
ColorSet
.
ChatDate
.
color
)
Text
(
String
(
viewerCnt
.
0
))
}
.
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
)
{
...
...
@@ -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 {
MyChatContentView
(
message
:
ChatMessage
(
shipId
:
10000003
,
...
...
Seilassist/Sailassist/Chat/View/OtherChatContentView.swift
View file @
65b05c9e
...
...
@@ -4,10 +4,11 @@
//
// Created by Mamoru Sugita on 2023/11/09.
//
import
SwiftUI
import
AVKit
struct
OtherChatContentView
:
View
{
@State
private
var
isVideoPlayerPresented
=
false
var
message
:
ChatMessage
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
...
...
@@ -24,8 +25,13 @@ struct OtherChatContentView: View {
if
let
msg
=
message
.
message
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
}
else
if
msg
.
contains
(
".mp4"
)
||
msg
.
contains
(
".mov"
)
{
ChatUrlVideoView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
// .onTapGesture {
// if message.type == 3,
// let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// isVideoPlayerPresented = true
// }
// }
}
else
{
Text
(
msg
)
.
font
(
FontStyle
.
DefaultText
.
font
)
...
...
@@ -54,6 +60,12 @@ struct OtherChatContentView: View {
.
padding
(
.
leading
,
20
)
Spacer
()
}
// .fullScreenCover(isPresented: $isVideoPlayerPresented) {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// VideoPlayerView(videoURL: url)
// }
// }
}
}
...
...
Seilassist/Sailassist/Chat/ViewModel/ChatInputViewModel.swift
View file @
65b05c9e
import
SwiftUI
import
Combine
import
Speech
import
AVFoundation
import
UIKit
enum
MediaInputType
{
case
none
...
...
@@ -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
func
sendChatMessage
()
{
guard
!
SharingData
.
message
.
sendInf
else
{
return
}
...
...
@@ -109,14 +87,62 @@ class ChatInputViewModel: ObservableObject {
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
?
{
let
fileName
=
UUID
()
.
uuidString
+
".jpg"
let
tempDir
=
FileManager
.
default
.
temporaryDirectory
let
fileURL
=
tempDir
.
appendingPathComponent
(
fileName
)
guard
let
data
=
image
.
jpegData
(
compressionQuality
:
0.8
)
else
{
print
(
"Image JPEG conversion failed"
)
print
(
debug
:
"Image JPEG conversion failed"
)
return
nil
}
...
...
@@ -124,7 +150,7 @@ class ChatInputViewModel: ObservableObject {
try
data
.
write
(
to
:
fileURL
)
return
fileURL
.
absoluteString
}
catch
{
print
(
"Image JPEG conversion failed:
\(
error
.
localizedDescription
)
"
)
print
(
debug
:
"Image JPEG conversion failed:
\(
error
.
localizedDescription
)
"
)
return
nil
}
}
...
...
@@ -167,4 +193,75 @@ class ChatInputViewModel: ObservableObject {
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
)
}
}
}
Seilassist/Sailassist/Json/ResGetMessages.swift
View file @
65b05c9e
...
...
@@ -28,7 +28,10 @@ struct ChatMessage: Codable {
var
fromId
:
String
?
//ユーザーID
var
mode
:
Int
// 0:通常 , 1:Warning中
var
message
:
String
?
//テキスト時:テキスト , 画像時:サムネイルのUri
// var videoUrl: String? //TODO: - テスト
var
stampId
:
Int
//スタンプ番号 0:Fire~
// var Latitude: Double? //TODO: - テスト
// var Longitude: Double? //TODO: - テスト
var
viewer
:
[
Viewer
]
=
[]
//閲覧者情報
}
...
...
Seilassist/Sailassist/SailassistApp.swift
View file @
65b05c9e
...
...
@@ -218,7 +218,6 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
if
existingMsg
.
message
!.
hasPrefix
(
"file://"
)
{
updatedMsg
.
message
=
existingMsg
.
message
}
self
.
msg
.
messages
[
index
]
=
updatedMsg
}
else
{
self
.
msg
.
messages
.
append
(
newMsg
)
...
...
Seilassist/Sailassist/ServerSession/SessionUploadImage.swift
View file @
65b05c9e
...
...
@@ -28,9 +28,7 @@ class SessionUploadImage : ObservableObject {
//MARK: - 画像のアップロード
func
requestUploadImage
(
_
uploadImage
:
ReqUploadImage
)
async
throws
->
Data
{
print
(
debug
:
"called"
)
guard
!
Calling
else
{
throw
APIError
.
busy
}
guard
!
Calling
else
{
throw
APIError
.
busy
}
Calling
=
true
defer
{
Calling
=
false
}
...
...
@@ -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
{
var
request
=
URLRequest
(
url
:
url
)
request
.
httpMethod
=
"POST"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment