This repository has been archived by the owner on Jul 28, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
/
TimelineMode.hs
558 lines (537 loc) · 19.1 KB
/
TimelineMode.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module Komposition.Application.TimelineMode where
import Komposition.Application.Base
import qualified Prelude
import Control.Effect (Member)
import Control.Effect.Carrier (Carrier)
import Control.Lens
import qualified Data.List.NonEmpty as NonEmpty
import Data.Row.Records hiding (split)
import Data.String (fromString)
import System.FilePath ((</>))
import Komposition.Application.Form
import Komposition.Composition
import Komposition.Composition.Delete
import Komposition.Composition.Insert
import Komposition.Composition.Paste
import Komposition.Composition.Split
import Komposition.Duration
import Komposition.Focus
import Komposition.History
import Komposition.Import.Audio
import Komposition.Import.Video
import Komposition.Library
import Komposition.MediaType
import Komposition.Project
import Komposition.Project.Store
import Komposition.Render
import qualified Komposition.Render.Composition as Render
import qualified Komposition.Render.FFmpeg as FFmpeg
import Komposition.UserInterface.Dialog
import Komposition.UserInterface.Help
import Komposition.VideoSettings
import Komposition.Application.ImportMode
import Komposition.Application.KeyMaps
import Komposition.Application.LibraryMode
data TimelineModeResult
= TimelineExit TimelineModel
| TimelineClose
type TimelineEffects sig =
( Member ProjectStore sig
, Member VideoImport sig
, Member AudioImport sig
, Member Render sig
)
timelineMode
:: ( Application t m sig
, TimelineEffects sig
, Carrier sig m
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> t m r r TimelineModeResult
timelineMode gui model = do
patchWindow gui (timelineView model)
nextEventOrTimeout gui 5 >>= maybe resetStatusMessage onNextEvent
where
continue = timelineMode gui model
continueWithStatusMessage msg =
model & statusMessage ?~ msg & timelineMode gui
resetStatusMessage = model & statusMessage .~ Nothing & timelineMode gui
onNextEvent = \case
CommandKeyMappedEvent (FocusCommand cmd) ->
case
modifyFocus (currentProject model ^. timeline)
cmd
(model ^. currentFocus)
of
Left err -> do
beep gui
printUnexpectedFocusError err cmd
continue
Right newFocus ->
model
& currentFocus .~ newFocus
& refreshPreviewAndContinue gui
CommandKeyMappedEvent (JumpFocus newFocus) ->
case atFocus newFocus (currentProject model ^. timeline) of
Just _ -> refreshPreviewAndContinue gui (model & currentFocus .~ newFocus)
Nothing ->
beep gui >>> continueWithStatusMessage "Couldn't set focus."
CommandKeyMappedEvent (InsertCommand type' position) ->
insertIntoTimeline gui model type' position
CommandKeyMappedEvent Delete -> deleteFocused gui model
CommandKeyMappedEvent Copy ->
model
& clipboard
.~ atFocus (model ^. currentFocus) (currentProject model ^. timeline)
& timelineMode gui
CommandKeyMappedEvent (Paste pos) -> case model ^. clipboard of
Nothing -> beep gui >>> continue
Just cb ->
case
paste (model ^. currentFocus)
cb
pos
(currentProject model ^. timeline)
of
Just timeline' ->
model
& existingProject
. projectHistory
%~ edit (timeline .~ timeline')
& refreshPreviewAndContinue gui
Nothing ->
beep gui >> continueWithStatusMessage "Couldn't paste."
CommandKeyMappedEvent Split ->
case split (model ^. currentFocus) (currentProject model ^. timeline) of
Just (timeline', newFocus) ->
model
& existingProject
. projectHistory
%~ edit (timeline .~ timeline')
& currentFocus
.~ newFocus
& refreshPreviewAndContinue gui
Nothing -> do
beep gui
continueWithStatusMessage
"Can't split composition at current focus."
CommandKeyMappedEvent Import ->
selectFileToImport >>>= addImportedAssetsToLibrary gui model
CommandKeyMappedEvent Render ->
case Render.flattenTimeline (currentProject model ^. timeline) of
Just flat -> do
outDir <- ilift getDefaultProjectsDirectory
chooseFile gui (Save File) "Render To File" outDir >>>= \case
Just outFile -> do
stream <- ilift $ renderComposition
(currentProject model ^. videoSettings . renderVideoSettings)
VideoTranscoded
(FileOutput outFile)
flat
progressBar gui "Rendering" stream >>= \case
Just (Right ()) -> continue
Just (Left (SomeException err)) ->
ilift (logLnShow Error err) >>> continue
Nothing -> continue
Nothing -> continue
Nothing -> do
beep gui
continueWithStatusMessage
"Cannot render a composition without video clips."
CommandKeyMappedEvent Preview ->
previewFocusedComposition gui model >>> continue
CommandKeyMappedEvent Undo ->
case model & existingProject . projectHistory %%~ undo of
Just m -> refreshPreviewAndContinue gui m
Nothing -> beep gui >> timelineMode gui model
CommandKeyMappedEvent Redo ->
case model & existingProject . projectHistory %%~ redo of
Just m -> refreshPreviewAndContinue gui m
Nothing -> beep gui >> timelineMode gui model
CommandKeyMappedEvent SaveProject ->
ilift (saveExistingProject (model ^. existingProject)) >>= \case
_ -> continue
CommandKeyMappedEvent CloseProject -> ireturn TimelineClose
CommandKeyMappedEvent Cancel -> continue
CommandKeyMappedEvent Help ->
help gui [ModeKeyMap STimelineMode (keymaps STimelineMode)] >>>= \case
Just HelpClosed -> continue
Nothing -> continue
CommandKeyMappedEvent Exit -> ireturn (TimelineExit model)
ZoomLevelChanged zl -> model & zoomLevel .~ zl & timelineMode gui
PreviewImageRefreshed p -> model & previewImagePath .~ p & timelineMode gui
FocusedClipSpeedSet speed ->
model
& modifyFocusedVideoPart (\case
VideoClip ann asset ts _ -> VideoClip ann asset ts speed
vg@VideoGap{} -> vg)
& refreshPreviewAndContinue gui
FocusedClipStartSet start ->
model
& modifyFocusedVideoPart (\case
VideoClip ann asset ts speed ->
VideoClip ann asset ts { spanStart = start } speed
vg@VideoGap{} -> vg)
& refreshPreviewAndContinue gui
FocusedClipEndSet end ->
model
& modifyFocusedVideoPart (\case
VideoClip ann asset ts speed ->
VideoClip ann asset ts { spanEnd = end } speed
vg@VideoGap{} -> vg)
& refreshPreviewAndContinue gui
WindowClosed -> ireturn (TimelineExit model)
printUnexpectedFocusError err cmd = case err of
UnhandledFocusModification{} ->
ilift
(logLnText Warning
("Could not handle focus modification: " <> show cmd)
)
_ -> ireturn ()
insertIntoTimeline
:: ( Application t m sig
, TimelineEffects sig
, Carrier sig m
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> InsertType
-> InsertPosition
-> t m r r TimelineModeResult
insertIntoTimeline gui model type' position =
case
( type'
, atFocus (model ^. currentFocus) (currentProject model ^. timeline)
)
of
(InsertComposition, Just (SomeSequence _)) ->
model
& existingProject
. projectHistory
%~ edit
( timeline
%~ insert_ (model ^. currentFocus)
(InsertParallel (Parallel () [] []))
RightOf
)
& refreshPreviewAndContinue gui
(InsertClip (Just mt), Just SomeParallel{}) -> case mt of
Video -> selectAssetAndInsert gui model SVideo position
Audio -> selectAssetAndInsert gui model SAudio position
(InsertClip Nothing, Just SomeVideoPart{}) ->
selectAssetAndInsert gui model SVideo position
(InsertClip Nothing, Just SomeAudioPart{}) ->
selectAssetAndInsert gui model SAudio position
(InsertGap (Just mt), Just SomeParallel{}) -> case mt of
Video -> insertGap gui model SVideo position >>>= refreshPreviewAndContinue gui
Audio -> insertGap gui model SAudio position >>>= refreshPreviewAndContinue gui
(InsertGap Nothing, Just SomeVideoPart{}) ->
insertGap gui model SVideo position >>>= refreshPreviewAndContinue gui
(InsertGap Nothing, Just SomeAudioPart{}) ->
insertGap gui model SAudio position >>>= refreshPreviewAndContinue gui
(c, Just f) -> do
let
msg =
"Cannot perform "
<> show c
<> " when focused at "
<> prettyFocusedAt f
timelineMode gui (model & statusMessage ?~ msg)
(_, Nothing) -> do
ilift (logLnText Warning "Focus is invalid.")
continue
where continue = timelineMode gui model
insertGap
:: (Application t m sig, HasType parent (Window (t m) parentEvent) r)
=> Name parent
-> TimelineModel
-> SMediaType mt
-> InsertPosition
-> t m r r TimelineModel
insertGap parent model mediaType' position = do
gapDuration <- prompt parent
"Insert Gap"
"Please specify a gap duration in seconds."
"Insert Gap"
(PromptNumber (0.1, 10e10, 0.1))
let gapInsertion seconds = case mediaType' of
SVideo -> InsertVideoParts [VideoGap () (durationFromSeconds seconds)]
SAudio -> InsertAudioParts [AudioGap () (durationFromSeconds seconds)]
case gapDuration of
Just seconds ->
model
& existingProject
. projectHistory
%~ edit
( timeline
%~ insert_ (model ^. currentFocus)
(gapInsertion seconds)
position
)
& ireturn
Nothing -> ireturn model
prettyFocusedAt :: FocusedAt a -> Text
prettyFocusedAt = \case
SomeSequence{} -> "sequence"
SomeParallel{} -> "parallel"
SomeVideoPart{} -> "video track"
SomeAudioPart{} -> "audio track"
previewFocusedComposition
:: ( Application t m sig
, HasType n (Window (t m) e) r
, Carrier sig m
, TimelineEffects sig
)
=> Name n
-> TimelineModel
-> t m r r TimelineModel
previewFocusedComposition gui model = case flatComposition of
Just flat -> do
streamingProcess <- ilift $ renderComposition
(currentProject model ^. videoSettings . proxyVideoSettings)
VideoProxy
(HttpStreamingOutput "localhost" 12345)
flat
_ <- previewStream
gui
"http://localhost:12345"
streamingProcess
(currentProject model ^. videoSettings . proxyVideoSettings)
ireturn model
Nothing -> do
beep gui
model
& statusMessage
?~ "Cannot preview a composition without video clips."
& ireturn
where
flatComposition :: Maybe Render.Composition
flatComposition =
atFocus (model ^. currentFocus) (currentProject model ^. timeline)
Prelude.>>= \case
SomeSequence s -> Render.flattenSequence s
SomeParallel p -> Render.flattenParallel p
_ -> Nothing
noAssetsMessage :: SMediaType mt -> Text
noAssetsMessage mt =
"You have no "
<> mt'
<> " assets in your library. Use 'Import' to add some assets."
where
mt' = case mt of
SVideo -> "video"
SAudio -> "audio"
selectAssetAndInsert
:: ( Application t m sig
, TimelineEffects sig
, Carrier sig m
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> SMediaType mt
-> InsertPosition
-> t m r r TimelineModeResult
selectAssetAndInsert gui model mediaType' position = case mediaType' of
SVideo ->
case NonEmpty.nonEmpty (currentProject model ^. library . videoAssets) of
Just vs ->
selectAsset (SelectAssetsModel SVideo vs [])
>>>= insertSelectedAssets gui model SVideo position
Nothing -> onNoAssets gui SVideo
SAudio ->
case NonEmpty.nonEmpty (currentProject model ^. library . audioAssets) of
Just as ->
selectAsset (SelectAssetsModel SAudio as [])
>>>= insertSelectedAssets gui model SAudio position
Nothing -> onNoAssets gui SAudio
where
onNoAssets
:: ( Application t m sig
, TimelineEffects sig
, Carrier sig m
, r ~ (n .== Window (t m) (Event 'TimelineMode))
, IxPointed (t m)
)
=> Name n
-> SMediaType mt
-> t m r r TimelineModeResult
onNoAssets gui' mt = do
beep gui'
model & statusMessage ?~ noAssetsMessage mt & timelineMode gui'
insertSelectedAssets
:: ( Application t m sig
, Carrier sig m
, TimelineEffects sig
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> SMediaType mt
-> InsertPosition
-> Maybe [Asset mt]
-> t m r r TimelineModeResult
insertSelectedAssets gui model mediaType' position result = do
model' <- case result of
Just assets ->
model
& existingProject
. projectHistory
%~ edit
(\p -> p & timeline %~ insert_ (model ^. currentFocus) (insertionOf mediaType' assets) position)
& ireturn
Nothing -> do
beep gui
model & statusMessage ?~ noAssetsMessage mediaType' & ireturn
refreshPreviewAndContinue gui model'
insertionOf
:: SMediaType mt
-> [Asset mt]
-> Insertion ()
insertionOf SVideo a = InsertVideoParts (toVideoClip <$> a)
where
toVideoClip videoAsset =
let ts = maybe (TimeSpan 0 (durationOf OriginalDuration videoAsset))
snd
(videoAsset ^. videoClassifiedScene)
speed = videoAsset ^. videoSpeed
in VideoClip () videoAsset ts speed
insertionOf SAudio a = InsertAudioParts (AudioClip () <$> a)
addImportedAssetsToLibrary
:: ( Application t m sig
, Carrier sig m
, TimelineEffects sig
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> Maybe (ImportFileForm Valid)
-> t m r r TimelineModeResult
addImportedAssetsToLibrary gui model (Just selected) = do
model' <-
importSelectedFile gui (model ^. existingProject) selected >>>= \case
Just (Left err) -> do
ilift (logLnShow Error err)
_ <- dialog
gui
DialogProperties
{ dialogTitle = "Import Failed!"
, dialogMessage = show err
, dialogChoices = [Ok]
}
ireturn model
Just (Right (Left vs)) ->
model
& existingProject
. projectHistory
%~ edit (library . videoAssets %~ (<> vs))
& ireturn
Just (Right (Right as)) ->
model
& existingProject
. projectHistory
%~ edit (library . audioAssets %~ (<> as))
& ireturn
Nothing -> ireturn model
timelineMode gui model'
addImportedAssetsToLibrary gui model Nothing = timelineMode gui model
deleteFocused
:: ( Application t m sig
, Carrier sig m
, TimelineEffects sig
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> t m r r TimelineModeResult
deleteFocused gui model =
case delete (model ^. currentFocus) (currentProject model ^. timeline) of
Nothing -> beep gui >> continueWithStatusMessage "Delete failed."
Just (DeletionResult timeline' deleted (Just cmd)) ->
case
modifyFocus (currentProject model ^. timeline)
cmd
(model ^. currentFocus)
of
Left err -> do
beep gui
ilift (logLnText Error ("Deleting failed: " <> show err))
continueWithStatusMessage "Delete failed."
Right newFocus ->
model
& existingProject
. projectHistory
%~ edit (timeline .~ timeline')
& currentFocus
.~ newFocus
& clipboard
?~ deleted
& refreshPreviewAndContinue gui
Just (DeletionResult timeline' deleted Nothing) ->
model
& existingProject
. projectHistory
%~ edit (timeline .~ timeline')
& clipboard
?~ deleted
& refreshPreviewAndContinue gui
where
continueWithStatusMessage msg =
model & statusMessage ?~ msg & timelineMode gui
refreshPreview
:: ( Application t m sig
, Carrier sig m
, TimelineEffects sig
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> t m r r ()
refreshPreview gui model = do
cacheDir <- ilift getCacheDirectory
case atFocus (model ^. currentFocus) (currentProject model ^. timeline) of
Just (SomeVideoPart (VideoClip _ videoAsset ts _)) ->
runInBackground gui $
pure . PreviewImageRefreshed . Just <$>
FFmpeg.extractFrameToFile'
(currentProject model ^. videoSettings . proxyVideoSettings)
Render.FirstFrame
VideoProxy
videoAsset
ts
(cacheDir </> "preview-frame")
_ -> runInBackground gui (pure (pure (PreviewImageRefreshed Nothing)))
refreshPreviewAndContinue
:: ( Application t m sig
, Carrier sig m
, TimelineEffects sig
, r ~ (n .== Window (t m) (Event TimelineMode))
)
=> Name n
-> TimelineModel
-> t m r r TimelineModeResult
refreshPreviewAndContinue gui model = do
refreshPreview gui model
timelineMode gui model
modifyFocusedVideoPart
:: (VideoPart () -> VideoPart ()) -> TimelineModel -> TimelineModel
modifyFocusedVideoPart f model =
model
& existingProject
. projectHistory
%~ edit (timeline . focusing (model ^. currentFocus) %~ f)