Compare commits

..

331 Commits
0.6.0 ... 0.6.3

Author SHA1 Message Date
Bruno Herbelin
109e6f590a Disabling vtenc streaming for OSX (could not make it work) 2022-01-04 14:37:56 +01:00
Bruno Herbelin
8f0491ea57 Video Streamer with H264 hardware acceleration
Simplified option in user menu; lower bandwidth with H264, otherwise use JPEG. Always use RAW for localhost.
2022-01-04 12:33:46 +01:00
Bruno Herbelin
e0522608a4 UI improvement; hovering icons indicate possible action
Source filters icons without button. Unified lock icon with view. Updated help.
2022-01-04 00:54:12 +01:00
Bruno Herbelin
4b9a230803 oops 2022-01-03 22:16:55 +01:00
Bruno Herbelin
fc5246efaa New & improved align and distribute in MixingView 2022-01-03 18:20:02 +01:00
Bruno Herbelin
4eebfbb89f Improved Glyph layout
Support for shape and placement of glyph
2022-01-03 17:16:55 +01:00
Bruno Herbelin
353d2c4744 New Glyph decoration to show initials of source 2022-01-03 15:41:08 +01:00
Bruno Herbelin
2718e83132 Happy new year 2022 2022-01-02 23:17:22 +01:00
Bruno Herbelin
7547d1179d Cleanup UI
Ensure icons are dedicated to one single thing.
2022-01-02 19:54:48 +01:00
Bruno Herbelin
61e89286bc Fixed Device manager: restore gmainloop
The gmainloop is used by gst to detect devices. Fixed bugs on detection of invalid devices.
2022-01-02 14:17:10 +01:00
Bruno Herbelin
53ae715816 Restart the source after insertion from New Source panel 2022-01-02 11:49:02 +01:00
Bruno Herbelin
8cb37dba36 ORDER change: add sources at the end when create
Inserting sources at front was changing their index every time, which broke OSC addressing
2022-01-02 11:41:47 +01:00
Bruno Herbelin
4426f70de7 Code editor for Custom pattern gstreamer
Bugfix in Streamsource and UI
2022-01-01 23:59:30 +01:00
Bruno Herbelin
f0ca13150f New Custom pattern entry for New Source 2022-01-01 10:14:51 +01:00
Bruno Herbelin
780a20689c Improved user message for stream discovery failure 2022-01-01 10:13:46 +01:00
Bruno Herbelin
28f9ed1d8d Cleanup & new unwrapped function 2022-01-01 10:12:50 +01:00
Bruno Herbelin
2b5b8ad02c Bugfix Stream timeout initialization test 2021-12-31 14:24:51 +01:00
Bruno Herbelin
d5092b1765 Save & load GenericStrreamSource 2021-12-31 13:30:51 +01:00
Bruno Herbelin
fda62314f9 UI integration of GenericStreamSource 2021-12-31 13:16:39 +01:00
Bruno Herbelin
17018c137f MultiLine text display 2021-12-31 13:16:16 +01:00
Bruno Herbelin
8838c19c39 String functions to wrap test or join lists 2021-12-31 13:16:00 +01:00
Bruno Herbelin
f02a99a4e2 Improved GenericStreamSource, with stream discoverer
Also timeout to fail if open does not works + new GST icon.
2021-12-31 13:15:23 +01:00
Bruno Herbelin
7b26b0f23e Unified IMGUI_SAME_LINE width 2021-12-31 13:13:29 +01:00
Bruno Herbelin
0e9984827a Cleanup gst_element_get_state 2021-12-31 13:12:12 +01:00
Bruno Herbelin
033d41863a Added Stream Discoverer to detect frame size from a gstreamer pipeline
Previous use of Stream are not affected (the discoverer is passively returning the given width and height). But if the Stream is created without dimensions, it will run a discoverer to try to get preroll frames and detect width and height from there.
2021-12-30 00:15:43 +01:00
Bruno Herbelin
bc540044ac Accept launch of vimix if OSC connection failed 2021-12-29 14:41:27 +01:00
Bruno Herbelin
76a2535da3 Fixed issue of low quality stream in JPEG: new default to RGB RAW RTP stream
Backward compatibility through menu un stream output view (allow selecting JPEG)
2021-12-29 14:37:56 +01:00
Bruno Herbelin
ff48877d16 Fixed OSC feedback after source change. Added OSC command to lock source. 2021-12-27 23:36:28 +01:00
Bruno Herbelin
4b8efabc5f Improve and cleanup OSC control and translation
Changed default send Port to 7001. Updated documentation.
2021-12-27 17:28:11 +01:00
Bruno Herbelin
c79be090df Implementation of OSC settings and translator
Translations are in a config xml file in settings directory, and can be directly edited in text by the user. Settings UI allows changing Ports for incoming and outgoing UDP.
2021-12-27 01:04:49 +01:00
Bruno Herbelin
626eab7e8f Update and advertising of TouchOSC layout 2021-12-27 01:02:43 +01:00
Bruno Herbelin
c103b7d883 Finalization of OSC API, Tutorial for TouchOSC 2021-12-26 14:44:07 +01:00
Bruno Herbelin
cde055e29b Implementation of Session control
With Session recall from OSC
2021-12-26 01:20:44 +01:00
Bruno Herbelin
1cb448c42e Output session fading fixed for OSC and animation.
Linear interpolation (instead of dichotomy converge) for fading at Session update. Mixing View update reads value of session fading to animate the cursor (which was preventing other manipulation of fading). Cleanup fading in OSC controller, with animation options and fade-in and fade-out controls.
2021-12-26 00:41:02 +01:00
Bruno Herbelin
3d05444f30 Improved OSC control with TouchOSC
Added Looming source callback, and cleanup sync of sources. New horizontal version of OSCTouch UI.
2021-12-25 16:05:43 +01:00
Bruno Herbelin
7a551189d9 Improved log of OSC message. 2021-12-25 00:41:51 +01:00
Bruno Herbelin
b885e70fed Remove spaces from Source name
Replace space by underscore
2021-12-25 00:41:24 +01:00
Bruno Herbelin
0a27c14041 Control manager and TouchOSC sync 2021-12-23 22:17:05 +01:00
Bruno Herbelin
eb8e33e311 Correct call to Source Activation (inheritance) 2021-12-23 22:16:16 +01:00
Bruno Herbelin
2d44a60b90 Bi-directional OSC communication for Control manager
Unified OSC message declaration with Communicator
2021-12-21 23:48:20 +01:00
Bruno Herbelin
135b6a5702 cleanup SourceCallbacks on source destructor 2021-12-21 00:19:55 +01:00
Bruno Herbelin
706c72fda8 More OSC control
Grab and resize dynamically, select source by index, etc.
2021-12-21 00:19:39 +01:00
Bruno Herbelin
fb7bdba388 Code cleanup 2021-12-20 00:30:59 +01:00
Bruno Herbelin
733d08638d Control manager thread save with SourceCallbacks 2021-12-20 00:30:50 +01:00
Bruno Herbelin
cb3cca8a64 catchup previous commits 2021-12-20 00:29:57 +01:00
Bruno Herbelin
a3a581794e Node update callbacks do not need to be disabled 2021-12-20 00:26:08 +01:00
Bruno Herbelin
f921e7610c New mechanism for source update with callbacks
Similarly to Node update callbacks, sources now have SourceCallbacks called at the start of each update. Several SourceCallback are implemented to ensure thread safe update of more complex properties (mixing alpha, depth, etc.).
2021-12-20 00:25:42 +01:00
Bruno Herbelin
8deb364025 Cleanup of main update calbacks
Clarify update and draw of rendering manager by using callbacks (instead of hidden calls in draw method).
2021-12-19 01:12:25 +01:00
Bruno Herbelin
3a9c6f56bf Work in progress OSC Control manager
Support for log, output and source targets. Now needs to be developed for all attributes.
2021-12-19 01:11:29 +01:00
Bruno Herbelin
a612154123 Initial implementation of Control manager
Control manager will handle control actions, recorded or from OSC. Here skeleton for receiving OSC messages is in place. Cleanup of includes for NetworkToolkit. Touched a bit the BaseToolkit.
2021-12-18 16:02:37 +01:00
Bruno Herbelin
bbc5e50491 bugfix show Player when clic source 2021-12-18 10:18:36 +01:00
Bruno Herbelin
a7689a8f54 Help window, setting to show/hide Tooltips
Menu and keyboard shortcut declaration centralized. List of all keyboard shortcuts. ImGuiToolkit unified tooltips.
2021-12-12 23:12:56 +01:00
Bruno Herbelin
731a1af1a6 Defines for ImGuiToolkit icons for source. 2021-12-12 23:10:07 +01:00
Bruno Herbelin
f53ebd4389 BugFix crash on close Player window 2021-12-11 21:35:23 +01:00
Bruno Herbelin
baa6ddb401 Implementation of user defined mixing deactivation limit
Mixing view handles to grab and scale limbo area. Saving of user defined limit in Session (and snapshot). Testing for source activation outside of update during session update loop.
2021-12-08 23:55:27 +01:00
Bruno Herbelin
315a8534d5 Store output PNG capture in list of recent recordings 2021-12-06 12:35:09 +01:00
Bruno Herbelin
d77bd4034d Improved UI tooltips 2021-12-06 12:29:22 +01:00
Bruno Herbelin
fa71797ed2 Unified implementation saving and loading settings history files 2021-12-06 11:41:03 +01:00
Bruno Herbelin
8c63552573 Global settings for Save and continue auto-preload
Added configuration for recent recording list. Added tooltip for filename in list.
2021-12-06 11:16:47 +01:00
Bruno Herbelin
a18d53c637 Improved Save and continue recording
When triggered from menu, prepare the UI for next openning of the new source pannel
2021-12-06 00:16:53 +01:00
Bruno Herbelin
ffe05368e8 Update New Source panel for Media
Added list of recent files, recent recordings, and folders list of media files. All saved in settings. Connect list of recent recordings with recorder.
2021-12-05 18:41:58 +01:00
Bruno Herbelin
923d84f378 Unified SystemToolkit list directory with dialog file patterns 2021-12-05 18:39:58 +01:00
Bruno Herbelin
e5334aae0a Cleaner file patterns for dialogs 2021-12-05 18:33:53 +01:00
Bruno Herbelin
4675be7e2a Unified notification of source creation with Source info
New virtual function source::info used for notification after adding source
2021-12-05 18:32:23 +01:00
Bruno Herbelin
bf3fc61ef7 Bad idea: do not use nvidia hardware decoder for jpeg
just crashes without reason and not very useful
2021-12-04 23:05:12 +01:00
Bruno Herbelin
ebd9fab312 Improved ordering of hardware decoding and log info 2021-12-04 00:23:27 +01:00
Bruno Herbelin
d359cf33d1 Improved runtime check of hardware encoder
gstreamer toolkit has_feature now tests possible instanciation
2021-12-03 23:36:07 +01:00
Bruno Herbelin
14bab1e299 Runtime selection of hardware GPU encoder
temporary implementation
2021-12-03 01:05:32 +01:00
Bruno Herbelin
4c4ad144b9 Finish Frame grabber after return on failed initialization 2021-12-03 01:05:04 +01:00
Bruno Herbelin
1157c0b1c5 cleanup frame grabber gst timer 2021-12-03 01:03:21 +01:00
Bruno Herbelin
68b2c5e0c1 Frame grabber threaded initialization
Start gstreamer init of frame grabber in a thread and wait future return from initializer before switching to active recording mode.
2021-12-02 11:45:22 +01:00
Bruno Herbelin
b97fd06f2a Bugfix Resolution frame buffer
Set width to power of 2, needed for encoding (even if OpenGL accepts to display it)
2021-12-02 11:40:31 +01:00
Bruno Herbelin
51f0f5bd66 Fixed ending of recording on exit 2021-12-02 09:35:02 +01:00
Bruno Herbelin
66f445997d Preliminary implementation of recording 'save & continue' 2021-12-01 23:05:41 +01:00
Bruno Herbelin
73d4f7c1ea Ensure swap interval 2021-11-28 23:58:01 +01:00
Bruno Herbelin
25fc5562db Unified layout of HelpMarkers 2021-11-28 23:57:33 +01:00
Bruno Herbelin
4d52bcb5b3 Fix glfw set window pos 2021-11-28 20:50:56 +01:00
Bruno Herbelin
3d2de560b0 Timelines of metro-synched media player 2021-11-28 11:36:56 +01:00
Bruno Herbelin
809e30d906 Timeline display in beat unit for synched to metronome 2021-11-27 19:26:33 +01:00
Bruno Herbelin
ef9e41f20d Fixed display of time in minimal string mode 2021-11-27 19:23:51 +01:00
Bruno Herbelin
1b4849f214 Media player synchronicity to beat or phase
Metronome synched play, rewind and step. saving in xml.
2021-11-26 12:22:39 +01:00
Bruno Herbelin
e123d139e4 Introducing modes of Metronome synchronicity 2021-11-24 21:48:51 +01:00
Bruno Herbelin
a8abd52afb Merge remote-tracking branch 'origin/add-code-of-conduct-1' 2021-11-24 20:32:49 +01:00
Bruno Herbelin
091e99f21b New export function for Version of a session 2021-11-24 20:32:29 +01:00
Bruno Herbelin
b6593c2a83 Added date to snapshot
Allows showing date of version
2021-11-24 00:15:40 +01:00
Bruno Herbelin
5ac7887360 Convert Snapshots into Versions of session
Added auto-snapshot on save to have an Iterative Saving mode, and change terminology of 'snapshots' to 'versions' management.
2021-11-23 22:47:44 +01:00
Bruno Herbelin
ed7627af6f Fixed UI spacing proportional to screen DPI 2021-11-23 22:46:22 +01:00
BHBN
1ea9fc54b2 Create CODE_OF_CONDUCT.md 2021-11-22 00:22:18 +01:00
Bruno Herbelin
3819571ec0 Fixed UI display time in readable form 2021-11-21 22:39:11 +01:00
Bruno Herbelin
94f131fc57 Fixed panel window show/hide 2021-11-21 22:10:17 +01:00
Bruno Herbelin
3c20314aab Metronome and Stopwatch User Interface
New Timer window in UI for Metronome (Ableton Link management) and replaces Timers. Former Timers in Metrics are replaced with Runtime (of session, of program and of total vimix runtime in settings). Temporarily disconnected Metronome from MediaPlayer actions.
2021-11-21 16:54:56 +01:00
Bruno Herbelin
1506d36407 Readable GST time is at 1/10th second precision 2021-11-21 16:52:09 +01:00
Bruno Herbelin
aa4b2967c7 Adding a timestamp to Session instanciation
Used to compute runtime of a session
2021-11-21 16:51:19 +01:00
Bruno Herbelin
a42881d31f Texture view slider UI fix 2021-11-21 16:46:18 +01:00
Bruno Herbelin
6a3ff2f235 More freedom of grab translation for all views 2021-11-17 23:11:15 +01:00
Bruno Herbelin
fc4e3dc362 Metronome settings and UI improvements 2021-11-14 00:18:32 +01:00
Bruno Herbelin
d6c689c5bb Cleanup include ImGuiToolkit 2021-11-14 00:18:10 +01:00
Bruno Herbelin
8676e9b900 Integration of Ableton link in vimix application
No useful functionality yet. Only connecting, set parameters, show metrics and save settings.
2021-11-13 15:01:02 +01:00
Bruno Herbelin
c271cad9aa Cleanup headers and dependencies 2021-11-13 00:14:05 +01:00
Bruno Herbelin
8e3bf786c0 Initial implementation of Metronome from Ableton LINK
Added submodule for github ableton link, and compiled draft of Metronome class.
2021-11-13 00:13:50 +01:00
Bruno Herbelin
a6ba694fbd Code warning cleanup and add GPL license header to all CPP files 2021-11-10 23:19:38 +01:00
Bruno
790ccc320e OSX bundle install of Frei0r plugins 2021-11-10 21:16:16 +01:00
Bruno Herbelin
26e951e59b Update website screenshot 2021-11-10 11:02:05 +01:00
Bruno Herbelin
a97581f5d7 semi-transparent icon of lock to inform its not interactive
need to CTRL+clic to unlock, as opposed to other handles: this is not perfect but shows the difference
2021-11-10 00:13:57 +01:00
Bruno Herbelin
5bf280ca4d warning on not found pattern 2021-11-10 00:13:08 +01:00
Bruno Herbelin
fe00baa701 Fixed snap to find frei0r plugins
added the package to install and the PATH to find in $SNAP
2021-11-10 00:12:55 +01:00
Bruno Herbelin
d3cb1d7f42 oops 2021-11-08 00:08:19 +01:00
Bruno Herbelin
593363732a Pattern generator improvement
Testing gstreamer feature to provide only available patterns, and added many more patterns to choose from.
2021-11-08 00:05:16 +01:00
Bruno Herbelin
d00f4cf715 Cosmetics
More adapted icons and link to user manual
2021-11-07 12:25:51 +01:00
Bruno Herbelin
cac31dbb21 Help info for mask file open 2021-11-07 12:25:06 +01:00
Bruno Herbelin
eb1fa6ca04 clean file dialog after cancel 2021-11-07 12:24:46 +01:00
Bruno Herbelin
0ac515ea5a Yet more update graphical manual 2021-11-07 01:42:39 +01:00
Bruno Herbelin
190e2a4952 Continue update graphical manual 2021-11-06 22:50:59 +01:00
Bruno Herbelin
0857b1bab6 Update graphical user manual
New screenshots from vimix 6.1
2021-11-06 18:08:52 +01:00
Bruno Herbelin
f3e42fdc95 Bugfix window resize log window 2021-10-31 00:06:08 +02:00
Bruno Herbelin
d617f3308a Added Cancel button to Transition view
And minor code improvements.
2021-10-30 19:57:43 +02:00
Bruno Herbelin
27cec85443 Do not re-open previous session if new session last created 2021-10-26 23:50:26 +02:00
Bruno Herbelin
63f7cab508 Improved gstreamer support for GPU decoding in Linux 2021-10-26 23:38:41 +02:00
Bruno Herbelin
ce0ac1bee1 Minor improvement precision media player gap timing 2021-10-12 23:20:16 +02:00
Bruno Herbelin
2c2584c8df Keyboard shortcut END for Disable output 2021-10-11 23:17:29 +02:00
Bruno Herbelin
14fd4d96c3 Shortcut for output window fullscreen and raise 2021-10-11 22:46:38 +02:00
Bruno Herbelin
dd7a63413c Fixed keyboard arrows control 2021-10-09 23:40:18 +02:00
Bruno Herbelin
6d0c2301c1 Keyboard input for Source Controller
SPACE toggle play/pause, B for begin restart
2021-10-09 00:16:55 +02:00
Bruno Herbelin
8bf8f05add compilation warnings 2021-09-24 00:46:20 +02:00
Bruno Herbelin
bd773d54c6 Improve stability FPS measure in timecounter 2021-09-24 00:45:47 +02:00
Bruno Herbelin
f4c52b7ed3 Fixed output monitor disablling 2021-09-19 11:07:13 +02:00
Bruno Herbelin
5b1504c8f6 Added general DISABLE output action menu
Makes sure the output is black, unrelated to session openning or opacity
2021-09-17 11:31:52 +02:00
Bruno
06187b9a1a work-in progress Helper and keyboard shortcuts 2021-08-26 15:51:07 +02:00
Bruno
7fb6e57829 Added 1200px height selection (e.g. for WUXGA displays) 2021-08-20 21:32:12 +02:00
Bruno
5ec954dbb5 UI fix 2021-08-16 22:55:21 +02:00
Bruno
a6bc30cf62 Fixed Frame grabber 2021-08-16 22:26:59 +02:00
Bruno
df165252fa Fixed OSX vtenc_h264_hw support for Recording 2021-08-16 15:52:58 +02:00
Bruno
da9c94f675 Temporarily disable v4l loopback: not working anymore 2021-08-15 00:32:20 +02:00
Bruno
031cef6357 optimize jpegenc 2021-08-15 00:30:28 +02:00
Bruno
ef5f3efd2e BugFix changing resolution of session 2021-08-14 23:15:18 +02:00
Bruno
bc8c4e3c7b Cleanup UI for centralized Recording settings 2021-08-14 21:57:59 +02:00
Bruno
f5da4c8bc2 Recording: support for NVIDIA nvenc and improved stability
Let gstreamer appsrc generate PTS automatically (seems to fix crash of encoding after long duration). Added test for GPU encoders and switch if enabled and available.
2021-08-14 13:41:53 +02:00
Bruno
644741a1ab Attempt at improving recoding when buffer if full 2021-08-12 00:05:41 +02:00
Bruno
09f46e7a27 Minor GUI layout improvement 2021-08-12 00:05:22 +02:00
Bruno
db4e1d214f BugFix drop Session file 2021-08-11 22:18:08 +02:00
Bruno
79433dd45c Improved Log message Video recording 2021-08-11 22:17:43 +02:00
Bruno
fe72c9b829 Cleanup and improve stability of FrameGrabber 2021-08-11 20:48:18 +02:00
Bruno
b37d22ba47 Improved FrameGrabber with clock duration and priority strategies
Keep track of actual FrameGrabber duration (different from timestamp). Two strategies for frame PTS: clock and framerate priorities. Implemented variable Framerate selection for VideoRecorder.  Integration of all this in UserInterface and Settings.
2021-08-11 00:20:28 +02:00
Bruno
7fb08e618f Added a READABLE time string conversion 2021-08-11 00:17:07 +02:00
Bruno
63c6f1169b Add icons on info and warning Logs 2021-08-11 00:16:19 +02:00
Bruno
0eff8fd24d Minor compilation warning fixed 2021-08-09 10:08:32 +02:00
Bruno
818e554d35 removing debuging info 2021-08-08 23:58:54 +02:00
Bruno
5a18dbaf37 Video Recoding Buffer management
Implemented methods to supervise encoding in FrameGrabber, avoid running out of buffer, and give user a selection of buffer sizes for recording.
2021-08-08 23:58:35 +02:00
Bruno
ddd9bb4e99 minor compilation fix 2021-08-07 20:26:59 +02:00
Bruno
38a2aa90e0 Improved FrameBufferSuface API 2021-08-07 20:26:30 +02:00
Bruno
139137770c BugFix: prevent missing symbol when attach 2021-08-07 20:25:52 +02:00
Bruno
789bf1bd00 BugFix prevent failed FrameBuffer fill 2021-08-07 20:23:20 +02:00
Bruno
563e762dde Store mask after fill 2021-08-07 20:22:07 +02:00
Bruno
28da5f8f39 BugFix Restore fading when cross fading 2021-08-07 20:21:37 +02:00
Bruno
5c42061fd9 Work in progress - towards display initials on sources 2021-08-07 20:21:37 +02:00
Bruno
843224ca35 oops.. correct Mesh file parse 2021-08-07 14:14:16 +02:00
Bruno
e47e76962b Fixed Recording (timing and UI)
Improved frame grabber timing and fixed UserInterface to show the "saving file" info.
2021-08-07 12:34:05 +02:00
Bruno
2f0e4e3212 Improved recording time acuracy 2021-08-07 01:02:39 +02:00
Bruno
fb3e1d0d25 Detecting EOF recording and unexpected termination 2021-08-06 21:23:01 +02:00
Bruno
e9b7e55570 work in progress recording probe 2021-08-06 17:56:48 +02:00
Bruno
8c206898f0 Dialog media include more formats
Integrate exotic file extensions and uppercase equivalent of all possible files to select with dialogs. 
Code cleanup
2021-08-06 16:43:25 +02:00
Bruno
a9c9683b8b longer recording timeout 2021-08-06 13:26:19 +02:00
Bruno
4f43ddf088 Draw a glyph in IMGUI
Proof of concept to show how to access Font texture in opengl to draw one glygh
2021-08-06 13:25:19 +02:00
Bruno
a9c8b67975 Implementation of custom Masks
FrameBuffer accepts to fill any size of FrameBufferImage as input, and a Dialog in TextureView allows to select a JPG or PNG.
2021-08-06 13:23:59 +02:00
Bruno
d1b7073ff9 Reimplementation of Dialogs
Cleanup code to integrate multithreading process for dialogs into the DialogToolkit (avoid poluting UserInterfaceManager and improves reliability)
2021-08-06 13:21:16 +02:00
Bruno
58afcacab9 BugFix thumbnailing
1. avoid crash by cathing the correct exception and 2. ensure we capture a frame by waiting a little
2021-08-04 12:55:51 +02:00
Bruno
5eddfcf196 Add tag icon in thumbnail to identify as user defined 2021-08-04 00:29:31 +02:00
Bruno
9a87764949 Improved UI for inactive videos
Display Player for videos even in disabled state, but with disabled controls
2021-08-03 20:02:04 +02:00
Bruno
fc4e40fba3 Display mixing source original texture when inactive
Re-using activesurface_ for manipulation and display of the source's input texture in the Mixer icon when inactive.
2021-08-02 22:27:06 +02:00
Bruno
e8acfc1c26 New Media Player option to Rewind on Disabled 2021-08-01 19:10:46 +02:00
Bruno
eaadc210ae Performance improvement for transliteration
Tracing CPU usage identified the cost of ICU transliteration: using a static dictionnary to improve performance
2021-08-01 16:44:46 +02:00
Bruno
8002f3164c Confirmed performance improvement without glBufferSubData 2021-08-01 16:43:25 +02:00
Bruno
48f92bc52b Cleanup session properties panel 2021-08-01 12:13:38 +02:00
Bruno
d1e833e0a1 Properties pannel of Session
Also added custom thumbnail of session.
2021-08-01 00:29:44 +02:00
Bruno
c5f0be2b32 Compilation Linux 2021-07-30 21:36:47 +02:00
Bruno
dbcf3bb0ea backward compatibility title window 2021-07-30 16:48:26 +02:00
Bruno
e7a79f6cdc Cleanup path_relative_to_path and path_absolute_from_path 2021-07-30 16:08:24 +02:00
Bruno
63b043dc4b Improved windows titles management
Display filename (no path) before APP_NAME, clean APP_TITLE when no file, bugs fixed.
2021-07-30 16:08:00 +02:00
Bruno
d2a576c99c Support for relative path for files in mix
File path in mix session file add a relative reference to the location of the session mix file. If SessionCreator cannot find the absolute path, it tries to load the file at the relative location. Done for MediaSource, SessionFileSource and SequenceSource.
2021-07-30 00:22:44 +02:00
Bruno
fc91e7cbdd Draft Function path_relative_to_path 2021-07-28 19:03:38 +02:00
Bruno
0555361a57 BugFix glfw set Window Title
Function is not thread safe, causing crash when saving.
2021-07-27 20:05:39 +02:00
Bruno
c923815a01 Added Apple Code signing script in cmake 2021-07-27 20:05:01 +02:00
Bruno
7a4d2ac027 Merge remote-tracking branch 'origin/master' 2021-07-27 09:15:47 +02:00
Bruno
442e1096be Compilation and packaging OSX 10.14 2021-07-27 09:12:53 +02:00
Bruno
6eaf8852ae OSX compilation 2021-07-27 09:06:53 +02:00
Bruno
3612fca707 Add keyboard shortcut play/pause
Space bar to toggle play/pause current source, B  for 'beginning' and N for 'next frame'
2021-07-26 12:51:07 +02:00
Bruno
4736d403a1 bugfix save as 2021-07-26 12:21:06 +02:00
Bruno
a18fd3177c Follow clang-tidy and clazy suggestions
variables non-POD should not be 'static' outside a class. Always use and init variables. Delete useless classes.
2021-07-17 16:45:01 +02:00
Bruno
5930b0f8fe UI bugfix locked source in texture view 2021-07-17 11:55:23 +02:00
Bruno
1c7c64db59 Merge remote-tracking branch 'origin/master' 2021-07-17 10:33:53 +02:00
Bruno
9bc780bcda Update CMAKE for OSX
minor text message changes
2021-07-17 10:33:43 +02:00
Bruno
b3f89e0464 for OSX 2021-07-17 10:32:13 +02:00
Bruno
1de4822c67 OSX compile 2021-07-03 11:27:08 +02:00
Bruno
c846e4072a postponing the dev of snapshot interpolation 2021-07-03 10:19:00 +02:00
Bruno
c4f26bd500 added -v and -t command line options 2021-07-02 22:16:55 +02:00
Bruno
041c01135a Small improvement timing fade in and out
Adding a buffer of 0 opacity before or after fading to avoid jumps to previous or next frame of a segment
2021-06-28 23:43:44 +02:00
Bruno
aa904f26ad Recording timeout with timing slider
Changed timout recording in uint milisecond.
2021-06-28 21:33:17 +02:00
Bruno
ff99d37eb6 Player Video Fading dialog
New dialog to apply fade in & out with parameters. Fixed Timeline fading functions. New ImGuiToolkit items to draw icons in Combo boxes.
2021-06-28 00:21:29 +02:00
Bruno
e8a500dc99 BugFix negative play speed Selection 2021-06-24 23:15:50 +02:00
Bruno
9f4f247cd2 Bugfix jump gaps MediaPlayer 2
Timeline copy should not overwrite pts of first frame.
2021-06-23 20:34:00 +02:00
Bruno
e1ac930dd6 Pedantic tooltip 2021-06-21 23:15:17 +02:00
Bruno
c20ed94f46 Bugfix jump gaps MediaPlayer
now that we use first frame time, testing jumps should be done with beginning time
2021-06-21 23:14:43 +02:00
Bruno
4efe754a8d MediaPlayer decoder information improved 2021-06-20 23:54:19 +02:00
Bruno
bb83f7fcb7 Timeline fade in and fade out 2021-06-20 23:53:52 +02:00
Bruno
79fa6082b0 Player: shoft slider on first frame of MediaPlayer 2021-06-20 18:50:12 +02:00
Bruno
f2ecc88955 Clean string from infor visitor on media player 2021-06-20 18:49:39 +02:00
Bruno
7253c1ec1a Merge remote-tracking branch 'origin/master' 2021-06-19 09:49:53 +02:00
BHBN
cf32c9fc12 Merge pull request #39 from brunoherbelin/dev
Accept new Player (dev)
2021-06-19 09:49:11 +02:00
Bruno
3086735be1 Merge branch 'HEAD' 2021-06-19 01:04:11 +02:00
Bruno
5a54e84dd8 Player slight improvements
tick marks count adapted to fps, clamped refresh frequency computation, listing of all sources playable from menu
2021-06-19 01:03:21 +02:00
Bruno
1ef26c0c95 Warning on failed discovery of MediaPlayer framerate
Default to 30fps
2021-06-19 01:03:21 +02:00
Bruno
887142079b Fixed Timeline display 2021-06-19 01:03:21 +02:00
Bruno
b75ea00c0d Unique play/pause button for multiple sources 2021-06-19 01:03:21 +02:00
Bruno
319fbfa84d Bugfix display STRING time 2021-06-19 01:03:21 +02:00
Bruno
3874252797 Bugfix computation time with gaps 2021-06-19 01:03:21 +02:00
Bruno
5dfc45af5f Fixed Timeline ticks display 2021-06-19 01:03:21 +02:00
Bruno
1f203801db Player: reset selection on session change 2021-06-19 01:03:21 +02:00
Bruno
291410a2b3 Player UI: video menu and speed reset icon
+ rename private variables to follow usual style
2021-06-19 01:03:21 +02:00
Bruno
dfc4937688 Player: move up timeline and adjust size
keep play button bar at the bottom for all configurations, avoid text and buttons overlay when Player is small. Fix cut timing in selection
2021-06-19 01:03:21 +02:00
Bruno
a0b763ab71 Timeline management in Player
Actions at key times (durations of all videos) to allow to adjust other videos duration (change speed of cut)
2021-06-19 01:03:21 +02:00
Bruno
ad36ac5cd9 Player timeline for selection
Selection of media sources now displays in a list with proportional timelines, showing actual play time and cursor on effective timeline with opacity curve.
2021-06-19 01:03:21 +02:00
Bruno
cd40d6d7e8 Improved management of selection in Player 2021-06-19 01:03:21 +02:00
Bruno
ec4214ebf8 improved quality realtime recorder h264 2021-06-19 01:03:21 +02:00
Bruno
a403d40b6c Stick window on current view
menu item for media player and output preview to pin the window in current view
2021-06-19 01:03:21 +02:00
Bruno
7dcfc97f33 UI details 2021-06-19 01:03:21 +02:00
Bruno
5ea056a483 Bugfix timeline display array 2021-06-19 01:03:21 +02:00
Bruno
cd1702bb53 Define UNICODE symbols 2021-06-19 01:03:21 +02:00
Bruno
6ff266581a work in progress selection timeline 2021-06-19 01:03:21 +02:00
Bruno
6b7d108407 Minor improvements timeline display 2021-06-19 01:03:21 +02:00
Bruno
473e24bcd7 Fixed and improved TimeCounter 2021-06-19 01:03:21 +02:00
Bruno
1f5056bf15 BugFix IconButton (pop id) 2021-06-19 01:03:21 +02:00
Bruno
61fa062794 Correct implementation of DeviceSource resize
previous commit was bad...
2021-06-19 01:03:21 +02:00
Bruno
0e48cf4505 Bugfix and cleanup Info on source in UI 2021-06-19 01:03:21 +02:00
Bruno
b606f479e9 Bugfix change device source resolution 2021-06-19 01:03:21 +02:00
Bruno
2ccbf1ec12 Bugfix closing single frame stream 2021-06-19 01:03:21 +02:00
Bruno
ac6e84bb1c New InfoVisitor: get string to describe sources
Unified code in ImGui visitor and Player
2021-06-19 01:03:21 +02:00
Bruno
11d12c1f29 New Timeline actions
Smooth and auto cut actions added on the side of the timeline UI.
2021-06-19 01:03:21 +02:00
Bruno
c9707e7335 Improved link between ImGuiVisitor and SourcePlayer
Source panel shows description and icon to open player UI. Changed icon player, and fixed source selection.
2021-06-19 01:03:21 +02:00
Bruno
2c0be68a3c Cleaup UI Selection source Player 2021-06-19 01:03:21 +02:00
Bruno
2add317106 Improved UI media player (info media) 2021-06-19 01:03:21 +02:00
Bruno
60ec11982a OSX compilation 2021-06-19 01:03:21 +02:00
Bruno
b2284cf1b4 Improved cursor EditPlotHistoLines 2021-06-19 01:02:12 +02:00
Bruno
e87ef2774b New SourcePlayer
Work in progress; Sources now have play/pause and associated play functions. Media player can play all playable sources, and adapts to control a media player when possible. Selection of play groups (to finalize)
2021-06-19 01:02:12 +02:00
Bruno
7a9fcaefd6 Minor code improvements 2021-06-19 01:02:12 +02:00
Bruno
2333a7a11a Bugfix Do not lock session in action manager
This was causing mutex deadlock
2021-06-19 01:02:12 +02:00
Bruno
2a7857c499 Bugfix; verify frame grabbers before use 2021-06-19 01:02:12 +02:00
Bruno
e892dc1eb5 Implemented delayed start of recording 2021-06-19 01:02:12 +02:00
Bruno
9c8d1f31f6 bugfix linux shared webcam ui 2021-06-19 01:02:12 +02:00
Bruno
048db7a44b Merge remote-tracking branch 'origin/master' 2021-06-19 01:01:59 +02:00
Bruno
1d94d494b6 Merge remote-tracking branch 'origin/dev' into dev 2021-06-19 00:50:31 +02:00
Bruno
c6ac35addb Player slight improvements
tick marks count adapted to fps, clamped refresh frequency computation, listing of all sources playable from menu
2021-06-19 00:49:20 +02:00
Bruno
9ec279754b Warning on failed discovery of MediaPlayer framerate
Default to 30fps
2021-06-19 00:49:20 +02:00
Bruno
bdcf28c5da Fixed Timeline display 2021-06-19 00:49:20 +02:00
Bruno
edf0f8074a Unique play/pause button for multiple sources 2021-06-19 00:49:20 +02:00
Bruno
c83a946cbd Bugfix display STRING time 2021-06-19 00:49:20 +02:00
Bruno
8604babeb6 Bugfix computation time with gaps 2021-06-19 00:49:20 +02:00
Bruno
7252b74539 Fixed Timeline ticks display 2021-06-19 00:49:20 +02:00
Bruno
08fbaa039f Player: reset selection on session change 2021-06-19 00:49:20 +02:00
Bruno
30f9fb50eb Player UI: video menu and speed reset icon
+ rename private variables to follow usual style
2021-06-19 00:49:20 +02:00
Bruno
e3578df8a0 Player: move up timeline and adjust size
keep play button bar at the bottom for all configurations, avoid text and buttons overlay when Player is small. Fix cut timing in selection
2021-06-19 00:49:20 +02:00
Bruno
37445b8857 Timeline management in Player
Actions at key times (durations of all videos) to allow to adjust other videos duration (change speed of cut)
2021-06-19 00:49:20 +02:00
Bruno
99ea14fab0 Player timeline for selection
Selection of media sources now displays in a list with proportional timelines, showing actual play time and cursor on effective timeline with opacity curve.
2021-06-19 00:49:20 +02:00
Bruno
1717c143b2 Improved management of selection in Player 2021-06-19 00:49:20 +02:00
Bruno
53223d0876 improved quality realtime recorder h264 2021-06-19 00:49:20 +02:00
Bruno
096bcb4132 Stick window on current view
menu item for media player and output preview to pin the window in current view
2021-06-19 00:49:20 +02:00
Bruno
05cc70bdbd UI details 2021-06-19 00:49:20 +02:00
Bruno
45653b52b5 Bugfix timeline display array 2021-06-19 00:49:20 +02:00
Bruno
d1841f2863 Define UNICODE symbols 2021-06-19 00:49:20 +02:00
Bruno
f85de11711 work in progress selection timeline 2021-06-19 00:49:20 +02:00
Bruno
9d81a105ee Minor improvements timeline display 2021-06-19 00:49:20 +02:00
Bruno
deb6af9dea Fixed and improved TimeCounter 2021-06-19 00:49:20 +02:00
Bruno
ab512b76aa BugFix IconButton (pop id) 2021-06-19 00:49:20 +02:00
Bruno
bcbeee7247 Correct implementation of DeviceSource resize
previous commit was bad...
2021-06-19 00:49:20 +02:00
Bruno
bcdc94c3b9 Bugfix and cleanup Info on source in UI 2021-06-19 00:49:20 +02:00
Bruno
6ebcf49758 Bugfix change device source resolution 2021-06-19 00:49:20 +02:00
Bruno
a936ab6851 Bugfix closing single frame stream 2021-06-19 00:49:20 +02:00
Bruno
95378660dd New InfoVisitor: get string to describe sources
Unified code in ImGui visitor and Player
2021-06-19 00:49:20 +02:00
Bruno
e49bdac3e8 New Timeline actions
Smooth and auto cut actions added on the side of the timeline UI.
2021-06-19 00:49:20 +02:00
Bruno
48380fab7e Improved link between ImGuiVisitor and SourcePlayer
Source panel shows description and icon to open player UI. Changed icon player, and fixed source selection.
2021-06-19 00:49:20 +02:00
Bruno
ce92529a84 Cleaup UI Selection source Player 2021-06-19 00:49:20 +02:00
Bruno
8e29a555c8 Improved UI media player (info media) 2021-06-19 00:49:20 +02:00
Bruno
a1b6ec066b OSX compilation 2021-06-19 00:49:20 +02:00
Bruno
fb59bf491f Improved cursor EditPlotHistoLines 2021-06-19 00:48:11 +02:00
Bruno
86aec7d2ba New SourcePlayer
Work in progress; Sources now have play/pause and associated play functions. Media player can play all playable sources, and adapts to control a media player when possible. Selection of play groups (to finalize)
2021-06-19 00:48:11 +02:00
Bruno
579f7d5609 Minor code improvements 2021-06-19 00:48:11 +02:00
Bruno
c87b1ac363 Bugfix Do not lock session in action manager
This was causing mutex deadlock
2021-06-19 00:48:11 +02:00
Bruno
543648112b Bugfix; verify frame grabbers before use 2021-06-19 00:48:11 +02:00
Bruno
daa3b9e978 Implemented delayed start of recording 2021-06-19 00:48:11 +02:00
Bruno
fb8da181da bugfix linux shared webcam ui 2021-06-19 00:48:11 +02:00
Bruno
6cc5a8af9e Player slight improvements
tick marks count adapted to fps, clamped refresh frequency computation, listing of all sources playable from menu
2021-06-19 00:47:47 +02:00
Bruno
1a1956962a Warning on failed discovery of MediaPlayer framerate
Default to 30fps
2021-06-18 23:58:23 +02:00
Bruno
e422a1b403 Fixed Timeline display 2021-06-18 23:57:42 +02:00
Bruno
295ece79ae Unique play/pause button for multiple sources 2021-06-18 00:22:07 +02:00
Bruno
08f8ee159a Bugfix display STRING time 2021-06-18 00:21:36 +02:00
Bruno
e2d416b3fb Bugfix computation time with gaps 2021-06-18 00:21:09 +02:00
Bruno
16ed97b4cb Fixed Timeline ticks display 2021-06-17 21:42:15 +02:00
Bruno
82739702bd Player: reset selection on session change 2021-06-16 23:47:42 +02:00
Bruno
0010c9e3d5 Player UI: video menu and speed reset icon
+ rename private variables to follow usual style
2021-06-16 23:12:31 +02:00
Bruno
f59d4af92b Player: move up timeline and adjust size
keep play button bar at the bottom for all configurations, avoid text and buttons overlay when Player is small. Fix cut timing in selection
2021-06-15 23:51:59 +02:00
Bruno
a7df619a05 Timeline management in Player
Actions at key times (durations of all videos) to allow to adjust other videos duration (change speed of cut)
2021-06-14 23:42:20 +02:00
Bruno
f3759c2ef5 Player timeline for selection
Selection of media sources now displays in a list with proportional timelines, showing actual play time and cursor on effective timeline with opacity curve.
2021-06-13 00:24:45 +02:00
Bruno
d547f3a8a8 Improved management of selection in Player 2021-06-07 22:53:29 +02:00
Bruno
583e53d8a8 improved quality realtime recorder h264 2021-06-07 00:04:23 +02:00
Bruno
a27bf08ce0 Stick window on current view
menu item for media player and output preview to pin the window in current view
2021-06-07 00:04:06 +02:00
Bruno
060fb5ad2d UI details 2021-06-07 00:03:06 +02:00
Bruno
ffc00d9035 Bugfix timeline display array 2021-06-06 14:54:55 +02:00
Bruno
83a9d281c2 Define UNICODE symbols 2021-06-06 14:54:21 +02:00
Bruno
b6c853c308 work in progress selection timeline 2021-06-06 00:17:19 +02:00
Bruno
552c09d377 Minor improvements timeline display 2021-05-30 22:56:13 +02:00
Bruno
61164e627b Fixed and improved TimeCounter 2021-05-26 23:31:34 +02:00
Bruno
3c71ee1ff2 BugFix IconButton (pop id) 2021-05-25 22:17:57 +02:00
Bruno
07f610e84a Correct implementation of DeviceSource resize
previous commit was bad...
2021-05-25 20:20:43 +02:00
Bruno
faf344bc03 Bugfix and cleanup Info on source in UI 2021-05-25 09:09:23 +02:00
Bruno
e9482a3dfc Bugfix change device source resolution 2021-05-25 09:08:51 +02:00
Bruno
d4a7ce3487 Bugfix closing single frame stream 2021-05-25 09:08:24 +02:00
Bruno
3ef2737d82 New InfoVisitor: get string to describe sources
Unified code in ImGui visitor and Player
2021-05-24 20:39:56 +02:00
BHBN
582b67f4e1 Add ref to GLP 3 or later 2021-05-24 12:10:51 +02:00
Bruno
5dd6c0af78 New Timeline actions
Smooth and auto cut actions added on the side of the timeline UI.
2021-05-24 00:58:21 +02:00
Bruno
893e4f4723 Improved link between ImGuiVisitor and SourcePlayer
Source panel shows description and icon to open player UI. Changed icon player, and fixed source selection.
2021-05-23 12:32:32 +02:00
Bruno
d137e87f0e Cleaup UI Selection source Player 2021-05-23 00:55:24 +02:00
Bruno
7f152077e5 Improved UI media player (info media) 2021-05-22 01:34:19 +02:00
Bruno
74a9b229d0 OSX compilation 2021-05-19 23:30:07 +02:00
Bruno
80cf57a979 Improved cursor EditPlotHistoLines 2021-05-19 23:29:57 +02:00
Bruno
19ba943075 New SourcePlayer
Work in progress; Sources now have play/pause and associated play functions. Media player can play all playable sources, and adapts to control a media player when possible. Selection of play groups (to finalize)
2021-05-19 00:31:37 +02:00
Bruno
ba0e25a272 Minor code improvements 2021-05-17 16:13:21 +02:00
Bruno
575c487fa6 Bugfix Do not lock session in action manager
This was causing mutex deadlock
2021-05-17 16:10:46 +02:00
Bruno
1e91b2aa29 Bugfix; verify frame grabbers before use 2021-05-13 23:15:47 +02:00
Bruno
e10cf40f38 Implemented delayed start of recording 2021-05-13 22:15:50 +02:00
Bruno
23defd2117 bugfix linux shared webcam ui 2021-05-12 21:01:19 +02:00
Bruno
1227b87565 Merge remote-tracking branch 'origin/master' 2021-05-08 23:19:23 +02:00
Bruno
6a96c91fe1 Merge branch 'dev' 2021-05-08 12:19:13 +02:00
192 changed files with 13139 additions and 3589 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@ osx/.DS_Store
.DS_Store
osx/runvimix
*.autosave

3
.gitmodules vendored
View File

@@ -19,3 +19,6 @@
[submodule "ext/glm"]
path = ext/glm
url = https://github.com/g-truc/glm.git
[submodule "ext/link"]
path = ext/link
url = https://github.com/Ableton/link.git

View File

@@ -1,5 +1,25 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <string>
#include <algorithm>
#include <thread>
#include "Log.h"
#include "View.h"
@@ -12,6 +32,7 @@
#include "Settings.h"
#include "BaseToolkit.h"
#include "Interpolator.h"
#include "SystemToolkit.h"
#include "ActionManager.h"
@@ -26,32 +47,36 @@ using namespace tinyxml2;
void captureMixerSession(tinyxml2::XMLDocument *doc, std::string node, std::string label)
{
// create node
XMLElement *sessionNode = doc->NewElement( node.c_str() );
doc->InsertEndChild(sessionNode);
// label describes the action
sessionNode->SetAttribute("label", label.c_str() );
// label describes the action
sessionNode->SetAttribute("date", SystemToolkit::date_time_string().c_str() );
// view indicates the view when this action occured
sessionNode->SetAttribute("view", (int) Mixer::manager().view()->mode());
// get session to operate on
Session *se = Mixer::manager().session();
se->lock();
// get the thumbnail (requires one opengl update to render)
FrameBufferImage *thumbnail = se->thumbnail();
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, doc);
if (imageelement)
sessionNode->InsertEndChild(imageelement);
delete thumbnail;
FrameBufferImage *thumbnail = se->renderThumbnail();
if (thumbnail) {
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, doc);
if (imageelement)
sessionNode->InsertEndChild(imageelement);
delete thumbnail;
}
// save session attributes
sessionNode->SetAttribute("activationThreshold", se->activationThreshold());
// save all sources using source visitor
SessionVisitor sv(doc, sessionNode);
for (auto iter = se->begin(); iter != se->end(); ++iter, sv.setRoot(sessionNode) )
(*iter)->accept(sv);
se->unlock();
}
@@ -184,7 +209,7 @@ void Action::restore(uint target)
void Action::snapshot(const std::string &label)
void Action::snapshot(const std::string &label, bool threaded)
{
// ignore if locked
if (locked_)
@@ -199,8 +224,11 @@ void Action::snapshot(const std::string &label)
Session *se = Mixer::manager().session();
se->snapshots()->keys_.push_back(id);
// threaded capture state of current session
std::thread(captureMixerSession, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label).detach();
if (threaded)
// threaded capture state of current session
std::thread(captureMixerSession, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label).detach();
else
captureMixerSession(se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label);
#ifdef ACTION_DEBUG
Log::Info("Snapshot stored %d '%s'", id, snap_label.c_str());
@@ -280,6 +308,23 @@ std::string Action::label(uint64_t snapshotid) const
return label;
}
std::string Action::date(uint64_t snapshotid) const
{
std::string date = "";
// get snapshot node of target in current session
Session *se = Mixer::manager().session();
const XMLElement *snap = se->snapshots()->xmlDoc_->FirstChildElement( SNAPSHOT_NODE(snapshotid).c_str() );
if (snap){
const char *d = snap->Attribute("date");
if (d)
date = std::string(d);
}
return date;
}
void Action::setLabel (uint64_t snapshotid, const std::string &label)
{
open(snapshotid);
@@ -303,6 +348,13 @@ FrameBufferImage *Action::thumbnail(uint64_t snapshotid) const
return img;
}
void Action::clearSnapshots()
{
Session *se = Mixer::manager().session();
while (!se->snapshots()->keys_.empty())
remove(se->snapshots()->keys_.front());
}
void Action::remove(uint64_t snapshotid)
{
if (snapshotid > 0)
@@ -396,3 +448,68 @@ void Action::interpolate(float val, uint64_t snapshotid)
}
// static multithreaded version saving
static void saveSnapshot(const std::string& filename, tinyxml2::XMLElement *snapshot_node)
{
if (!snapshot_node){
Log::Warning("Invalid version.", filename.c_str());
return;
}
const char *l = snapshot_node->Attribute("label");
if (!l) {
Log::Warning("Invalid version.", filename.c_str());
return;
}
// load the file: is it a session?
tinyxml2::XMLDocument xmlDoc;
XMLError eResult = xmlDoc.LoadFile(filename.c_str());
if ( XMLResultError(eResult)){
Log::Warning("%s could not be openned for re-export.", filename.c_str());
return;
}
XMLElement *header = xmlDoc.FirstChildElement(APP_NAME);
if (header == nullptr) {
Log::Warning("%s is not a %s session file.", filename.c_str(), APP_NAME);
return;
}
// remove all snapshots
XMLElement* snapshotNode = xmlDoc.FirstChildElement("Snapshots");
xmlDoc.DeleteChild(snapshotNode);
// swap "Session" node with version_node
XMLElement *sessionNode = xmlDoc.FirstChildElement("Session");
xmlDoc.DeleteChild(sessionNode);
sessionNode = snapshot_node->DeepClone(&xmlDoc)->ToElement();
sessionNode->SetName("Session");
xmlDoc.InsertEndChild(sessionNode);
// we got a session, set a new export filename
std::string newfilename = filename;
newfilename.insert(filename.size()-4, "_" + std::string(l));
// save new file to disk
if ( XMLSaveDoc(&xmlDoc, newfilename) )
Log::Notify("Version exported to %s.", newfilename.c_str());
else
// error
Log::Warning("Failed to export Session file %s.", newfilename.c_str());
}
void Action::saveas(const std::string& filename, uint64_t snapshotid)
{
// ignore if locked or if no label is given
if (locked_)
return;
if (snapshotid > 0)
open(snapshotid);
if (snapshot_node_) {
// launch a thread to save the session
std::thread (saveSnapshot, filename, snapshot_node_).detach();
}
}

View File

@@ -2,6 +2,7 @@
#define ACTIONMANAGER_H
#include <list>
#include <string>
#include <atomic>
#include <tinyxml2.h>
@@ -38,17 +39,19 @@ public:
FrameBufferImage *thumbnail (uint s) const;
// Snapshots
void snapshot (const std::string &label = "");
void snapshot (const std::string &label = "", bool threaded = false);
void clearSnapshots ();
std::list<uint64_t> snapshots () const;
uint64_t currentSnapshot () const { return snapshot_id_; }
void open (uint64_t snapshotid);
void open (uint64_t snapshotid);
void replace (uint64_t snapshotid = 0);
void restore (uint64_t snapshotid = 0);
void remove (uint64_t snapshotid = 0);
void saveas (const std::string& filename, uint64_t snapshotid = 0);
std::string label (uint64_t snapshotid) const;
std::string date (uint64_t snapshotid) const;
std::list<std::string> labels () const;
void setLabel (uint64_t snapshotid, const std::string &label);
FrameBufferImage *thumbnail (uint64_t snapshotid) const;

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "BaseToolkit.h"
#include <chrono>
@@ -7,6 +26,7 @@
#include <iomanip>
#include <algorithm>
#include <climits>
#include <map>
#include <locale>
#include <unicode/ustream.h>
@@ -52,26 +72,45 @@ std::string BaseToolkit::uniqueName(const std::string &basename, std::list<std::
std::string BaseToolkit::transliterate(const std::string &input)
{
auto ucs = icu::UnicodeString::fromUTF8(input);
// because icu::Transliterator is slow, we keep a dictionnary of already
// transliterated texts to be faster during repeated calls (update of user interface)
static std::map<std::string, std::string> dictionnary_;
std::map<std::string, std::string>::const_iterator existingentry = dictionnary_.find(input);
UErrorCode status = U_ZERO_ERROR;
icu::Transliterator *firstTrans = icu::Transliterator::createInstance(
"any-NFKD ; [:Nonspacing Mark:] Remove; NFKC; Latin", UTRANS_FORWARD, status);
firstTrans->transliterate(ucs);
delete firstTrans;
if (existingentry == dictionnary_.cend()) {
icu::Transliterator *secondTrans = icu::Transliterator::createInstance(
"any-NFKD ; [:Nonspacing Mark:] Remove; [@!#$*%~] Remove; NFKC", UTRANS_FORWARD, status);
secondTrans->transliterate(ucs);
delete secondTrans;
auto ucs = icu::UnicodeString::fromUTF8(input);
std::ostringstream output;
output << ucs;
UErrorCode status = U_ZERO_ERROR;
icu::Transliterator *firstTrans = icu::Transliterator::createInstance(
"any-NFKD ; [:Nonspacing Mark:] Remove; NFKC; Latin", UTRANS_FORWARD, status);
firstTrans->transliterate(ucs);
delete firstTrans;
return output.str();
icu::Transliterator *secondTrans = icu::Transliterator::createInstance(
"any-NFKD ; [:Nonspacing Mark:] Remove; [@!#$*%~] Remove; NFKC", UTRANS_FORWARD, status);
secondTrans->transliterate(ucs);
delete secondTrans;
std::ostringstream output;
output << ucs;
// remember for future
dictionnary_[input] = output.str();
}
// return remembered transliterated text
return dictionnary_[input];
}
std::string BaseToolkit::unspace(const std::string &input)
{
std::string output = input;
std::replace( output.begin(), output.end(), ' ', '_');
return output;
}
std::string BaseToolkit::byte_to_string(long b)
{
double numbytes = static_cast<double>(b);
@@ -85,7 +124,8 @@ std::string BaseToolkit::byte_to_string(long b)
++i;
numbytes /= 1024.0;
}
oss << std::fixed << std::setprecision(2) << numbytes << *i;
oss << std::fixed << std::setprecision(2) << numbytes;
if (i != list.end()) oss << *i;
return oss.str();
}
@@ -102,21 +142,59 @@ std::string BaseToolkit::bits_to_string(long b)
++i;
numbytes /= 1000.0;
}
oss << std::fixed << std::setprecision(2) << numbytes << *i;
oss << std::fixed << std::setprecision(2) << numbytes;
if (i != list.end()) oss << *i;
return oss.str();
}
std::string BaseToolkit::trunc_string(const std::string& path, int N)
std::string BaseToolkit::truncated(const std::string& str, int N)
{
std::string trunc = path;
int l = path.size();
std::string trunc = str;
int l = str.size();
if ( l > N ) {
trunc = std::string("...") + path.substr( l - N + 3 );
trunc = std::string("...") + str.substr( l - N + 3 );
}
return trunc;
}
std::list<std::string> BaseToolkit::splitted(const std::string& str, char delim)
{
std::list<std::string> strings;
size_t start = 0;
size_t end = 0;
while ((start = str.find_first_not_of(delim, end)) != std::string::npos) {
end = str.find(delim, start);
size_t delta = start > 0 ? 1 : 0;
strings.push_back(str.substr( start -delta, end - start + delta));
}
return strings;
}
std::string BaseToolkit::joinned(std::list<std::string> strlist, char separator)
{
std::string str;
for (auto it = strlist.cbegin(); it != strlist.cend(); ++it)
str += (*it) + separator;
return str;
}
bool BaseToolkit::is_a_number(const std::string& str, int *val)
{
bool isanumber = false;
try {
*val = std::stoi(str);
isanumber = true;
}
catch (const std::invalid_argument&) {
// avoids crash
}
return isanumber;
}
std::string BaseToolkit::common_prefix( const std::list<std::string> & allStrings )
{
@@ -221,7 +299,7 @@ std::string BaseToolkit::common_numbered_pattern(const std::list<std::string> &a
*min = std::min(*min, std::atoi(s.c_str()) );
if (n < 0)
n = s.size();
else if ( n != s.size() ) {
else if ( n != (int) s.size() ) {
n = 0;
break;
}

View File

@@ -16,14 +16,26 @@ std::string uniqueName(const std::string &basename, std::list<std::string> exist
// get a transliteration to Latin of any string
std::string transliterate(const std::string &input);
// replaces spaces by underscores in a string
std::string unspace(const std::string &input);
// get a string to display memory size with unit KB, MB, GB, TB
std::string byte_to_string(long b);
// get a string to display bit size with unit Kbit, MBit, Gbit, Tbit
std::string bits_to_string(long b);
// Truncate a string to display the right most N characters (e.g. ./home/me/toto.mpg -> ...ome/me/toto.mpg)
std::string trunc_string(const std::string& path, int N);
// cut a string to display the right most N characters (e.g. /home/me/toto.mpg -> ...ome/me/toto.mpg)
std::string truncated(const std::string& str, int N);
// split a string into list of strings separated by delimitor (e.g. /home/me/toto.mpg -> {home, me, toto.mpg} )
std::list<std::string> splitted(const std::string& str, char delim);
// rebuilds a splitted string
std::string joinned(std::list<std::string> strlist, char separator = ' ');
// returns true if the string
bool is_a_number(const std::string& str, int *val = nullptr);
// find common parts in a list of strings
std::string common_prefix(const std::list<std::string> &allStrings);

View File

@@ -20,15 +20,18 @@ if(GIT_EXECUTABLE)
add_definitions(-DVIMIX_VERSION_MAJOR=${VIMIX_VERSION_MAJOR})
add_definitions(-DVIMIX_VERSION_MINOR=${VIMIX_VERSION_MINOR})
add_definitions(-DVIMIX_VERSION_PATCH=${VIMIX_VERSION_PATCH})
message(STATUS "Compiling vimix version ${VIMIX_VERSION_MAJOR}.${VIMIX_VERSION_MINOR}.${VIMIX_VERSION_PATCH}")
else()
message(STATUS "Compiling vimix (unknown version)")
endif()
endif()
message(STATUS "Compiling vimix version ${VIMIX_VERSION_MAJOR}.${VIMIX_VERSION_MINOR}.${VIMIX_VERSION_PATCH}")
set(CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "ON")
set(CMAKE_INCLUDE_CURRENTDIR ON)
# Find the cmake modules
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules )
include(MacroLogFeature)
include(MacroFindGStreamerLibrary)
if(UNIX)
if (APPLE)
@@ -37,16 +40,19 @@ if(UNIX)
# the RPATH to be used when installing
set(CMAKE_SKIP_RPATH TRUE)
set(OpenGL_DIR /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/)
set(CMAKE_OSX_ARCHITECTURES x86_64)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
# set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X version to target for deployment")
set (ICU_ROOT /usr/local/Cellar/icu4c/67.1)
set(CMAKE_OSX_ARCHITECTURES "x86_64")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14")
# find icu4c in OSX (pretty well hidden...)
set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/opt/icu4c/lib/pkgconfig")
else()
add_definitions(-DLINUX)
# linux opengl
set(OpenGL_GL_PREFERENCE "GLVND")
# linux dialogs use GTK
find_package(GTK 3.0 REQUIRED)
macro_log_feature(GTK_FOUND "GTK" "GTK cross-platform widget toolkit" "http://www.gtk.org" TRUE)
@@ -71,9 +77,6 @@ macro_log_feature(GOBJECT_FOUND "GObject" "GTK object-oriented framework" "http:
find_package(PNG REQUIRED)
macro_log_feature(PNG_FOUND "PNG" "Portable Network Graphics" "http://www.libpng.org" TRUE)
find_package(ICU REQUIRED COMPONENTS i18n io uc)
macro_log_feature(ICU_FOUND "ICU" "International Components for Unicode" "http://site.icu-project.org" TRUE)
#
# GSTREAMER
#
@@ -82,49 +85,46 @@ find_package(GStreamer 1.0.0 COMPONENTS base)
macro_log_feature(GSTREAMER_FOUND "GStreamer"
"Open Source Multiplatform Multimedia Framework"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
macro_log_feature(GSTREAMER_BASE_LIBRARY_FOUND "GStreamer base library"
"${GSTREAMER_BASE_LIBRARY}"
"http://gstreamer.freedesktop.org/" FALSE "1.0.0")
find_package(GStreamerPluginsBase 1.0.0 COMPONENTS app audio video pbutils gl)
macro_log_feature(GSTREAMER_APP_LIBRARY_FOUND "GStreamer app library"
"${GSTREAMER_APP_LIBRARY}"
macro_log_feature(GSTREAMER_APP_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer app library"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
macro_log_feature(GSTREAMER_AUDIO_LIBRARY_FOUND "GStreamer audio library"
"${GSTREAMER_AUDIO_LIBRARY}"
macro_log_feature(GSTREAMER_AUDIO_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer audio library"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
macro_log_feature(GSTREAMER_VIDEO_LIBRARY_FOUND "GStreamer video library"
"${GSTREAMER_VIDEO_LIBRARY}"
macro_log_feature(GSTREAMER_VIDEO_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer video library"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
macro_log_feature(GSTREAMER_PBUTILS_LIBRARY_FOUND "GStreamer pbutils library"
"${GSTREAMER_PBUTILS_LIBRARY}"
macro_log_feature(GSTREAMER_PBUTILS_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer pbutils library"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
macro_log_feature(GSTREAMER_GL_LIBRARY_FOUND "GStreamer opengl library"
"${GSTREAMER_GL_LIBRARY}"
macro_log_feature(GSTREAMER_GL_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer opengl library"
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
#find_package(GStreamerPluginsBad 1.0.0 COMPONENTS player)
#macro_log_feature(GSTREAMER_PLAYER_LIBRARY_FOUND "GStreamer player library"
#"${GSTREAMER_PLAYER_LIBRARY}"
#"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
# Various preprocessor definitions for GST
add_definitions(-DGST_DISABLE_XML -DGST_DISABLE_LOADSAVE)
#
# ICU4C
#
if (PKG_CONFIG_FOUND)
pkg_check_modules(ICU REQUIRED icu-i18n icu-uc icu-io)
else ()
find_package(ICU REQUIRED COMPONENTS i18n io uc)
endif ()
macro_log_feature(ICU_FOUND "ICU" "International Components for Unicode" "http://site.icu-project.org" TRUE)
#
# GLFW3
# NB: set glfw3_PATH to /usr/local/Cellar/glfw/3.3.2/lib/cmake/glfw3
#
find_package(glfw3 3.2 REQUIRED)
macro_log_feature(glfw3_FOUND "GLFW3" "Open Source multi-platform library for OpenGL" "http://www.glfw.org" TRUE)
set(GLFW_LIBRARY glfw)
if (PKG_CONFIG_FOUND)
pkg_check_modules(GLFW3 REQUIRED glfw3>=3.2)
else ()
find_package(glfw3 3.2 REQUIRED)
endif()
macro_log_feature(GLFW3_FOUND "glfw3" "Open Source multi-platform library for OpenGL" "http://www.glfw.org" TRUE)
macro_display_feature_log()
@@ -135,14 +135,21 @@ set(BUILD_STATIC_LIBS ON)
# GLM
#
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/glm)
message(STATUS "Compiling 'GLM' OpenGL mathematics https://glm.g-truc.net")
message(STATUS "Compiling 'GLM' OpenGL mathematics https://glm.g-truc.net -- ${CMAKE_CURRENT_SOURCE_DIR}/ext/glm")
#
# Ableton LINK
#
#add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/link)
include(${CMAKE_CURRENT_SOURCE_DIR}/ext/link/AbletonLinkConfig.cmake)
message(STATUS "Compiling Ableton 'Link' https://github.com/Ableton/link -- ${CMAKE_CURRENT_SOURCE_DIR}/ext/link")
#
# GLAD
#
set(GLAD_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/glad/include)
add_library(GLAD "${CMAKE_CURRENT_SOURCE_DIR}/ext/glad/src/glad.c")
message(STATUS "Compiling 'GLAD' Open source multi-language OpenGL loader https://glad.dav1d.de -- ${GLAD_INCLUDE_DIR}.")
message(STATUS "Compiling 'GLAD' Open source multi-language OpenGL loader https://glad.dav1d.de -- ${GLAD_INCLUDE_DIR}")
#
# DEAR IMGUI
@@ -158,23 +165,14 @@ set(IMGUI_SRCS
set(IMGUI_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/imgui)
add_library(IMGUI "${IMGUI_SRCS}")
target_compile_definitions(IMGUI PRIVATE "IMGUI_IMPL_OPENGL_LOADER_GLAD")
message(STATUS "Compiling 'Dear ImGui' from https://github.com/ocornut/imgui.git -- ${IMGUI_INCLUDE_DIR}.")
#
# ImGui Color Text Editor
#
set(IMGUITEXTEDIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit)
set(IMGUITEXTEDIT_SRC
${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit/TextEditor.cpp
)
message(STATUS "Including 'ImGuiColorTextEdit' from https://github.com/BalazsJako/ImGuiColorTextEdit -- ${IMGUITEXTEDIT_INCLUDE_DIR}.")
message(STATUS "Compiling 'Dear ImGui' from https://github.com/ocornut/imgui.git -- ${IMGUI_INCLUDE_DIR}")
#
# TINY XML 2
#
set(TINYXML2_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/tinyxml2)
add_library(TINYXML2 "${CMAKE_CURRENT_SOURCE_DIR}/ext/tinyxml2/tinyxml2.cpp")
message(STATUS "Compiling 'TinyXML2' from https://github.com/leethomason/tinyxml2.git -- ${TINYXML2_INCLUDE_DIR}.")
message(STATUS "Compiling 'TinyXML2' from https://github.com/leethomason/tinyxml2.git -- ${TINYXML2_INCLUDE_DIR}")
#
# OSCPack
@@ -195,22 +193,7 @@ set(OSCPACK_SRCS
)
set(OSCPACK_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/OSCPack)
add_library(OSCPACK "${OSCPACK_SRCS}")
message(STATUS "Compiling 'OSCPack' from http://www.rossbencina.com/code/oscpack -- ${OSCPACK_INCLUDE_DIR}.")
#
# STB
#
set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/stb)
add_definitions(-DIMGUI_USE_STB_SPRINTF)
message(STATUS "Including 'STB Nothings' from https://github.com/nothings/stb -- ${STB_INCLUDE_DIR}.")
#
# DIRENT
#
if(WIN32)
set(DIRENT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/Dirent/include)
message(STATUS "Including 'Dirent' from https://github.com/tronkko/dirent -- ${DIRENT_INCLUDE_DIR}.")
endif( WIN32 )
message(STATUS "Compiling 'OSCPack' from http://www.rossbencina.com/code/oscpack -- ${OSCPACK_INCLUDE_DIR}")
#
# FILE DIALOG: use tinyfiledialog for all except Linux
@@ -230,11 +213,27 @@ else()
endif()
#
# OBJ LOADER - not used
# ImGui Color Text Editor
#
#set(OBJLOADER_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/obj)
#add_library(OBJLOADER "${CMAKE_CURRENT_SOURCE_DIR}/ext/obj/ObjLoader.cpp")
#message(STATUS "Compiling 'ObjLoader' from https://github.com/mortennobel/OpenGL_3_2_Utils -- ${OBJLOADER_INCLUDE_DIR}.")
set(IMGUITEXTEDIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit)
set(IMGUITEXTEDIT_SRC
${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit/TextEditor.cpp
)
message(STATUS "Including 'ImGuiColorTextEdit' from https://github.com/BalazsJako/ImGuiColorTextEdit -- ${IMGUITEXTEDIT_INCLUDE_DIR}")
#
# STB
#
set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/stb)
add_definitions(-DIMGUI_USE_STB_SPRINTF)
message(STATUS "Including 'STB Nothings' from https://github.com/nothings/stb -- ${STB_INCLUDE_DIR}")
#
# DIRENT (windows only)
if(WIN32)
set(DIRENT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/Dirent/include)
message(STATUS "Including 'Dirent' from https://github.com/tronkko/dirent -- ${DIRENT_INCLUDE_DIR}.")
endif()
#
# Application
@@ -249,9 +248,11 @@ include_directories(
${GSTREAMER_APP_INCLUDE_DIR}
${GSTREAMER_PBUTILS_INCLUDE_DIR}
${GSTREAMER_GL_INCLUDE_DIR}
${GLFW3_INCLUDE_DIRS}
${ICU_INCLUDE_DIRS}
${GLM_INCLUDE_DIRS}
${GLIB2_INCLUDE_DIR}
${GLAD_INCLUDE_DIR}
${GLM_INCLUDE_DIRS}
${IMGUI_INCLUDE_DIR}
${IMGUI_INCLUDE_DIR}/examples
${IMGUITEXTEDIT_INCLUDE_DIR}
@@ -260,7 +261,12 @@ include_directories(
${STB_INCLUDE_DIR}
${DIRENT_INCLUDE_DIR}
${OSCPACK_INCLUDE_DIR}
${ICU_INCLUDE_DIRS}
${link_HEADERS}
)
link_directories(
${GLFW3_LIBRARY_DIRS}
${ICU_LIBRARY_DIRS}
)
@@ -286,6 +292,7 @@ set(VMIX_SRCS
TextureView.cpp
TransitionView.cpp
Source.cpp
SourceCallback.cpp
SourceList.cpp
Session.cpp
Selection.cpp
@@ -293,6 +300,7 @@ set(VMIX_SRCS
SessionVisitor.cpp
Interpolator.cpp
SessionCreator.cpp
SessionParser.cpp
Mixer.cpp
FrameGrabber.cpp
Recorder.cpp
@@ -319,6 +327,7 @@ set(VMIX_SRCS
SearchVisitor.cpp
ImGuiToolkit.cpp
ImGuiVisitor.cpp
InfoVisitor.cpp
GstToolkit.cpp
GlmToolkit.cpp
SystemToolkit.cpp
@@ -328,6 +337,8 @@ set(VMIX_SRCS
Connection.cpp
ActionManager.cpp
Overlay.cpp
Metronome.cpp
ControlManager.cpp
)
@@ -404,6 +415,7 @@ set(VMIX_RSC_FILES
./rsc/mesh/shadow_perspective.ply
./rsc/mesh/point.ply
./rsc/mesh/square_point.ply
./rsc/mesh/triangle_point.ply
./rsc/mesh/icon_video.ply
./rsc/mesh/icon_image.ply
./rsc/mesh/icon_render.ply
@@ -453,7 +465,6 @@ IF(APPLE)
# create the application
add_executable(${VMIX_BINARY} MACOSX_BUNDLE
${VMIX_SRCS}
./osx/CustomDelegate.m
${IMGUITEXTEDIT_SRC}
${MACOSX_BUNDLE_ICON_FILE}
)
@@ -469,7 +480,7 @@ IF(APPLE)
ELSE(APPLE)
link_directories (${GTK3_LIBRARY_DIRS})
link_directories (${GTK3_LIBRARY_DIRS})
add_executable(${VMIX_BINARY}
${VMIX_SRCS}
@@ -490,11 +501,15 @@ set_property(TARGET ${VMIX_BINARY} PROPERTY C_STANDARD 11)
target_compile_definitions(${VMIX_BINARY} PUBLIC "IMGUI_IMPL_OPENGL_LOADER_GLAD")
target_link_libraries(${VMIX_BINARY} LINK_PRIVATE
vmix::rc
glm::glm
GLAD
TINYXML2
IMGUI
OSCPACK
${GLFW_LIBRARY}
${TINYFD_LIBRARY}
${GLFW3_LIBRARIES}
${ICU_LIBRARIES}
${CMAKE_DL_LIBS}
${GOBJECT_LIBRARIES}
${GSTREAMER_LIBRARY}
@@ -504,27 +519,21 @@ target_link_libraries(${VMIX_BINARY} LINK_PRIVATE
${GSTREAMER_VIDEO_LIBRARY}
${GSTREAMER_PBUTILS_LIBRARY}
${GSTREAMER_GL_LIBRARY}
${GSTREAMER_PLAYER_LIBRARY}
${TINYFD_LIBRARY}
Threads::Threads
PNG::PNG
glm::glm
ICU::i18n
ICU::io
ICU::uc
vmix::rc
Ableton::Link
${PLATFORM_LIBS}
)
### DEFINE THE PACKAGING (all OS)
SET(CPACK_PACKAGE_NAME "vimix")
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "vimix\nReal-time video mixing for live performance.")
SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md")
SET(CPACK_PACKAGE_CONTACT "bruno.herbelin@gmail.com")
SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING.txt")
SET(CPACK_PACKAGE_CONTACT "bruno.herbelin@gmail.com")
SET(CPACK_PACKAGE_HOMEPAGE_URL "https://brunoherbelin.github.io/vimix")
SET(CPACK_PACKAGE_VERSION_MAJOR "${VIMIX_VERSION_MAJOR}")
SET(CPACK_PACKAGE_VERSION_MINOR "${VIMIX_VERSION_MINOR}")
SET(CPACK_PACKAGE_VERSION_PATCH "${VIMIX_VERSION_PATCH}")
@@ -536,23 +545,19 @@ SET(CPACK_SOURCE_IGNORE_FILES
)
# optimize size ?
SET(CPACK_STRIP_FILES TRUE)
### DEFINE THE PACKAGING (OS specific)
IF(APPLE)
# include( InstallRequiredSystemLibraries )
# Bundle target
set(CPACK_GENERATOR "DragNDrop")
set(CPACK_GENERATOR DragNDrop)
set(CPACK_BINARY_DRAGNDROP ON)
# OSX cpack info
set(CPACK_SYSTEM_NAME "OSX_${CMAKE_OSX_DEPLOYMENT_TARGET}_${CMAKE_OSX_ARCHITECTURES}")
set(CPACK_BUNDLE_NAME ${CPACK_PACKAGE_NAME})
set(CPACK_BUNDLE_ICON ${MACOSX_BUNDLE_ICON_FILE})
set(CPACK_BUNDLE_PLIST ${MACOSX_BUNDLE_PLIST_FILE})
set(APPS "\${CMAKE_INSTALL_PREFIX}/vimix.app")
# set( APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/vimix${CMAKE_EXECUTABLE_SUFFIX}")
install(TARGETS ${VMIX_BINARY}
CONFIGURATIONS Release RelWithDebInfo
@@ -565,34 +570,56 @@ IF(APPLE)
### TODO configure auto to find installation dir of gst
message(STATUS "install gst-plugins ${PKG_GSTREAMER_PLUGIN_DIR}")
message(STATUS "install gst-plugins-base ${PKG_GSTREAMER_BASE_PLUGIN_DIR}")
if (PKG_CONFIG_FOUND)
pkg_check_modules(PKG_GSTREAMER_PLUGINS_BAD gstreamer-plugins-bad-${GSTREAMER_ABI_VERSION})
set(PKG_GSTREAMER_BAD_PLUGIN_DIR ${PKG_GSTREAMER_PLUGINS_BAD_LIBDIR}/gstreamer-${GSTREAMER_ABI_VERSION})
message(STATUS "install gst-plugins-bad ${PKG_GSTREAMER_BAD_PLUGIN_DIR}")
endif()
# intall the gst-plugin-scanner program (used by plugins at load time)
install(FILES "/usr/local/Cellar/gstreamer/1.18.1/libexec/gstreamer-1.0/gst-plugin-scanner"
set(PKG_GSTREAMER_SCANNER "${PKG_GSTREAMER_PREFIX}/libexec/gstreamer-1.0/gst-plugin-scanner")
message(STATUS "install gst-plugin-scanner ${PKG_GSTREAMER_SCANNER}")
install(FILES "${PKG_GSTREAMER_SCANNER}"
DESTINATION "${plugin_dest_dir}"
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
COMPONENT Runtime
)
# ICU DATA LIB GST dependency : undocumented and hacked here : seems to work
install(FILES "/usr/local/Cellar/icu4c/67.1/lib/libicudata.67.1.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" RENAME "libicudata.67.dylib" COMPONENT Runtime)
# Install the gst-plugins (all those installed with brew )
install(DIRECTORY "${PKG_GSTREAMER_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-base/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-good/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-bad/1.18.1_1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-ugly/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-libav/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "${PKG_GSTREAMER_BASE_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "${PKG_GSTREAMER_BAD_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
# install locally recompiled gst-plugins (because not included in brew package)
set(LOCAL_BUILD_BAD "/Users/herbelin/Development/gst/gst-plugins-bad-1.18.0/build")
install(FILES "${LOCAL_BUILD_BAD}/sys/applemedia/libgstapplemedia.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
install(FILES "${LOCAL_BUILD_BAD}/ext/libde265/libgstde265.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
install(FILES "${LOCAL_BUILD_BAD}/ext/x265/libgstx265.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-good/1.18.4/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-plugins-ugly/1.18.4_1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
install(DIRECTORY "/usr/local/Cellar/gst-libav/1.18.4/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
# install locally recompiled & installed gst-plugins (because not included in brew package)
install(FILES "/usr/local/lib/gstreamer-1.0/libgstapplemedia.dylib"
"/usr/local/lib/gstreamer-1.0/libgstde265.dylib"
"/usr/local/lib/gstreamer-1.0/libgstx265.dylib"
DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
# install frei0r plugins (dependencies of gstreamer-1.0/libgstfrei0r.dylib plugin)
install(FILES "/usr/local/Cellar/frei0r/1.7.0/lib/frei0r-1/lissajous0r.so"
"/usr/local/Cellar/frei0r/1.7.0/lib/frei0r-1/rgbnoise.so"
DESTINATION "${plugin_dest_dir}/frei0r-1" COMPONENT Runtime)
# ICU DATA LIB GST dependency : undocumented and hacked here : seems to work
# install(FILES "${ICU_LINK_LIBRARIES}" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
install(FILES "/usr/local/Cellar/icu4c/69.1/lib/libicudata.69.1.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" RENAME "libicudata.69.dylib" COMPONENT Runtime)
message(STATUS "install ${ICU_LINK_LIBRARIES} from ${ICU_LIBRARY_DIRS}")
# package runtime fixup bundle
set(APPS "\${CMAKE_INSTALL_PREFIX}/vimix.app")
install(CODE "
file(GLOB_RECURSE GSTPLUGINS \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/gstreamer-1.0/*.dylib\")
list(APPEND LIBS_PATH \"/usr/local/Cellar/icu4c/67.1/lib\")
list(APPEND LIBS_PATH \"\${ICU_LIBRARY_DIRS}\")
include(BundleUtilities)
set(BU_CHMOD_BUNDLE_ITEMS TRUE)
fixup_bundle(\"${APPS}\" \"\${GSTPLUGINS}\" \"${LIBS_PATH}\")
@@ -600,18 +627,39 @@ IF(APPLE)
COMPONENT Runtime
)
set(CPACK_BINARY_DRAGNDROP ON)
set(APPLE_CODESIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/osx/entitlements.plist")
set(APPLE_CODESIGN_IDENTITY "" CACHE STRING "")
string(LENGTH "${APPLE_CODESIGN_IDENTITY}" APPLE_CODESIGN_IDENTITY_LENGHT)
if( ${APPLE_CODESIGN_IDENTITY_LENGHT} LESS 40 )
message(STATUS "Not signing bundle. Specify APPLE_CODESIGN_IDENTITY to cmake before running cpack to sign")
else()
install(CODE "
execute_process(COMMAND
codesign --verbose=4 --deep --force
--entitlements \"${APPLE_CODESIGN_ENTITLEMENTS}\"
--sign \"${APPLE_CODESIGN_IDENTITY}\"
\"${APPS}\" )
"
COMPONENT Runtime
)
endif()
# # package runtime fixup bundle and codesign
# set(BUNDLE_NAME "vimix.app")
# set(BUNDLE_LIBS_DIR "${plugin_dest_dir}/gstreamer-1.0")
# set(BUNDLE_DIRS "${ICU_LIBRARY_DIRS}")
# set(APPLE_CODESIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/osx/entitlements.plist")
# configure_file(cmake/modules/BundleInstall.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/BundleInstall.cmake" @ONLY)
# install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/BundleInstall.cmake" COMPONENT Runtime)
ELSE(APPLE)
set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/vimix")
install(TARGETS ${VMIX_BINARY}
CONFIGURATIONS Release RelWithDebInfo
RUNTIME DESTINATION bin COMPONENT Runtime
)
ENDIF(APPLE)
# Package full name

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,28 +1,44 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <thread>
#include <chrono>
#include <vector>
#include <algorithm>
#include "osc/OscOutboundPacketStream.h"
#include "defines.h"
#include "Connection.h"
#include "Settings.h"
#include "Streamer.h"
#include "Log.h"
#include "Connection.h"
#ifndef NDEBUG
#define CONNECTION_DEBUG
#endif
Connection::Connection()
Connection::Connection() : receiver_(nullptr)
{
receiver_ = nullptr;
}
Connection::~Connection()
{
if (receiver_!=nullptr) {
@@ -147,7 +163,7 @@ int Connection::index(ConnectionInfo i) const
void Connection::print()
{
for(int i = 0; i<connections_.size(); i++) {
for(size_t i = 0; i<connections_.size(); i++) {
Log::Info(" - %s %s:%d", connections_[i].name.c_str(), connections_[i].address.c_str(), connections_[i].port_handshake);
}
}
@@ -157,7 +173,8 @@ void Connection::listen()
#ifdef CONNECTION_DEBUG
Log::Info("Accepting handshake on port %d", Connection::manager().connections_[0].port_handshake);
#endif
Connection::manager().receiver_->Run();
if (Connection::manager().receiver_)
Connection::manager().receiver_->Run();
}
void Connection::ask()
@@ -207,7 +224,7 @@ void Connection::ask()
}
void ConnectionRequestListener::ProcessMessage( const osc::ReceivedMessage& m,
void Connection::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint )
{
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];

View File

@@ -1,22 +1,17 @@
#ifndef CONNECTION_H
#define CONNECTION_H
#include <string>
#include <vector>
#include "osc/OscReceivedElements.h"
#include "osc/OscPacketListener.h"
#include "ip/UdpSocket.h"
#include "NetworkToolkit.h"
#define MAX_HANDSHAKE 20
#define HANDSHAKE_PORT 71310
#define STREAM_REQUEST_PORT 71510
#define OSC_DIALOG_PORT 71010
#define ALIVE 3
class ConnectionRequestListener : public osc::OscPacketListener {
protected:
virtual void ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint );
};
struct ConnectionInfo {
@@ -58,8 +53,6 @@ struct ConnectionInfo {
class Connection
{
friend class ConnectionRequestListener;
// Private Constructor
Connection();
Connection(Connection const& copy) = delete;
@@ -82,11 +75,19 @@ public:
int index(const std::string &name) const;
ConnectionInfo info(int index = 0); // index 0 for self
protected:
class RequestListener : public osc::OscPacketListener {
protected:
virtual void ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint );
};
private:
static void ask();
static void listen();
ConnectionRequestListener listener_;
RequestListener listener_;
UdpListeningReceiveSocket *receiver_;
std::vector< ConnectionInfo > connections_;

756
ControlManager.cpp Normal file
View File

@@ -0,0 +1,756 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <thread>
#include <mutex>
#include <sstream>
#include <iomanip>
#include "osc/OscOutboundPacketStream.h"
#include "defines.h"
#include "Log.h"
#include "Settings.h"
#include "BaseToolkit.h"
#include "Mixer.h"
#include "Source.h"
#include "ActionManager.h"
#include "SystemToolkit.h"
#include "tinyxml2Toolkit.h"
#include "ControlManager.h"
#ifndef NDEBUG
#define CONTROL_DEBUG
#endif
#define CONTROL_OSC_MSG "OSC: "
void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint )
{
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
remoteEndpoint.AddressAndPortAsString(sender);
try{
#ifdef CONTROL_DEBUG
Log::Info(CONTROL_OSC_MSG "received '%s' from %s", FullMessage(m).c_str(), sender);
#endif
// Preprocessing with Translator
std::string address_pattern = Control::manager().translate(m.AddressPattern());
// structured OSC address
std::list<std::string> address = BaseToolkit::splitted(address_pattern, OSC_SEPARATOR);
//
// A wellformed OSC address is in the form '/vimix/target/attribute {arguments}'
// First test: should have 3 elements and start with APP_NAME ('vimix')
//
if (address.size() > 2 && address.front().compare(OSC_PREFIX) == 0 ){
// done with the first part of the OSC address
address.pop_front();
// next part of the OSC message is the target
std::string target = address.front();
// next part of the OSC message is the attribute
address.pop_front();
std::string attribute = address.front();
// Log target: just print text in log window
if ( target.compare(OSC_INFO) == 0 )
{
if ( attribute.compare(OSC_INFO_NOTIFY) == 0) {
Log::Notify(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
}
else if ( attribute.compare(OSC_INFO_LOG) == 0) {
Log::Info(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
}
}
// Output target: concerns attributes of the rendering output
else if ( target.compare(OSC_OUTPUT) == 0 )
{
if ( Control::manager().receiveOutputAttribute(attribute, m.ArgumentStream())) {
// send the global status
Control::manager().sendOutputStatus(remoteEndpoint);
}
}
// Session target: concerns attributes of the session
else if ( target.compare(OSC_SESSION) == 0 )
{
if ( Control::manager().receiveSessionAttribute(attribute, m.ArgumentStream()) ) {
// send the global status
Control::manager().sendOutputStatus(remoteEndpoint);
// send the status of all sources
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
}
}
// ALL sources target: apply attribute to all sources of the session
else if ( target.compare(OSC_ALL) == 0 )
{
// Loop over selected sources
for (SourceList::iterator it = Mixer::manager().session()->begin(); it != Mixer::manager().session()->end(); ++it) {
// apply attributes
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
// and send back feedback if needed
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
}
}
// Selected sources target: apply attribute to all sources of the selection
else if ( target.compare(OSC_SELECTED) == 0 )
{
// Loop over selected sources
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
// apply attributes
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
// and send back feedback if needed
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
}
}
// Current source target: apply attribute to the current sources
else if ( target.compare(OSC_CURRENT) == 0 )
{
int sourceid = -1;
if ( attribute.compare(OSC_SYNC) == 0) {
// send the status of all sources
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
}
else if ( attribute.compare(OSC_NEXT) == 0) {
// set current to NEXT
Mixer::manager().setCurrentNext();
// send the status of all sources
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
}
else if ( attribute.compare(OSC_PREVIOUS) == 0) {
// set current to PREVIOUS
Mixer::manager().setCurrentPrevious();
// send the status of all sources
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
}
else if ( BaseToolkit::is_a_number( attribute.substr(1), &sourceid) ){
// set current to given INDEX
Mixer::manager().setCurrentIndex(sourceid);
// send the status of all sources
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
}
// all other attributes operate on current source
else {
// apply attributes to current source
if ( Control::manager().receiveSourceAttribute( Mixer::manager().currentSource(), attribute, m.ArgumentStream()) )
// and send back feedback if needed
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
}
}
// General case: try to identify the target
else {
// try to find source by index
Source *s = nullptr;
int sourceid = -1;
if ( BaseToolkit::is_a_number(target.substr(1), &sourceid) )
s = Mixer::manager().sourceAtIndex(sourceid);
// if failed, try to find source by name
if (s == nullptr)
s = Mixer::manager().findSource(target.substr(1));
// if a source with the given target name or index was found
if (s) {
// apply attributes to source
if ( Control::manager().receiveSourceAttribute(s, attribute, m.ArgumentStream()) )
// and send back feedback if needed
Control::manager().sendSourceAttibutes(remoteEndpoint, target, s);
}
else
Log::Info(CONTROL_OSC_MSG "Unknown target '%s' requested by %s.", target.c_str(), sender);
}
}
else {
Log::Info(CONTROL_OSC_MSG "Unknown osc message '%s' sent by %s.", m.AddressPattern(), sender);
}
}
catch( osc::Exception& e ){
// any parsing errors such as unexpected argument types, or
// missing arguments get thrown as exceptions.
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s' from %s : %s", m.AddressPattern(), sender, e.what());
}
}
std::string Control::RequestListener::FullMessage( const osc::ReceivedMessage& m )
{
// build a string with the address pattern of the message
std::ostringstream message;
message << m.AddressPattern() << " ";
// try to fill the string with the arguments
std::ostringstream arguments;
try{
// loop over all arguments
osc::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();
while (arg != m.ArgumentsEnd()) {
if( arg->IsBool() ){
bool a = (arg++)->AsBoolUnchecked();
message << (a ? "T" : "F");
}
else if( arg->IsInt32() ){
int a = (arg++)->AsInt32Unchecked();
message << "i";
arguments << " " << a;
}
else if( arg->IsFloat() ){
float a = (arg++)->AsFloatUnchecked();
message << "f";
arguments << " " << std::fixed << std::setprecision(2) << a;
}
else if( arg->IsString() ){
const char *a = (arg++)->AsStringUnchecked();
message << "s";
arguments << " " << a;
}
}
}
catch( osc::Exception& e ){
// any parsing errors such as unexpected argument types, or
// missing arguments get thrown as exceptions.
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s': %s", m.AddressPattern(), e.what());
}
// append list of arguments to the message string
message << arguments.str();
// returns the full message
return message.str();
}
Control::Control() : receiver_(nullptr)
{
}
Control::~Control()
{
terminate();
}
std::string Control::translate (std::string addresspattern)
{
auto it_translation = translation_.find(addresspattern);
if ( it_translation != translation_.end() )
return it_translation->second;
else
return addresspattern;
}
void Control::loadOscConfig()
{
// reset translations
translation_.clear();
// load osc config file
tinyxml2::XMLDocument xmlDoc;
tinyxml2::XMLError eResult = xmlDoc.LoadFile(Settings::application.control.osc_filename.c_str());
// the only reason to return false is if the file does not exist or is empty
if (eResult == tinyxml2::XML_ERROR_FILE_NOT_FOUND
| eResult == tinyxml2::XML_ERROR_FILE_COULD_NOT_BE_OPENED
| eResult == tinyxml2::XML_ERROR_FILE_READ_ERROR
| eResult == tinyxml2::XML_ERROR_EMPTY_DOCUMENT )
resetOscConfig();
// found the file, could open and read it
else if (eResult != tinyxml2::XML_SUCCESS)
Log::Warning(CONTROL_OSC_MSG "Error while parsing Translator: %s", xmlDoc.ErrorIDToName(eResult));
// no XML parsing error
else {
// parse all entries 'osc'
tinyxml2::XMLElement* osc = xmlDoc.FirstChildElement("osc");
for( ; osc ; osc=osc->NextSiblingElement()) {
// get the 'from' entry
tinyxml2::XMLElement* from = osc->FirstChildElement("from");
if (from) {
const char *str_from = from->GetText();
if (str_from) {
// get the 'to' entry
tinyxml2::XMLElement* to = osc->FirstChildElement("to");
if (to) {
const char *str_to = to->GetText();
// if could get both; add to translator
if (str_to)
translation_[str_from] = str_to;
}
}
}
}
}
Log::Info(CONTROL_OSC_MSG "Loaded %d translation%s.", translation_.size(), translation_.size()>1?"s":"");
}
void Control::resetOscConfig()
{
// generate a template xml translation dictionnary
tinyxml2::XMLDocument xmlDoc;
tinyxml2::XMLDeclaration *pDec = xmlDoc.NewDeclaration();
xmlDoc.InsertFirstChild(pDec);
tinyxml2::XMLComment *pComment = xmlDoc.NewComment("The OSC translator converts OSC address patterns into other ones.\n"
"Complete the dictionnary by adding as many <osc> translations as you want.\n"
"Each <osc> should contain a <from> pattern to translate into a <to> pattern.\n"
"More at https://github.com/brunoherbelin/vimix/wiki/Open-Sound-Control-API.");
xmlDoc.InsertEndChild(pComment);
tinyxml2::XMLElement *from = xmlDoc.NewElement( "from" );
from->InsertFirstChild( xmlDoc.NewText("/example/osc/message") );
tinyxml2::XMLElement *to = xmlDoc.NewElement( "to" );
to->InsertFirstChild( xmlDoc.NewText("/vimix/info/log") );
tinyxml2::XMLElement *osc = xmlDoc.NewElement("osc");
osc->InsertEndChild(from);
osc->InsertEndChild(to);
xmlDoc.InsertEndChild(osc);
// save xml in osc config file
xmlDoc.SaveFile(Settings::application.control.osc_filename.c_str());
// reset and fill translation with default example
translation_.clear();
translation_["/example/osc/message"] = "/vimix/info/log";
}
bool Control::init()
{
//
// terminate before init (allows calling init() multiple times)
//
terminate();
//
// load OSC Translator
//
loadOscConfig();
//
// launch OSC listener
//
try {
// try to create listenning socket
// through exception runtime if fails
receiver_ = new UdpListeningReceiveSocket( IpEndpointName( IpEndpointName::ANY_ADDRESS,
Settings::application.control.osc_port_receive ), &listener_ );
// listen for answers in a separate thread
std::thread(listen).detach();
// inform user
IpEndpointName ip = receiver_->LocalEndpointFor( IpEndpointName( NetworkToolkit::hostname().c_str(),
Settings::application.control.osc_port_receive ));
static char *addresseip = (char *)malloc(IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH);
ip.AddressAndPortAsString(addresseip);
Log::Info(CONTROL_OSC_MSG "Listening to UDP messages sent to %s", addresseip);
}
catch (const std::runtime_error &e) {
// arg, the receiver could not be initialized
// (often because the port was not available)
receiver_ = nullptr;
Log::Warning(CONTROL_OSC_MSG "Failed to init listener on port %d; %s", Settings::application.control.osc_port_receive, e.what());
}
return receiver_ != nullptr;
}
void Control::listen()
{
if (Control::manager().receiver_)
Control::manager().receiver_->Run();
Control::manager().receiver_end_.notify_all();
}
void Control::terminate()
{
if ( receiver_ != nullptr ) {
// request termination of receiver
receiver_->AsynchronousBreak();
// wait for the receiver_end_ notification
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
// if waited more than 2 seconds, its dead :(
if ( receiver_end_.wait_for(lck,std::chrono::seconds(2)) == std::cv_status::timeout)
Log::Warning(CONTROL_OSC_MSG "Failed to terminate; try again.");
// delete receiver and ready to initialize
delete receiver_;
receiver_ = nullptr;
}
}
bool Control::receiveOutputAttribute(const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments)
{
bool need_feedback = false;
try {
if ( attribute.compare(OSC_SYNC) == 0) {
need_feedback = true;
}
/// e.g. '/vimix/output/enable' or '/vimix/output/enable 1.0' or '/vimix/output/enable 0.0'
else if ( attribute.compare(OSC_OUTPUT_ENABLE) == 0) {
float on = 1.f;
if ( !arguments.Eos()) {
arguments >> on >> osc::EndMessage;
}
Settings::application.render.disabled = on < 0.5f;
}
/// e.g. '/vimix/output/disable' or '/vimix/output/disable 1.0' or '/vimix/output/disable 0.0'
else if ( attribute.compare(OSC_OUTPUT_DISABLE) == 0) {
float on = 1.f;
if ( !arguments.Eos()) {
arguments >> on >> osc::EndMessage;
}
Settings::application.render.disabled = on > 0.5f;
}
/// e.g. '/vimix/output/fading f 0.2' or '/vimix/output/fading ff 1.0 300.f'
else if ( attribute.compare(OSC_OUTPUT_FADING) == 0) {
float f = 0.f, d = 0.f;
// first argument is fading value
arguments >> f;
if (arguments.Eos())
arguments >> osc::EndMessage;
// if a second argument is given, it is a duration
else
arguments >> d >> osc::EndMessage;
Mixer::manager().session()->setFadingTarget(f, d);
}
/// e.g. '/vimix/output/fadein' or '/vimix/output/fadein f 300.f'
else if ( attribute.compare(OSC_OUTPUT_FADE_IN) == 0) {
float f = 0.f;
// if argument is given, it is a duration
if (!arguments.Eos())
arguments >> f >> osc::EndMessage;
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() - f * 0.01);
need_feedback = true;
}
else if ( attribute.compare(OSC_OUTPUT_FADE_OUT) == 0) {
float f = 0.f;
// if argument is given, it is a duration
if (!arguments.Eos())
arguments >> f >> osc::EndMessage;
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() + f * 0.01);
need_feedback = true;
}
#ifdef CONTROL_DEBUG
else {
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'output'", attribute.c_str());
}
#endif
}
catch (osc::MissingArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'output'", attribute.c_str());
}
catch (osc::ExcessArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'output'", attribute.c_str());
}
catch (osc::WrongArgumentTypeException &e) {
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'output'", attribute.c_str());
}
return need_feedback;
}
bool Control::receiveSourceAttribute(Source *target, const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments)
{
bool send_feedback = false;
if (target == nullptr)
return send_feedback;
try {
/// e.g. '/vimix/current/play' or '/vimix/current/play T' or '/vimix/current/play F'
if ( attribute.compare(OSC_SOURCE_PLAY) == 0) {
float on = 1.f;
if ( !arguments.Eos()) {
arguments >> on >> osc::EndMessage;
}
target->call( new SetPlay(on > 0.5f) );
}
/// e.g. '/vimix/current/pause' or '/vimix/current/pause T' or '/vimix/current/pause F'
else if ( attribute.compare(OSC_SOURCE_PAUSE) == 0) {
float on = 1.f;
if ( !arguments.Eos()) {
arguments >> on >> osc::EndMessage;
}
target->call( new SetPlay(on < 0.5f) );
}
/// e.g. '/vimix/current/replay'
else if ( attribute.compare(OSC_SOURCE_REPLAY) == 0) {
target->call( new RePlay() );
}
/// e.g. '/vimix/current/alpha f 0.3'
else if ( attribute.compare(OSC_SOURCE_LOCK) == 0) {
float x = 1.f;
arguments >> x >> osc::EndMessage;
target->call( new SetLock(x > 0.5f ? true : false) );
}
/// e.g. '/vimix/current/alpha f 0.3'
else if ( attribute.compare(OSC_SOURCE_ALPHA) == 0) {
float x = 1.f;
arguments >> x >> osc::EndMessage;
target->call( new SetAlpha(x), true );
}
/// e.g. '/vimix/current/alpha f 0.3'
else if ( attribute.compare(OSC_SOURCE_LOOM) == 0) {
float x = 1.f;
arguments >> x >> osc::EndMessage;
target->call( new Loom(x), true );
// this will require to send feedback status about source
send_feedback = true;
}
/// e.g. '/vimix/current/transparency f 0.7'
else if ( attribute.compare(OSC_SOURCE_TRANSPARENCY) == 0) {
float x = 0.f;
arguments >> x >> osc::EndMessage;
target->call( new SetAlpha(1.f - x), true );
}
/// e.g. '/vimix/current/depth f 5.0'
else if ( attribute.compare(OSC_SOURCE_DEPTH) == 0) {
float x = 0.f;
arguments >> x >> osc::EndMessage;
target->call( new SetDepth(x), true );
}
/// e.g. '/vimix/current/translation ff 10.0 2.2'
else if ( attribute.compare(OSC_SOURCE_GRAB) == 0) {
float x = 0.f, y = 0.f;
arguments >> x >> y >> osc::EndMessage;
target->call( new Grab( x, y), true );
}
/// e.g. '/vimix/current/scale ff 10.0 2.2'
else if ( attribute.compare(OSC_SOURCE_RESIZE) == 0) {
float x = 0.f, y = 0.f;
arguments >> x >> y >> osc::EndMessage;
target->call( new Resize( x, y), true );
}
/// e.g. '/vimix/current/turn f 1.0'
else if ( attribute.compare(OSC_SOURCE_TURN) == 0) {
float x = 0.f, y = 0.f;
arguments >> x;
if (arguments.Eos())
arguments >> osc::EndMessage;
else // ignore second argument
arguments >> y >> osc::EndMessage;
target->call( new Turn( x ), true );
}
/// e.g. '/vimix/current/reset'
else if ( attribute.compare(OSC_SOURCE_RESET) == 0) {
target->call( new ResetGeometry(), true );
}
#ifdef CONTROL_DEBUG
else {
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
}
#endif
// overwrite value if source locked
if (target->locked())
send_feedback = true;
}
catch (osc::MissingArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
}
catch (osc::ExcessArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
}
catch (osc::WrongArgumentTypeException &e) {
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
}
return send_feedback;
}
bool Control::receiveSessionAttribute(const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments)
{
bool send_feedback = false;
try {
if ( attribute.compare(OSC_SYNC) == 0) {
send_feedback = true;
}
else if ( attribute.compare(OSC_SESSION_VERSION) == 0) {
float v = 0.f;
arguments >> v >> osc::EndMessage;
size_t id = (int) ceil(v);
std::list<uint64_t> snapshots = Action::manager().snapshots();
if ( id < snapshots.size() ) {
for (size_t i = 0; i < id; ++i)
snapshots.pop_back();
uint64_t snap = snapshots.back();
Action::manager().restore(snap);
}
send_feedback = true;
}
#ifdef CONTROL_DEBUG
else {
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'session'", attribute.c_str());
}
#endif
}
catch (osc::MissingArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'session'", attribute.c_str());
}
catch (osc::ExcessArgumentException &e) {
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'session'", attribute.c_str());
}
catch (osc::WrongArgumentTypeException &e) {
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'session'", attribute.c_str());
}
return send_feedback;
}
void Control::sendSourceAttibutes(const IpEndpointName &remoteEndpoint, std::string target, Source *s)
{
// default values
char name[21] = {"\0"};
float lock = 0.f;
float play = 0.f;
float depth = 0.f;
float alpha = 0.f;
// get source or current source
Source *_s = s;
if ( target.compare(OSC_CURRENT) == 0 )
_s = Mixer::manager().currentSource();
// fill values if the source is valid
if (_s!=nullptr) {
strncpy(name, _s->name().c_str(), 20);
lock = _s->locked() ? 1.f : 0.f;
play = _s->playing() ? 1.f : 0.f;
depth = _s->depth();
alpha = _s->alpha();
}
// build socket to send message to indicated endpoint
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
// build messages packet
char buffer[IP_MTU_SIZE];
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
// create bundle
p.Clear();
p << osc::BeginBundle();
/// name
std::string address = std::string(OSC_PREFIX) + target + OSC_SOURCE_NAME;
p << osc::BeginMessage( address.c_str() ) << name << osc::EndMessage;
/// Play status
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_LOCK;
p << osc::BeginMessage( address.c_str() ) << lock << osc::EndMessage;
/// Play status
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_PLAY;
p << osc::BeginMessage( address.c_str() ) << play << osc::EndMessage;
/// Depth
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_DEPTH;
p << osc::BeginMessage( address.c_str() ) << depth << osc::EndMessage;
/// Alpha
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_ALPHA;
p << osc::BeginMessage( address.c_str() ) << alpha << osc::EndMessage;
// send bundle
p << osc::EndBundle;
socket.Send( p.Data(), p.Size() );
}
void Control::sendSourcesStatus(const IpEndpointName &remoteEndpoint, osc::ReceivedMessageArgumentStream arguments)
{
// (if an argument is given, it indicates the number of sources to update)
float N = 0.f;
if ( !arguments.Eos())
arguments >> N >> osc::EndMessage;
// build socket to send message to indicated endpoint
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
// build messages packet
char buffer[IP_MTU_SIZE];
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
p.Clear();
p << osc::BeginBundle();
int i = 0;
char oscaddr[128];
int index_current = Mixer::manager().indexCurrentSource();
for (; i < Mixer::manager().count(); ++i) {
// send status of currently selected
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
p << osc::BeginMessage( oscaddr ) << (index_current == i ? 1.f : 0.f) << osc::EndMessage;
// send status of alpha
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
p << osc::BeginMessage( oscaddr ) << Mixer::manager().sourceAtIndex(i)->alpha() << osc::EndMessage;
}
for (; i < (int) N ; ++i) {
// reset status of currently selected
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
// reset status of alpha
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
}
p << osc::EndBundle;
socket.Send( p.Data(), p.Size() );
// send status of current source
sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
}
void Control::sendOutputStatus(const IpEndpointName &remoteEndpoint)
{
// build socket to send message to indicated endpoint
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
// build messages packet
char buffer[IP_MTU_SIZE];
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
p.Clear();
p << osc::BeginBundle();
/// output attributes
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_ENABLE );
p << (Settings::application.render.disabled ? 0.f : 1.f);
p << osc::EndMessage;
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_FADING );
p << Mixer::manager().session()->fading();
p << osc::EndMessage;
p << osc::EndBundle;
socket.Send( p.Data(), p.Size() );
}

105
ControlManager.h Normal file
View File

@@ -0,0 +1,105 @@
#ifndef CONTROL_H
#define CONTROL_H
#include <map>
#include <string>
#include <condition_variable>
#include "NetworkToolkit.h"
#define OSC_SYNC "/sync"
#define OSC_INFO "/info"
#define OSC_INFO_LOG "/log"
#define OSC_INFO_NOTIFY "/notify"
#define OSC_OUTPUT "/output"
#define OSC_OUTPUT_ENABLE "/enable"
#define OSC_OUTPUT_DISABLE "/disable"
#define OSC_OUTPUT_FADING "/fading"
#define OSC_OUTPUT_FADE_IN "/fade-in"
#define OSC_OUTPUT_FADE_OUT "/fade-out"
#define OSC_ALL "/all"
#define OSC_SELECTED "/selected"
#define OSC_CURRENT "/current"
#define OSC_NEXT "/next"
#define OSC_PREVIOUS "/previous"
#define OSC_SOURCE_NAME "/name"
#define OSC_SOURCE_LOCK "/lock"
#define OSC_SOURCE_PLAY "/play"
#define OSC_SOURCE_PAUSE "/pause"
#define OSC_SOURCE_REPLAY "/replay"
#define OSC_SOURCE_ALPHA "/alpha"
#define OSC_SOURCE_LOOM "/loom"
#define OSC_SOURCE_TRANSPARENCY "/transparency"
#define OSC_SOURCE_DEPTH "/depth"
#define OSC_SOURCE_GRAB "/grab"
#define OSC_SOURCE_RESIZE "/resize"
#define OSC_SOURCE_TURN "/turn"
#define OSC_SOURCE_RESET "/reset"
#define OSC_SESSION "/session"
#define OSC_SESSION_VERSION "/version"
class Session;
class Source;
class Control
{
// Private Constructor
Control();
Control(Control const& copy) = delete;
Control& operator=(Control const& copy) = delete;
public:
static Control& manager ()
{
// The only instance
static Control _instance;
return _instance;
}
~Control();
bool init();
void terminate();
std::string translate (std::string addresspattern);
protected:
class RequestListener : public osc::OscPacketListener {
protected:
virtual void ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint );
std::string FullMessage( const osc::ReceivedMessage& m );
};
bool receiveOutputAttribute(const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments);
bool receiveSourceAttribute(Source *target, const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments);
bool receiveSessionAttribute(const std::string &attribute,
osc::ReceivedMessageArgumentStream arguments);
void sendSourceAttibutes(const IpEndpointName& remoteEndpoint, std::string target, Source *s = nullptr);
void sendSourcesStatus(const IpEndpointName& remoteEndpoint, osc::ReceivedMessageArgumentStream arguments);
void sendOutputStatus(const IpEndpointName& remoteEndpoint);
private:
static void listen();
RequestListener listener_;
std::condition_variable receiver_end_;
UdpListeningReceiveSocket *receiver_;
std::map<std::string, std::string> translation_;
void loadOscConfig();
void resetOscConfig();
};
#endif // CONTROL_H

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - Live video mixer
*
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "GlmToolkit.h"
#include "Scene.h"
#include "Source.h"

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/vector_angle.hpp>
#include <glm/gtx/component_wise.hpp>
@@ -11,6 +30,8 @@
#include "Resource.h"
#include "Log.h"
#include "imgui.h"
#include <glad/glad.h>
Frame::Frame(CornerType corner, BorderType border, ShadowType shadow) : Node(),
right_(nullptr), left_(nullptr), top_(nullptr), shadow_(nullptr), square_(nullptr)
@@ -249,7 +270,7 @@ void Handles::draw(glm::mat4 modelview, glm::mat4 projection)
init();
}
if ( visible_ ) {
if ( visible_ && handle_) {
static Mesh *handle_active = new Mesh("mesh/border_handles_overlay_filled.ply");
// set color
@@ -526,11 +547,6 @@ Disk::Disk() : Node()
color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
}
Disk::~Disk()
{
}
void Disk::draw(glm::mat4 modelview, glm::mat4 projection)
{
if ( !initialized() ) {
@@ -556,55 +572,88 @@ void Disk::accept(Visitor& v)
v.visit(*this);
}
Surface *Glyph::font_ = nullptr;
//SelectionBox::SelectionBox()
//{
//// color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
// color = glm::vec4( 1.f, 0.f, 0.f, 1.f);
// square_ = new LineSquare( 3 );
Glyph::Glyph(int imgui_font_index) : Node(), character_(' '), font_index_(imgui_font_index), baseline_(0.f), shape_(1.f, 1.f)
{
if (Glyph::font_ == nullptr)
Glyph::font_ = new Surface;
//}
uvTransform_ = glm::identity<glm::mat4>();
color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
}
//void SelectionBox::draw (glm::mat4 modelview, glm::mat4 projection)
//{
// if ( !initialized() ) {
// square_->init();
// init();
// }
void Glyph::draw(glm::mat4 modelview, glm::mat4 projection)
{
if ( !initialized() ) {
// if (visible_) {
if (!Glyph::font_->initialized()) {
// // use a visitor bounding box to calculate extend of all selected nodes
// BoundingBoxVisitor vbox;
uint tex = (uint)(intptr_t)ImGui::GetIO().Fonts->TexID;
if ( tex > 0) {
Glyph::font_->init();
font_->setTextureIndex(tex);
}
}
// // visit every child of the selection
// for (NodeSet::iterator node = children_.begin();
// node != children_.end(); node++) {
// // reset the transform before
// vbox.setModelview(glm::identity<glm::mat4>());
// (*node)->accept(vbox);
// }
if (Glyph::font_->initialized()) {
init();
setChar(character_);
}
}
// // get the bounding box
// bbox_ = vbox.bbox();
if ( visible_ ) {
//// Log::Info(" -------- visitor box (%f, %f)-(%f, %f)", bbox_.min().x, bbox_.min().y, bbox_.max().x, bbox_.max().y);
// set color
Glyph::font_->shader()->color = color;
// // set color
// square_->shader()->color = color;
// modify the shader iTransform to select UVs of the desired glyph
Glyph::font_->shader()->iTransform = uvTransform_;
// // compute transformation from bounding box
//// glm::mat4 ctm = modelview * GlmToolkit::transform(glm::vec3(0.f), glm::vec3(0.f), glm::vec3(1.f));
// glm::mat4 ctm = modelview * GlmToolkit::transform(bbox_.center(), glm::vec3(0.f), bbox_.scale());
// rebuild a matrix with rotation (see handles) and translation from modelview + translation_
// and define scale to be 1, 1
glm::mat4 ctm;
glm::vec3 rot(0.f);
glm::vec4 vec = modelview * glm::vec4(1.f, 0.f, 0.f, 0.f);
rot.z = glm::orientedAngle( glm::vec3(1.f, 0.f, 0.f), glm::normalize(glm::vec3(vec)), glm::vec3(0.f, 0.f, 1.f) );
// extract scaling
ctm = glm::rotate(glm::identity<glm::mat4>(), -rot.z, glm::vec3(0.f, 0.f, 1.f)) * modelview ;
vec = ctm * glm::vec4(1.f, 1.f, 0.f, 0.f);
glm::vec2 sca = glm::vec2(vec.y) * glm::vec2(scale_.y) * shape_;
// extract translation
glm::vec3 tran = glm::vec3(modelview[3][0], modelview[3][1], modelview[3][2]) ;
tran += (translation_ + glm::vec3(0.f, baseline_ * scale_.y, 0.f) ) * glm::vec3(vec);
// apply local rotation
rot.z += rotation_.z;
// generate matrix
ctm = GlmToolkit::transform(tran, rot, glm::vec3(sca, 1.f));
// // draw bbox
//// square_->draw( modelview, projection);
// square_->draw( ctm, projection);
Glyph::font_->draw( ctm, projection);
}
}
// // DEBUG
//// visible_=false;
// }
void Glyph::setChar(char c)
{
character_ = c;
//}
if (initialized()) {
// get from imgui the UVs of the given char
const ImGuiIO& io = ImGui::GetIO();
ImFont* myfont = io.Fonts->Fonts[0];
if (font_index_ > 0 && font_index_ < io.Fonts->Fonts.size() )
myfont = io.Fonts->Fonts[font_index_];
const ImFontGlyph* glyph = myfont->FindGlyph(character_);
if (glyph) {
// create a texture UV transform to get the UV coordinates of the glyph
const glm::vec3 uv_t = glm::vec3( glyph->U0, glyph->V0, 0.f);
const glm::vec3 uv_s = glm::vec3( glyph->U1 - glyph->U0, glyph->V1 - glyph->V0, 1.f);
const glm::vec3 uv_r = glm::vec3(0.f, 0.f, 0.f);
uvTransform_ = GlmToolkit::transform(uv_t, uv_r, uv_s);
// remember glyph shape
shape_ = glm::vec2(glyph->X1 - glyph->X0, glyph->Y1 - glyph->Y0) / myfont->FontSize;
baseline_ = -glyph->Y0 / myfont->FontSize;
}
}
}

View File

@@ -83,7 +83,6 @@ class Disk : public Node
{
public:
Disk();
~Disk();
void draw (glm::mat4 modelview, glm::mat4 projection) override;
void accept (Visitor& v) override;
@@ -94,20 +93,26 @@ protected:
static Mesh *disk_;
};
//class SelectionBox : public Group
//{
//public:
// SelectionBox();
class Glyph : public Node
{
public:
Glyph(int imgui_font_index = 0);
void setChar(char c);
// void draw (glm::mat4 modelview, glm::mat4 projection) override;
void draw (glm::mat4 modelview, glm::mat4 projection) override;
// glm::vec4 color;
glm::vec4 color;
//protected:
// LineSquare *square_;
// GlmToolkit::AxisAlignedBoundingBox bbox_;
protected:
//};
char character_;
int font_index_;
float baseline_;
glm::vec2 shape_;
glm::mat4 uvTransform_;
static Surface *font_;
};
#endif // DECORATIONS_H

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
@@ -19,7 +38,7 @@
#ifndef NDEBUG
#define DEVICE_DEBUG
//#define GST_DEVICE_DEBUG
#define GST_DEVICE_DEBUG
#endif
@@ -101,69 +120,82 @@ gboolean
Device::callback_device_monitor (GstBus *, GstMessage * message, gpointer )
{
GstDevice *device;
gchar *name;
switch (GST_MESSAGE_TYPE (message)) {
case GST_MESSAGE_DEVICE_ADDED: {
case GST_MESSAGE_DEVICE_ADDED:
{
gst_message_parse_device_added (message, &device);
name = gst_device_get_display_name (device);
// ignore if already in the list
if ( std::find(manager().src_name_.begin(), manager().src_name_.end(), name) != manager().src_name_.end())
break;
manager().src_name_.push_back(name);
#ifdef GST_DEVICE_DEBUG
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
g_print("\nDevice %s plugged : %s\n", name, stru);
g_free (stru);
#endif
g_free (name);
std::string p = pipelineForDevice(device, manager().src_description_.size());
manager().src_description_.push_back(p);
DeviceConfigSet confs = getDeviceConfigs(p);
manager().src_config_.push_back(confs);
manager().list_uptodate_ = false;
manager().add(device);
gst_object_unref (device);
}
break;
case GST_MESSAGE_DEVICE_REMOVED: {
case GST_MESSAGE_DEVICE_REMOVED:
{
gst_message_parse_device_removed (message, &device);
name = gst_device_get_display_name (device);
manager().remove(name);
#ifdef GST_DEVICE_DEBUG
g_print("\nDevice %s unplugged\n", name);
#endif
g_free (name);
manager().list_uptodate_ = false;
manager().remove(device);
gst_object_unref (device);
}
break;
default:
default:
break;
}
return G_SOURCE_CONTINUE;
}
void Device::remove(const char *device)
void Device::add(GstDevice *device)
{
if (device==nullptr)
return;
gchar *device_name = gst_device_get_display_name (device);
if ( std::find(manager().src_name_.begin(), manager().src_name_.end(), device_name) == manager().src_name_.end()) {
std::string p = pipelineForDevice(device, manager().src_description_.size());
DeviceConfigSet confs = getDeviceConfigs(p);
// add if not in the list and valid
if (!p.empty() && !confs.empty()) {
src_name_.push_back(device_name);
src_description_.push_back(p);
src_config_.push_back(confs);
#ifdef GST_DEVICE_DEBUG
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
g_print("\nDevice %s plugged : %s\n", device_name, stru);
g_free (stru);
#endif
}
list_uptodate_ = false;
}
g_free (device_name);
}
void Device::remove(GstDevice *device)
{
if (device==nullptr)
return;
gchar *device_name = gst_device_get_display_name (device);
std::vector< std::string >::iterator nameit = src_name_.begin();
std::vector< std::string >::iterator descit = src_description_.begin();
std::vector< DeviceConfigSet >::iterator coit = src_config_.begin();
while (nameit != src_name_.end()){
if ( (*nameit).compare(device) == 0 )
if ( (*nameit).compare(device_name) == 0 )
{
src_name_.erase(nameit);
src_description_.erase(descit);
src_config_.erase(coit);
list_uptodate_ = false;
#ifdef GST_DEVICE_DEBUG
g_print("\nDevice %s unplugged\n", device_name);
#endif
break;
}
@@ -171,10 +203,16 @@ void Device::remove(const char *device)
++descit;
++coit;
}
g_free (device_name);
}
Device::Device()
Device::Device(): list_uptodate_(false)
{
}
void Device::init()
{
GstBus *bus;
GstCaps *caps;
@@ -198,24 +236,9 @@ Device::Device()
GList *devices = gst_device_monitor_get_devices(monitor_);
GList *tmp;
for (tmp = devices; tmp ; tmp = tmp->next ) {
GstDevice *device = (GstDevice *) tmp->data;
gchar *name = gst_device_get_display_name (device);
src_name_.push_back(name);
g_free (name);
#ifdef GST_DEVICE_DEBUG
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
g_print("\nDevice %s already plugged : %s", name, stru);
g_free (stru);
#endif
std::string p = pipelineForDevice(device, src_description_.size());
src_description_.push_back(p);
DeviceConfigSet confs = getDeviceConfigs(p);
src_config_.push_back(confs);
add(device);
gst_object_unref (device);
}
g_list_free(devices);
@@ -344,7 +367,6 @@ DeviceSource::~DeviceSource()
void DeviceSource::setDevice(const std::string &devicename)
{
device_ = devicename;
Log::Notify("Creating Source with device '%s'", device_.c_str());
int index = Device::manager().index(device_);
if (index > -1) {
@@ -365,28 +387,34 @@ void DeviceSource::setDevice(const std::string &devicename)
Log::Info(" - %s %s %d x %d %.1f fps", (*it).stream.c_str(), (*it).format.c_str(), (*it).width, (*it).height, fps);
}
#endif
DeviceConfig best = *confs.rbegin();
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
Log::Info("Device %s selected its optimal config: %s %s %dx%d@%.1ffps", device_.c_str(), best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
if (!confs.empty()) {
DeviceConfig best = *confs.rbegin();
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
Log::Info("Device %s selected its optimal config: %s %s %dx%d@%.1ffps", device_.c_str(), best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
pipeline << " ! " << best.stream;
if (!best.format.empty())
pipeline << ",format=" << best.format;
pipeline << ",framerate=" << best.fps_numerator << "/" << best.fps_denominator;
pipeline << ",width=" << best.width;
pipeline << ",height=" << best.height;
pipeline << " ! " << best.stream;
if (!best.format.empty())
pipeline << ",format=" << best.format;
pipeline << ",framerate=" << best.fps_numerator << "/" << best.fps_denominator;
pipeline << ",width=" << best.width;
pipeline << ",height=" << best.height;
if ( best.stream.find("jpeg") != std::string::npos )
pipeline << " ! jpegdec";
if ( best.stream.find("jpeg") != std::string::npos )
pipeline << " ! jpegdec";
if ( device_.find("Screen") != std::string::npos )
pipeline << " ! videoconvert ! video/x-raw,format=RGB ! queue max-size-buffers=3";
if ( device_.find("Screen") != std::string::npos )
pipeline << " ! videoconvert ! video/x-raw,format=RGB ! queue max-size-buffers=3";
pipeline << " ! videoconvert";
pipeline << " ! videoconvert";
// open gstreamer
stream_->open( pipeline.str(), best.width, best.height);
stream_->play(true);
// resize render buffer
if (renderbuffer_)
renderbuffer_->resize(best.width, best.height);
// open gstreamer
stream_->open( pipeline.str(), best.width, best.height);
stream_->play(true);
}
// will be ready after init and one frame rendered
ready_ = false;
@@ -561,9 +589,14 @@ DeviceConfigSet Device::getDeviceConfigs(const std::string &src_description)
glm::ivec2 DeviceSource::icon() const
{
if ( device_.find("Screen") != std::string::npos )
return glm::ivec2(19, 1);
return glm::ivec2(ICON_SOURCE_DEVICE_SCREEN);
else
return glm::ivec2(2, 14);
return glm::ivec2(ICON_SOURCE_DEVICE);
}
std::string DeviceSource::info() const
{
return std::string("device '") + device_ + "'";
}

View File

@@ -25,6 +25,7 @@ public:
inline std::string device() const { return device_; }
glm::ivec2 icon() const override;
std::string info() const override;
private:
std::string device_;
@@ -99,6 +100,7 @@ public:
return _instance;
}
void init();
int numDevices () const;
std::string name (int index) const;
std::string description (int index) const;
@@ -115,7 +117,8 @@ public:
private:
void remove(const char *device);
void remove(GstDevice *device);
void add(GstDevice *device);
std::vector< std::string > src_name_;
std::vector< std::string > src_description_;

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
// multiplatform implementation of system dialogs
//
@@ -6,16 +25,17 @@
// because of 'zenity' access rights nightmare :(
// Thus this re-implementation of native GTK+ dialogs for linux
#include "defines.h"
#include "Settings.h"
#include "SystemToolkit.h"
#include "DialogToolkit.h"
#if defined(LINUX)
#define USE_TINYFILEDIALOG 0
#else
#define USE_TINYFILEDIALOG 1
#endif
#include "defines.h"
#include "SystemToolkit.h"
#include "DialogToolkit.h"
#if USE_TINYFILEDIALOG
#include "tinyfiledialogs.h"
#else
@@ -40,8 +60,10 @@ void add_filter_file_dialog( GtkWidget *dialog, int const countfilterPatterns, c
GtkFileFilter *filter;
filter = gtk_file_filter_new();
gtk_file_filter_set_name( filter, filterDescription );
for (int i = 0; i < countfilterPatterns; i++ )
gtk_file_filter_add_pattern( filter, filterPatterns[i] );
for (int i = 0; i < countfilterPatterns; i++ ) {
gtk_file_filter_add_pattern( filter, g_ascii_strdown(filterPatterns[i], -1) );
gtk_file_filter_add_pattern( filter, g_ascii_strup(filterPatterns[i], -1) );
}
gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter );
}
@@ -61,25 +83,156 @@ bool gtk_init()
}
#endif
std::string DialogToolkit::saveSessionFileDialog(const std::string &path)
// globals
const std::chrono::milliseconds timeout = std::chrono::milliseconds(4);
bool DialogToolkit::FileDialog::busy_ = false;
//
// FileDialog common functions
//
DialogToolkit::FileDialog::FileDialog(const std::string &name) : id_(name)
{
if ( Settings::application.dialogRecentFolder.count(id_) == 0 )
Settings::application.dialogRecentFolder[id_] = SystemToolkit::home_path();
}
bool DialogToolkit::FileDialog::closed()
{
if ( !promises_.empty() ) {
// check that file dialog thread finished
if (promises_.back().wait_for(timeout) == std::future_status::ready ) {
// get the filename from this file dialog
path_ = promises_.back().get();
if (!path_.empty()) {
// save path location
Settings::application.dialogRecentFolder[id_] = SystemToolkit::path_filename(path_);
}
// done with this file dialog
promises_.pop_back();
busy_ = false;
return true;
}
}
return false;
}
//
// type specific implementations
//
std::string openImageFileDialog(const std::string &label, const std::string &path);
void DialogToolkit::OpenImageDialog::open()
{
if ( !busy_ && promises_.empty() ) {
promises_.emplace_back( std::async(std::launch::async, openImageFileDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
std::string openSessionFileDialog(const std::string &label, const std::string &path);
void DialogToolkit::OpenSessionDialog::open()
{
if ( !busy_ && promises_.empty() ) {
promises_.emplace_back( std::async(std::launch::async, openSessionFileDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
std::string openMediaFileDialog(const std::string &label, const std::string &path);
void DialogToolkit::OpenMediaDialog::open()
{
if ( !busy_ && promises_.empty() ) {
promises_.emplace_back( std::async(std::launch::async, openMediaFileDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
std::string saveSessionFileDialog(const std::string &label, const std::string &path);
void DialogToolkit::SaveSessionDialog::open()
{
if ( !busy_ && promises_.empty() ) {
promises_.emplace_back( std::async(std::launch::async, saveSessionFileDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
std::string openFolderDialog(const std::string &label, const std::string &path);
void DialogToolkit::OpenFolderDialog::open()
{
if ( !busy_ && promises_.empty() ) {
promises_.emplace_back( std::async(std::launch::async, openFolderDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
std::list<std::string> selectImagesFileDialog(const std::string &label,const std::string &path);
void DialogToolkit::MultipleImagesDialog::open()
{
if ( !busy_ && promisedlist_.empty() ) {
promisedlist_.emplace_back( std::async(std::launch::async, selectImagesFileDialog, id_,
Settings::application.dialogRecentFolder[id_]) );
busy_ = true;
}
}
bool DialogToolkit::MultipleImagesDialog::closed()
{
if ( !promisedlist_.empty() ) {
// check that file dialog thread finished
if (promisedlist_.back().wait_for(timeout) == std::future_status::ready ) {
// get the filename from this file dialog
std::list<std::string> list = promisedlist_.back().get();
if (!list.empty()) {
// selected a filenames
pathlist_ = list;
path_ = list.front();
// save path location
Settings::application.dialogRecentFolder[id_] = SystemToolkit::path_filename(path_);
}
else {
pathlist_.clear();
path_.clear();
}
// done with this file dialog
promisedlist_.pop_back();
busy_ = false;
return true;
}
}
return false;
}
//
//
// CALLBACKS
//
//
std::string saveSessionFileDialog(const std::string &label, const std::string &path)
{
std::string filename = "";
char const * save_pattern[1] = { "*.mix" };
char const * save_pattern[1] = { VIMIX_FILES_PATTERN };
#if USE_TINYFILEDIALOG
char const * save_file_name;
save_file_name = tinyfd_saveFileDialog( "Save a session file", path.c_str(), 1, save_pattern, "vimix session");
save_file_name = tinyfd_saveFileDialog( label.c_str(), path.c_str(), 1, save_pattern, "vimix session");
if (save_file_name)
filename = std::string(save_file_name);
#else
if (!gtk_init()) {
ErrorDialog("Could not initialize GTK+ for dialog");
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return filename;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Save Session File", NULL,
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT, NULL );
@@ -115,32 +268,32 @@ std::string DialogToolkit::saveSessionFileDialog(const std::string &path)
#endif
std::string extension = filename.substr(filename.find_last_of(".") + 1);
if (extension != "mix")
if (!filename.empty() && extension != "mix")
filename += ".mix";
return filename;
}
std::string DialogToolkit::openSessionFileDialog(const std::string &path)
std::string openSessionFileDialog(const std::string &label, const std::string &path)
{
std::string filename = "";
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
char const * open_pattern[1] = { "*.mix" };
char const * open_pattern[1] = { VIMIX_FILES_PATTERN };
#if USE_TINYFILEDIALOG
char const * open_file_name;
open_file_name = tinyfd_openFileDialog( "Import a file", startpath.c_str(), 1, open_pattern, "vimix session", 0);
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 1, open_pattern, "vimix session", 0);
if (open_file_name)
filename = std::string(open_file_name);
#else
if (!gtk_init()) {
ErrorDialog("Could not initialize GTK+ for dialog");
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return filename;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open Session File", NULL,
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL );
@@ -178,36 +331,32 @@ std::string DialogToolkit::openSessionFileDialog(const std::string &path)
}
std::string DialogToolkit::openMediaFileDialog(const std::string &path)
std::string openMediaFileDialog(const std::string &label, const std::string &path)
{
std::string filename = "";
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
char const * open_pattern[18] = { "*.mix", "*.mp4", "*.mpg",
"*.avi", "*.mov", "*.mkv",
"*.webm", "*.mod", "*.wmv",
"*.mxf", "*.ogg", "*.flv",
"*.asf", "*.jpg", "*.png",
"*.gif", "*.tif", "*.svg" };
char const * open_pattern[26] = { MEDIA_FILES_PATTERN };
#if USE_TINYFILEDIALOG
char const * open_file_name;
open_file_name = tinyfd_openFileDialog( "Open Media File", startpath.c_str(), 18, open_pattern, "All supported formats", 0);
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 26, open_pattern, "All supported formats", 0);
if (open_file_name)
filename = std::string(open_file_name);
#else
if (!gtk_init()) {
ErrorDialog("Could not initialize GTK+ for dialog");
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return filename;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open Media File", NULL,
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL );
// set file filters
add_filter_file_dialog(dialog, 18, open_pattern, "All supported formats");
add_filter_file_dialog(dialog, 26, open_pattern, "Supported formats (videos, images, sessions)");
add_filter_any_file_dialog(dialog);
// Set the default path
@@ -238,24 +387,82 @@ std::string DialogToolkit::openMediaFileDialog(const std::string &path)
return filename;
}
std::string DialogToolkit::openFolderDialog(const std::string &path)
std::string openImageFileDialog(const std::string &label, const std::string &path)
{
std::string filename = "";
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
char const * open_pattern[5] = { IMAGES_FILES_PATTERN };
#if USE_TINYFILEDIALOG
char const * open_file_name;
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 5, open_pattern, "Image (JPG, PNG, BMP, PPM, GIF)", 0);
if (open_file_name)
filename = std::string(open_file_name);
#else
if (!gtk_init()) {
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return filename;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL );
// set file filters
add_filter_file_dialog(dialog, 5, open_pattern, "Image (JPG, PNG, BMP, PPM, GIF)");
add_filter_any_file_dialog(dialog);
// Set the default path
gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER(dialog), startpath.c_str() );
// ensure front and centered
gtk_window_set_keep_above( GTK_WINDOW(dialog), TRUE );
if (window_x > 0 && window_y > 0)
gtk_window_move( GTK_WINDOW(dialog), window_x, window_y);
// display and get filename
if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) {
char *open_file_name = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
if (open_file_name)
filename = std::string(open_file_name);
g_free( open_file_name );
}
// remember position
gtk_window_get_position( GTK_WINDOW(dialog), &window_x, &window_y);
// done
gtk_widget_destroy(dialog);
wait_for_event();
#endif
return filename;
}
std::string openFolderDialog(const std::string &label, const std::string &path)
{
std::string foldername = "";
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
#if USE_TINYFILEDIALOG
char const * open_folder_name;
open_folder_name = tinyfd_selectFolderDialog("Select folder", startpath.c_str());
open_folder_name = tinyfd_selectFolderDialog(label.c_str(), startpath.c_str());
if (open_folder_name)
foldername = std::string(open_folder_name);
#else
if (!gtk_init()) {
ErrorDialog("Could not initialize GTK+ for dialog");
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return foldername;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Select folder", NULL,
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Select", GTK_RESPONSE_ACCEPT, NULL );
@@ -290,16 +497,16 @@ std::string DialogToolkit::openFolderDialog(const std::string &path)
}
std::list<std::string> DialogToolkit::selectImagesFileDialog(const std::string &path)
std::list<std::string> selectImagesFileDialog(const std::string &label,const std::string &path)
{
std::list<std::string> files;
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
char const * open_pattern[3] = { "*.tif", "*.jpg", "*.png" };
char const * open_pattern[6] = { "*.jpg", "*.png", "*.tif" };
#if USE_TINYFILEDIALOG
char const * open_file_names;
open_file_names = tinyfd_openFileDialog( "Select images", startpath.c_str(), 3, open_pattern, "Images", 1);
open_file_names = tinyfd_openFileDialog(label.c_str(), startpath.c_str(), 3, open_pattern, "Images (JPG, PNG, TIF)", 1);
if (open_file_names) {
@@ -325,17 +532,17 @@ std::list<std::string> DialogToolkit::selectImagesFileDialog(const std::string &
#else
if (!gtk_init()) {
ErrorDialog("Could not initialize GTK+ for dialog");
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
return files;
}
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Select images", NULL,
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT, NULL );
// set file filters
add_filter_file_dialog(dialog, 3, open_pattern, "All supported formats");
add_filter_file_dialog(dialog, 3, open_pattern, "Images (JPG, PNG, TIF)");
add_filter_any_file_dialog(dialog);
// multiple files

View File

@@ -3,23 +3,86 @@
#include <string>
#include <list>
#include <vector>
#include <future>
#define VIMIX_FILES_PATTERN "*.mix"
#define MEDIA_FILES_PATTERN "*.mix", "*.mp4", "*.mpg", "*.mpeg", "*.m2v", "*.m4v", "*.avi", "*.mov",\
"*.mkv", "*.webm", "*.mod", "*.wmv", "*.mxf", "*.ogg",\
"*.flv", "*.hevc", "*.asf", "*.jpg", "*.png", "*.gif",\
"*.tif", "*.tiff", "*.webp", "*.bmp", "*.ppm", "*.svg,"
#define IMAGES_FILES_PATTERN "*.jpg", "*.png", "*.bmp", "*.ppm", "*.gif"
namespace DialogToolkit
{
std::string saveSessionFileDialog(const std::string &path);
std::string openSessionFileDialog(const std::string &path);
std::string openMediaFileDialog(const std::string &path);
std::string openFolderDialog(const std::string &path);
std::list<std::string> selectImagesFileDialog(const std::string &path);
void ErrorDialog(const char* message);
class FileDialog
{
protected:
std::string id_;
std::string directory_;
std::string path_;
std::vector< std::future<std::string> >promises_;
static bool busy_;
public:
FileDialog(const std::string &name);
virtual void open() = 0;
virtual bool closed();
inline std::string path() const { return path_; }
static bool busy() { return busy_; }
};
class OpenImageDialog : public FileDialog
{
public:
OpenImageDialog(const std::string &name) : FileDialog(name) {}
void open();
};
class OpenSessionDialog : public FileDialog
{
public:
OpenSessionDialog(const std::string &name) : FileDialog(name) {}
void open();
};
class OpenMediaDialog : public FileDialog
{
public:
OpenMediaDialog(const std::string &name) : FileDialog(name) {}
void open();
};
class SaveSessionDialog : public FileDialog
{
public:
SaveSessionDialog(const std::string &name) : FileDialog(name) {}
void open();
};
class OpenFolderDialog : public FileDialog
{
public:
OpenFolderDialog(const std::string &name) : FileDialog(name) {}
void open();
};
class MultipleImagesDialog : public FileDialog
{
std::list<std::string> pathlist_;
std::vector< std::future< std::list<std::string> > > promisedlist_;
public:
MultipleImagesDialog(const std::string &name) : FileDialog(name) {}
void open() override;
bool closed() override;
inline std::list<std::string> images() const { return pathlist_; }
};
}

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <glm/gtc/matrix_transform.hpp>
@@ -17,8 +36,8 @@ DrawVisitor::DrawVisitor(Node *nodetodraw, glm::mat4 projection, bool force): fo
}
DrawVisitor::DrawVisitor(const std::vector<Node *> &nodestodraw, glm::mat4 projection, bool force):
force_(force), targets_(nodestodraw), modelview_(glm::identity<glm::mat4>()),
projection_(projection), num_duplicat_(1), transform_duplicat_(glm::identity<glm::mat4>())
modelview_(glm::identity<glm::mat4>()), projection_(projection), targets_(nodestodraw), force_(force),
num_duplicat_(1), transform_duplicat_(glm::identity<glm::mat4>())
{
}
@@ -93,6 +112,6 @@ void DrawVisitor::visit(Switch &n)
modelview_ = mv;
}
void DrawVisitor::visit(Primitive &n)
void DrawVisitor::visit(Primitive &)
{
}

View File

@@ -21,7 +21,7 @@ public:
void visit(Scene& n) override;
void visit(Node& n) override;
void visit(Primitive& n) override;
void visit(Primitive& ) override;
void visit(Group& n) override;
void visit(Switch& n) override;
};

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - Live video mixer
*
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <fstream>
#include <iostream>
#include <sstream>

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <sstream>
#include "FrameBuffer.h"
@@ -14,13 +33,14 @@
const char* FrameBuffer::aspect_ratio_name[5] = { "4:3", "3:2", "16:10", "16:9", "21:9" };
glm::vec2 FrameBuffer::aspect_ratio_size[5] = { glm::vec2(4.f,3.f), glm::vec2(3.f,2.f), glm::vec2(16.f,10.f), glm::vec2(16.f,9.f) , glm::vec2(21.f,9.f) };
const char* FrameBuffer::resolution_name[4] = { "720", "1080", "1440", "2160" };
float FrameBuffer::resolution_height[4] = { 720.f, 1080.f, 1440.f, 2160.f };
const char* FrameBuffer::resolution_name[5] = { "720", "1080", "1200", "1440", "2160" };
float FrameBuffer::resolution_height[5] = { 720.f, 1080.f, 1200.f, 1440.f, 2160.f };
glm::vec3 FrameBuffer::getResolutionFromParameters(int ar, int h)
{
float width = aspect_ratio_size[ar].x * resolution_height[h] / aspect_ratio_size[ar].y;
width -= (int)width % 2;
glm::vec3 res = glm::vec3( width, resolution_height[h] , 0.f);
return res;
@@ -171,6 +191,31 @@ glm::vec3 FrameBuffer::resolution() const
return glm::vec3(attrib_.viewport.x, attrib_.viewport.y, 0.f);
}
void FrameBuffer::resize(int width, int height)
{
if (framebufferid_) {
if (attrib_.viewport.x != width || attrib_.viewport.y != height)
{
// de-init
glDeleteFramebuffers(1, &framebufferid_);
framebufferid_ = 0;
if (intermediate_framebufferid_)
glDeleteFramebuffers(1, &intermediate_framebufferid_);
intermediate_framebufferid_ = 0;
if (textureid_)
glDeleteTextures(1, &textureid_);
textureid_ = 0;
if (intermediate_textureid_)
glDeleteTextures(1, &intermediate_textureid_);
intermediate_textureid_ = 0;
// change resolution
attrib_.viewport = glm::ivec2(width, height);
}
}
}
void FrameBuffer::begin(bool clear)
{
if (!framebufferid_)
@@ -277,7 +322,7 @@ void FrameBuffer::checkFramebufferStatus()
break;
case GL_FRAMEBUFFER_COMPLETE:
#ifndef NDEBUG
Log::Info("Framebuffer created %d x %d.", width(), height());
g_print("Framebuffer created %d x %d\n", width(), height());
#endif
break;
}
@@ -317,7 +362,16 @@ FrameBufferImage::FrameBufferImage(jpegBuffer jpgimg) :
rgb = stbi_load_from_memory(jpgimg.buffer, jpgimg.len, &width, &height, &c, 3);
}
FrameBufferImage::~FrameBufferImage() {
FrameBufferImage::FrameBufferImage(const std::string &filename) :
rgb(nullptr), width(0), height(0)
{
int c = 0;
if (!filename.empty())
rgb = stbi_load(filename.c_str(), &width, &height, &c, 3);
}
FrameBufferImage::~FrameBufferImage()
{
if (rgb!=nullptr)
delete rgb;
}
@@ -352,6 +406,10 @@ FrameBufferImage *FrameBuffer::image(){
if (!framebufferid_)
return img;
// only compatible for RGB FrameBuffers
if (use_alpha_ || use_multi_sampling_)
return img;
// allocate image
img = new FrameBufferImage(attrib_.viewport.x, attrib_.viewport.y);
@@ -367,22 +425,49 @@ bool FrameBuffer::fill(FrameBufferImage *image)
if (!framebufferid_)
init();
// not compatible for RGB
// only compatible for RGB FrameBuffers
if (use_alpha_ || use_multi_sampling_)
return false;
// invalid image
if ( image == nullptr ||
image->rgb==nullptr ||
image->width !=attrib_.viewport.x ||
image->height!=attrib_.viewport.y )
image->width < 1 ||
image->height < 1 )
return false;
// fill texture with image
glBindTexture(GL_TEXTURE_2D, textureid_);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height,
GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
glBindTexture(GL_TEXTURE_2D, 0);
// is it same size ?
if (image->width == attrib_.viewport.x && image->height == attrib_.viewport.y ) {
// directly fill texture with image
glBindTexture(GL_TEXTURE_2D, textureid_);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height,
GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
glBindTexture(GL_TEXTURE_2D, 0);
}
else {
uint textureimage, framebufferimage;
// generate texture
glGenTextures(1, &textureimage);
glBindTexture(GL_TEXTURE_2D, textureimage);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, image->width, image->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
glBindTexture(GL_TEXTURE_2D, 0);
// create a framebuffer object
glGenFramebuffers(1, &framebufferimage);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferimage);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureimage, 0);
// blit to the frame buffer object with interpolation
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebufferimage);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferid_);
glBlitFramebuffer(0, 0, image->width, image->height,
0, 0, attrib_.viewport.x, attrib_.viewport.y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// cleanup
glDeleteFramebuffers(1, &framebufferimage);
glDeleteTextures(1, &textureimage);
}
return true;
}

View File

@@ -24,6 +24,7 @@ public:
FrameBufferImage(int w, int h);
FrameBufferImage(jpegBuffer jpgimg);
FrameBufferImage(const std::string &filename);
// non assignable class
FrameBufferImage(FrameBufferImage const&) = delete;
FrameBufferImage& operator=(FrameBufferImage const&) = delete;
@@ -39,8 +40,8 @@ public:
// size descriptions
static const char* aspect_ratio_name[5];
static glm::vec2 aspect_ratio_size[5];
static const char* resolution_name[4];
static float resolution_height[4];
static const char* resolution_name[5];
static float resolution_height[5];
static glm::vec3 getResolutionFromParameters(int ar, int h);
static glm::ivec2 getParametersFromResolution(glm::vec3 res);
// unbind any framebuffer object
@@ -69,6 +70,7 @@ public:
inline uint width() const { return attrib_.viewport.x; }
inline uint height() const { return attrib_.viewport.y; }
glm::vec3 resolution() const;
void resize(int width, int height);
float aspectRatio() const;
std::string info() const;
@@ -78,6 +80,7 @@ public:
void setProjectionArea(glm::vec2 c);
// internal pixel format
inline uint opengl_id() const { return framebufferid_; }
inline bool use_alpha() const { return use_alpha_; }
inline bool use_multisampling() const { return use_multi_sampling_; }

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
// Desktop OpenGL function loader
@@ -41,12 +60,31 @@ void FrameGrabbing::add(FrameGrabber *rec)
grabbers_.push_back(rec);
}
void FrameGrabbing::chain(FrameGrabber *rec, FrameGrabber *next_rec)
{
if (rec != nullptr && next_rec != nullptr)
{
// add grabber if not yet
if ( std::find(grabbers_.begin(), grabbers_.end(), rec) == grabbers_.end() )
grabbers_.push_back(rec);
grabbers_chain_[next_rec] = rec;
}
}
void FrameGrabbing::verify(FrameGrabber **rec)
{
if ( std::find(grabbers_.begin(), grabbers_.end(), *rec) == grabbers_.end() &&
grabbers_chain_.find(*rec) == grabbers_chain_.end() )
*rec = nullptr;
}
FrameGrabber *FrameGrabbing::front()
{
if (grabbers_.empty())
return nullptr;
else
return grabbers_.front();
return grabbers_.front();
}
struct fgId: public std::unary_function<FrameGrabber*, bool>
@@ -85,13 +123,17 @@ void FrameGrabbing::clearAll()
{
FrameGrabber *rec = *iter;
rec->stop();
iter = grabbers_.erase(iter);
delete rec;
if (rec->finished()) {
iter = grabbers_.erase(iter);
delete rec;
}
else
++iter;
}
}
void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer)
{
if (frame_buffer == nullptr)
return;
@@ -128,7 +170,6 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
"format", G_TYPE_STRING, use_alpha_ ? "RGBA" : "RGB",
"width", G_TYPE_INT, width_,
"height", G_TYPE_INT, height_,
"framerate", GST_TYPE_FRACTION, 30, 1,
NULL);
}
@@ -181,15 +222,16 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
pbo_index_ = (pbo_index_ + 1) % 2;
// a frame was successfully grabbed
if (buffer != nullptr) {
if ( buffer != nullptr && gst_buffer_get_size(buffer) > 0) {
// give the frame to all recorders
std::list<FrameGrabber *>::iterator iter = grabbers_.begin();
while (iter != grabbers_.end())
{
FrameGrabber *rec = *iter;
rec->addFrame(buffer, caps_, dt);
rec->addFrame(buffer, caps_);
// remove finished recorders
if (rec->finished()) {
iter = grabbers_.erase(iter);
delete rec;
@@ -198,6 +240,27 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
++iter;
}
// manage the list of chainned recorder
std::map<FrameGrabber *, FrameGrabber *>::iterator chain = grabbers_chain_.begin();
while (chain != grabbers_chain_.end())
{
// update frame grabber of chain list
chain->first->addFrame(buffer, caps_);
// if the chained recorder is now active
if (chain->first->active_ && chain->first->accept_buffer_){
// add it to main grabbers,
grabbers_.push_back(chain->first);
// stop the replaced grabber
chain->second->stop();
// loop in chain list: done with this chain
chain = grabbers_chain_.erase(chain);
}
else
// loop in chain list
++chain;
}
// unref / free the frame
gst_buffer_unref(buffer);
}
@@ -208,14 +271,14 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
FrameGrabber::FrameGrabber(): finished_(false), active_(false), accept_buffer_(false),
pipeline_(nullptr), src_(nullptr), caps_(nullptr), timestamp_(0)
FrameGrabber::FrameGrabber(): finished_(false), initialized_(false), active_(false), endofstream_(false), accept_buffer_(false), buffering_full_(false),
pipeline_(nullptr), src_(nullptr), caps_(nullptr), timer_(nullptr), timer_firstframe_(0),
timestamp_(0), duration_(0), frame_count_(0), buffering_size_(MIN_BUFFER_SIZE), timestamp_on_clock_(false)
{
// unique id
id_ = BaseToolkit::uniqueId();
// configure fix parameter
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 30); // 30 FPS
timeframe_ = 2 * frame_duration_;
// configure default parameter
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, DEFAULT_GRABBER_FPS); // 25 FPS by default
}
FrameGrabber::~FrameGrabber()
@@ -224,8 +287,13 @@ FrameGrabber::~FrameGrabber()
gst_object_unref (src_);
if (caps_ != nullptr)
gst_caps_unref (caps_);
if (timer_)
gst_object_unref (timer_);
if (pipeline_ != nullptr) {
gst_element_set_state (pipeline_, GST_STATE_NULL);
GstState state = GST_STATE_NULL;
gst_element_set_state (pipeline_, state);
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
gst_object_unref (pipeline_);
}
}
@@ -243,24 +311,28 @@ bool FrameGrabber::busy() const
return false;
}
double FrameGrabber::duration() const
uint64_t FrameGrabber::duration() const
{
return gst_guint64_to_gdouble( GST_TIME_AS_MSECONDS(timestamp_) ) / 1000.0;
return GST_TIME_AS_MSECONDS(duration_);
}
void FrameGrabber::stop ()
{
// send end of stream
gst_app_src_end_of_stream (src_);
// TODO if not initialized wait for initializer
// stop recording
active_ = false;
// send end of stream
gst_app_src_end_of_stream (src_);
}
std::string FrameGrabber::info() const
{
if (!initialized_)
return "Initializing";
if (active_)
return GstToolkit::time_to_string(timestamp_);
return GstToolkit::time_to_string(duration_);
else
return "Inactive";
}
@@ -277,83 +349,163 @@ void FrameGrabber::callback_need_data (GstAppSrc *, guint , gpointer p)
void FrameGrabber::callback_enough_data (GstAppSrc *, gpointer p)
{
FrameGrabber *grabber = static_cast<FrameGrabber *>(p);
if (grabber)
if (grabber) {
grabber->accept_buffer_ = false;
#ifndef NDEBUG
Log::Info("Frame capture : Buffer full");
#endif
}
}
void FrameGrabber::addFrame (GstBuffer *buffer, GstCaps *caps, float dt)
GstPadProbeReturn FrameGrabber::callback_event_probe(GstPad *, GstPadProbeInfo * info, gpointer p)
{
GstEvent *event = GST_PAD_PROBE_INFO_EVENT(info);
if (GST_EVENT_TYPE (event) == GST_EVENT_EOS)
{
FrameGrabber *grabber = static_cast<FrameGrabber *>(p);
if (grabber)
grabber->endofstream_ = true;
}
return GST_PAD_PROBE_OK;
}
std::string FrameGrabber::initialize(FrameGrabber *rec, GstCaps *caps)
{
return rec->init(caps);
}
void FrameGrabber::addFrame (GstBuffer *buffer, GstCaps *caps)
{
// ignore
if (buffer == nullptr)
return;
// first time initialization
if (pipeline_ == nullptr)
init(caps);
// cancel if finished
if (finished_)
return;
// stop if an incompatilble frame buffer given
if ( !gst_caps_is_equal( caps_, caps ))
{
stop();
// Log::Warning("FrameGrabber interrupted: new session (%s)\nincompatible with recording (%s)", gst_caps_to_string(frame.caps), gst_caps_to_string(caps_));
Log::Warning("FrameGrabber interrupted because the resolution changed.");
if (pipeline_ == nullptr) {
initializer_ = std::async( FrameGrabber::initialize, this, caps);
}
// store a frame if recording is active
if (active_)
{
// calculate dt in ns
timeframe_ += gst_gdouble_to_guint64( dt * 1000000.f );
// if time is passed one frame duration (with 10% margin)
// and if the encoder accepts data
if ( timeframe_ > frame_duration_ - 3000000 && accept_buffer_) {
// set timing of buffer
buffer->pts = timestamp_;
buffer->duration = frame_duration_;
// increment ref counter to make sure the frame remains available
gst_buffer_ref(buffer);
// push
gst_app_src_push_buffer (src_, buffer);
// NB: buffer will be unrefed by the appsrc
accept_buffer_ = false;
// next timestamp
timestamp_ += frame_duration_;
// restart frame counter
timeframe_ = 0;
}
}
// did the recording terminate with sink receiving end-of-stream ?
else {
if (!finished_)
// initializer ongoing in separate thread
if (initializer_.valid()) {
// try to get info from initializer
if (initializer_.wait_for( std::chrono::milliseconds(4) ) == std::future_status::ready )
{
// Wait for EOS message
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
GstMessage *msg = gst_bus_poll(bus, GST_MESSAGE_EOS, GST_TIME_AS_USECONDS(1));
// received EOS
if (msg) {
// stop the pipeline
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_NULL);
if (ret == GST_STATE_CHANGE_FAILURE)
Log::Warning("FrameGrabber Could not stop.");
// done initialization
std::string msg = initializer_.get();
// if initialization succeeded
if (initialized_) {
// attach EOS detector
GstPad *pad = gst_element_get_static_pad (gst_bin_get_by_name (GST_BIN (pipeline_), "sink"), "sink");
gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, FrameGrabber::callback_event_probe, this, NULL);
gst_object_unref (pad);
// start recording
active_ = true;
// inform
Log::Info("%s", msg.c_str());
}
// else show warning
else {
// end frame grabber
finished_ = true;
// inform
Log::Warning("%s", msg.c_str());
}
}
}
if (finished_)
terminate();
// stop if an incompatilble frame buffer given after initialization
if (initialized_ && !gst_caps_is_subset( caps_, caps ))
{
stop();
Log::Warning("Frame capture interrupted because the resolution changed.");
}
// store a frame if recording is active and if the encoder accepts data
if (active_)
{
if (accept_buffer_) {
GstClockTime t = 0;
// initialize timer on first occurence
if (timer_ == nullptr) {
timer_ = gst_pipeline_get_clock ( GST_PIPELINE(pipeline_) );
timer_firstframe_ = gst_clock_get_time(timer_);
}
else
// time since timer starts (first frame registered)
t = gst_clock_get_time(timer_) - timer_firstframe_;
// if time is zero (first frame) or if delta time is passed one frame duration (with a margin)
if ( t == 0 || (t - duration_) > (frame_duration_ - 3000) ) {
// count frames
frame_count_++;
// set duration to an exact multiples of frame duration
duration_ = ( t / frame_duration_) * frame_duration_;
if (timestamp_on_clock_)
// automatic frame presentation time stamp
// set time to actual time
// & round t to a multiples of frame duration
timestamp_ = duration_;
else {
// monotonic time increment to keep fixed FPS
timestamp_ += frame_duration_;
// force frame presentation time stamp
buffer->pts = timestamp_;
// set frame duration
buffer->duration = frame_duration_;
}
// when buffering is (almost) full, refuse buffer 1 frame over 2
if (buffering_full_)
accept_buffer_ = frame_count_%2;
else
{
// enter buffering_full_ mode if the space left in buffering is for only few frames
// (this prevents filling the buffer entirely)
if ( buffering_size_ - gst_app_src_get_current_level_bytes(src_) < MIN_BUFFER_SIZE ) {
#ifndef NDEBUG
Log::Info("Frame capture : Using %s of %s Buffer.",
BaseToolkit::byte_to_string(gst_app_src_get_current_level_bytes(src_)).c_str(),
BaseToolkit::byte_to_string(buffering_size_).c_str());
#endif
buffering_full_ = true;
}
}
// increment ref counter to make sure the frame remains available
gst_buffer_ref(buffer);
// push frame
gst_app_src_push_buffer (src_, buffer);
// NB: buffer will be unrefed by the appsrc
}
}
}
// if we received and end of stream (from callback_event_probe)
if (endofstream_)
{
// try to stop properly when interrupted
if (active_) {
// de-activate and re-send EOS
stop();
// inform
Log::Info("Frame capture : Unnexpected EOF signal (no space left on drive? File deleted?)");
Log::Warning("Frame capture : Failed after %s.", GstToolkit::time_to_string(duration_, GstToolkit::TIME_STRING_READABLE).c_str());
}
// terminate properly if finished
else
{
terminate();
finished_ = true;
}
}
}

View File

@@ -2,7 +2,9 @@
#define FRAMEGRABBER_H
#include <atomic>
#include <future>
#include <list>
#include <map>
#include <string>
#include <gst/gst.h>
@@ -13,6 +15,8 @@
// read pixels & pbo should be the fastest
// https://stackoverflow.com/questions/38140527/glreadpixels-vs-glgetteximage
#define USE_GLREADPIXEL
#define DEFAULT_GRABBER_FPS 30
#define MIN_BUFFER_SIZE 33177600 // 33177600 bytes = 1 frames 4K, 9 frames 720p
class FrameBuffer;
@@ -39,36 +43,49 @@ public:
virtual void stop();
virtual std::string info() const;
virtual double duration() const;
virtual uint64_t duration() const;
virtual bool finished() const;
virtual bool busy() const;
protected:
// only FrameGrabbing manager can add frame
virtual void addFrame(GstBuffer *buffer, GstCaps *caps, float dt);
virtual void addFrame(GstBuffer *buffer, GstCaps *caps);
// only addFrame method shall call those
virtual void init(GstCaps *caps) = 0;
virtual std::string init(GstCaps *caps) = 0;
virtual void terminate() = 0;
// thread-safe testing termination
std::atomic<bool> finished_;
std::atomic<bool> initialized_;
std::atomic<bool> active_;
std::atomic<bool> endofstream_;
std::atomic<bool> accept_buffer_;
std::atomic<bool> buffering_full_;
// gstreamer pipeline
GstElement *pipeline_;
GstAppSrc *src_;
GstCaps *caps_;
GstClockTime timeframe_;
GstClock *timer_;
GstClockTime timer_firstframe_;
GstClockTime timestamp_;
GstClockTime duration_;
GstClockTime frame_duration_;
guint64 frame_count_;
guint64 buffering_size_;
bool timestamp_on_clock_;
// async threaded initializer
std::future<std::string> initializer_;
static std::string initialize(FrameGrabber *rec, GstCaps *caps);
// gstreamer callbacks
static void callback_need_data (GstAppSrc *, guint, gpointer user_data);
static void callback_enough_data (GstAppSrc *, gpointer user_data);
static void callback_enough_data (GstAppSrc *, gpointer user_data);
static GstPadProbeReturn callback_event_probe(GstPad *, GstPadProbeInfo *info, gpointer user_data);
};
/**
@@ -100,6 +117,8 @@ public:
inline uint height() const { return height_; }
void add(FrameGrabber *rec);
void chain(FrameGrabber *rec, FrameGrabber *new_rec);
void verify(FrameGrabber **rec);
FrameGrabber *front();
FrameGrabber *get(uint64_t id);
void stopAll();
@@ -108,10 +127,11 @@ public:
protected:
// only for friend Session
void grabFrame(FrameBuffer *frame_buffer, float dt);
void grabFrame(FrameBuffer *frame_buffer);
private:
std::list<FrameGrabber *> grabbers_;
std::map<FrameGrabber *, FrameGrabber *> grabbers_chain_;
guint pbo_[2];
guint pbo_index_;
guint pbo_next_index_;

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - Live video mixer
*
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include "Log.h"

View File

@@ -1,18 +1,34 @@
// Opengl
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <string>
#include <sstream>
#include <iomanip>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtx/vector_angle.hpp>
#include "imgui.h"
#include "ImGuiToolkit.h"
// memmove
#include <string>
#include <sstream>
#include <iomanip>
#include "Mixer.h"
#include "defines.h"
#include "Source.h"
@@ -170,7 +186,7 @@ void GeometryView::resize ( int scale )
scene.root()->scale_.y = z;
// Clamp translation to acceptable area
glm::vec3 border(scene.root()->scale_.x * 1.5, scene.root()->scale_.y * 1.5, 0.f);
glm::vec3 border(2.f * Mixer::manager().session()->frame()->aspectRatio(), 2.f, 0.f);
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border);
}
@@ -224,7 +240,7 @@ void GeometryView::draw()
scene.accept(draw_overlays);
// 4. Draw control overlays of current source on top (if selectable)
if (canSelect(s)) {
if (s!=nullptr && canSelect(s)) {
s->setMode(Source::CURRENT);
DrawVisitor dv(s->overlays_[mode_], projection);
scene.accept(dv);
@@ -411,39 +427,40 @@ std::pair<Node *, glm::vec2> GeometryView::pick(glm::vec2 P)
if (current->workspace() != Settings::application.current_workspace){
current = nullptr;
}
// find if the current source was picked
auto itp = pv.rbegin();
for (; itp != pv.rend(); ++itp){
// test if source contains this node
Source::hasNode is_in_source((*itp).first );
if ( is_in_source( current ) ){
// a node in the current source was clicked !
pick = *itp;
break;
else {
// find if the current source was picked
auto itp = pv.rbegin();
for (; itp != pv.rend(); ++itp){
// test if source contains this node
Source::hasNode is_in_source((*itp).first );
if ( is_in_source( current ) ){
// a node in the current source was clicked !
pick = *itp;
break;
}
}
// not found: the current source was not clicked
if (itp == pv.rend()) {
current = nullptr;
}
// picking on the menu handle: show context menu
else if ( pick.first == current->handles_[mode_][Handles::MENU] ) {
openContextMenu(MENU_SOURCE);
}
// pick on the lock icon; unlock source
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->lock_ ) {
lock(current, false);
pick = { nullptr, glm::vec2(0.f) };
}
// pick on the open lock icon; lock source and cancel pick
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->unlock_ ) {
lock(current, true);
pick = { nullptr, glm::vec2(0.f) };
}
// pick a locked source ; cancel pick
else if ( current->locked() ) {
pick = { nullptr, glm::vec2(0.f) };
}
}
// not found: the current source was not clicked
if (itp == pv.rend()) {
current = nullptr;
}
// picking on the menu handle: show context menu
else if ( pick.first == current->handles_[mode_][Handles::MENU] ) {
openContextMenu(MENU_SOURCE);
}
// pick on the lock icon; unlock source
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->lock_ ) {
lock(current, false);
pick = { nullptr, glm::vec2(0.f) };
}
// pick on the open lock icon; lock source and cancel pick
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->unlock_ ) {
lock(current, true);
pick = { nullptr, glm::vec2(0.f) };
}
// pick a locked source ; cancel pick
else if ( current->locked() ) {
pick = { nullptr, glm::vec2(0.f) };
}
}
// the clicked source changed (not the current source)
@@ -516,7 +533,7 @@ std::pair<Node *, glm::vec2> GeometryView::pick(glm::vec2 P)
bool GeometryView::canSelect(Source *s) {
return ( View::canSelect(s) && s->ready() && s->active() && s->workspace() == Settings::application.current_workspace);
return ( s!=nullptr && View::canSelect(s) && s->ready() && s->active() && s->workspace() == Settings::application.current_workspace);
}
@@ -927,10 +944,10 @@ View::Cursor GeometryView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::p
sourceNode->rotation_.z = glm::radians( float(degrees) );
overlay_rotation_clock_->visible_ = true;
overlay_rotation_clock_->copyTransform(overlay_rotation_);
info << "Angle " << degrees << "\u00b0"; // degree symbol
info << "Angle " << degrees << UNICODE_DEGREE;
}
else
info << "Angle " << std::fixed << std::setprecision(1) << glm::degrees(sourceNode->rotation_.z) << "\u00b0"; // degree symbol
info << "Angle " << std::fixed << std::setprecision(1) << glm::degrees(sourceNode->rotation_.z) << UNICODE_DEGREE;
overlay_rotation_clock_hand_->visible_ = true;
overlay_rotation_clock_hand_->translation_.x = s->stored_status_->translation_.x;
@@ -1033,8 +1050,8 @@ void GeometryView::terminate()
void GeometryView::arrow (glm::vec2 movement)
{
static int accumulator = 0;
accumulator++;
static float accumulator = 0.f;
accumulator += dt_;
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
glm::vec3 gl_Position_to = Rendering::manager().unProject(movement, scene.root()->transform_);
@@ -1057,18 +1074,19 @@ void GeometryView::arrow (glm::vec2 movement)
// + ALT : discrete displacement
if (UserInterface::manager().altModifier()) {
if (accumulator > 10) {
if (accumulator > 100.f) {
dest_translation += glm::sign(gl_delta) * 0.11f;
dest_translation.x = ROUND(dest_translation.x, 10.f);
dest_translation.y = ROUND(dest_translation.y, 10.f);
accumulator = 0;
accumulator = 0.f;
}
else
break;
}
else {
// normal case: dest += delta
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
accumulator = 0.f;
}
// store action in history

View File

@@ -1,13 +1,32 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "GlmToolkit.h"
#include <chrono>
#include <ctime>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/vector_angle.hpp>
#include <glm/gtc/random.hpp>
#include <chrono>
#include <ctime>
#include "GlmToolkit.h"
glm::mat4 GlmToolkit::transform(glm::vec3 translation, glm::vec3 rotation, glm::vec3 scale)
{
@@ -57,6 +76,16 @@ GlmToolkit::AxisAlignedBoundingBox::AxisAlignedBoundingBox() :
{
}
GlmToolkit::AxisAlignedBoundingBox::AxisAlignedBoundingBox(const GlmToolkit::AxisAlignedBoundingBox &D) :
mMin(D.mMin), mMax(D.mMax)
{
}
void GlmToolkit::AxisAlignedBoundingBox::operator = (const GlmToolkit::AxisAlignedBoundingBox &D ) {
mMin = D.mMin;
mMax = D.mMax;
}
void GlmToolkit::AxisAlignedBoundingBox::extend(const glm::vec3& point)
{
if (isNull()) {

View File

@@ -15,11 +15,8 @@ class AxisAlignedBoundingBox
{
public:
AxisAlignedBoundingBox();
inline void operator = (const AxisAlignedBoundingBox &D ) {
mMin = D.mMin;
mMax = D.mMax;
}
AxisAlignedBoundingBox(const AxisAlignedBoundingBox &D);
void operator = (const AxisAlignedBoundingBox &D );
// test
inline bool isNull() const { return mMin.x > mMax.x || mMin.y > mMax.y || mMin.z > mMax.z;}

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <sstream>
#include <iomanip>
using namespace std;
@@ -14,6 +33,8 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
return "00:00:00.00";
case TIME_STRING_MINIMAL:
return "0.0";
case TIME_STRING_READABLE:
return "0 second";
default:
return "00.00";
}
@@ -23,23 +44,52 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
guint s = ms / 1000;
ostringstream oss;
// MINIMAL: keep only the 2 higher values (most significant)
if (m == TIME_STRING_MINIMAL) {
// READABLE : long format
if (m == TIME_STRING_READABLE) {
int count = 0;
if (s / 3600) {
oss << s / 3600 << ':';
oss << s / 3600 << " h ";
count++;
}
if ((s % 3600) / 60) {
oss << (s % 3600) / 60 << ':';
oss << (s % 3600) / 60 << " min ";
count++;
}
if (count < 2) {
oss << setw(count > 0 ? 2 : 1) << setfill('0') << (s % 3600) % 60;
count++;
if (count < 2 )
oss << '.'<< setw(1) << setfill('0') << (ms % 1000) / 100 << " sec";
else
oss << " s";
}
if (count < 2 )
oss << '.'<< setw(1) << setfill('0') << (ms % 1000) / 10;
}
// MINIMAL: keep only the 2 higher values (most significant)
else if (m == TIME_STRING_MINIMAL) {
int count = 0;
// hours
if (s / 3600) {
oss << s / 3600 << ':';
count++;
}
// minutes
if (count > 0) {
oss << setw(2) << setfill('0') << (s % 3600) / 60 << ':';
count++;
}
else if ((s % 3600) / 60)
{
oss << (s % 3600) / 60 << ':';
count++;
}
// seconds
{
oss << setw(count > 0 ? 2 : 1) << setfill('0') << (s % 3600) % 60;
count++;
}
if (count < 2)
oss << '.'<< setw((ms % 1000) / 100 ? 2 : 1) << setfill('0') << (ms % 1000) / 10;
}
else {
// TIME_STRING_FIXED : fixed length string (11 chars) HH:mm:ss.ii"
@@ -58,11 +108,13 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
std::string GstToolkit::filename_to_uri(std::string path)
{
if (path.empty())
return path;
// set uri to open
gchar *uritmp = gst_filename_to_uri(path.c_str(), NULL);
std::string uri( uritmp );
g_free(uritmp);
return uri;
}
@@ -119,6 +171,31 @@ bool GstToolkit::enable_feature (string name, bool enable) {
}
gst_registry_add_feature (registry, GST_PLUGIN_FEATURE (factory));
gst_object_unref (factory);
return true;
}
bool GstToolkit::has_feature (string name)
{
if (name.empty())
return false;
GstRegistry *registry = NULL;
GstElementFactory *factory = NULL;
registry = gst_registry_get();
if (!registry) return false;
factory = gst_element_factory_find (name.c_str());
if (!factory) return false;
GstElement *elem = gst_element_factory_create (factory, NULL);
gst_object_unref (factory);
if (!elem) return false;
gst_object_unref (elem);
return true;
}
@@ -136,11 +213,11 @@ string GstToolkit::gst_version()
#if GST_GL_HAVE_PLATFORM_GLX
// https://gstreamer.freedesktop.org/documentation/nvcodec/index.html?gi-language=c#plugin-nvcodec
const char *plugins[10] = { "omxmpeg4videodec", "omxmpeg2dec", "omxh264dec", "vdpaumpegdec",
"nvh264dec", "nvh265dec", "nvmpeg2videodec",
"nvmpeg4videodec", "nvvp8dec", "nvvp9dec"
};
const int N = 10;
// list ordered with higher priority first (e.g. nvidia proprietary before vaapi)
const char *plugins[11] = { "nvh264dec", "nvh265dec", "nvmpeg2videodec", "nvmpeg4videodec", "nvvp8dec", "nvvp9dec",
"vaapidecodebin", "omxmpeg4videodec", "omxmpeg2dec", "omxh264dec", "vdpaumpegdec",
};
const int N = 11;
#elif GST_GL_HAVE_PLATFORM_CGL
const char *plugins[2] = { "vtdec_hw", "vtdechw" };
const int N = 2;
@@ -166,7 +243,7 @@ std::list<std::string> GstToolkit::enable_gpu_decoding_plugins(bool enable)
GstPluginFeature* feature = gst_registry_lookup_feature(plugins_register, plugins[i]);
if(feature != NULL) {
plugins_list_.push_front( string( plugins[i] ) );
gst_plugin_feature_set_rank(feature, enable ? GST_RANK_PRIMARY + 1 : GST_RANK_MARGINAL);
gst_plugin_feature_set_rank(feature, enable ? GST_RANK_PRIMARY + (N-i) : GST_RANK_MARGINAL);
gst_object_unref(feature);
}
}
@@ -188,12 +265,9 @@ std::string GstToolkit::used_gpu_decoding_plugins(GstElement *gstbin)
{
GstElement *e = static_cast<GstElement*>(g_value_peek_pointer(&value));
if (e) {
gchar *name = gst_element_get_name(e);
// g_print(" - %s", name);
std::string e_name(name);
g_free(name);
const gchar *name = gst_element_get_name(e);
for (int i = 0; i < N; i++) {
if (e_name.find(plugins[i]) != std::string::npos) {
if (std::string(name).find(plugins[i]) != std::string::npos) {
found = plugins[i];
break;
}
@@ -207,3 +281,28 @@ std::string GstToolkit::used_gpu_decoding_plugins(GstElement *gstbin)
return found;
}
std::string GstToolkit::used_decoding_plugins(GstElement *gstbin)
{
std::string found = "";
GstIterator* it = gst_bin_iterate_recurse(GST_BIN(gstbin));
GValue value = G_VALUE_INIT;
for(GstIteratorResult r = gst_iterator_next(it, &value); r != GST_ITERATOR_DONE; r = gst_iterator_next(it, &value))
{
if ( r == GST_ITERATOR_OK )
{
GstElement *e = static_cast<GstElement*>(g_value_peek_pointer(&value));
if (e) {
const gchar *name = gst_element_get_name(e);
found += std::string(name) + ", ";
}
}
g_value_unset(&value);
}
gst_iterator_free(it);
return found;
}

View File

@@ -12,7 +12,8 @@ namespace GstToolkit
typedef enum {
TIME_STRING_FIXED = 0,
TIME_STRING_ADJUSTED,
TIME_STRING_MINIMAL
TIME_STRING_MINIMAL,
TIME_STRING_READABLE
} time_string_mode;
std::string time_to_string(guint64 t, time_string_mode m = TIME_STRING_ADJUSTED);
@@ -23,8 +24,10 @@ std::string gst_version();
std::list<std::string> all_plugins();
std::list<std::string> enable_gpu_decoding_plugins(bool enable = true);
std::string used_gpu_decoding_plugins(GstElement *gstbin);
std::string used_decoding_plugins(GstElement *gstbin);
std::list<std::string> all_plugin_features(std::string pluginname);
bool has_feature (std::string name);
bool enable_feature (std::string name, bool enable);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,14 @@
#include <glib.h>
#include <string>
#include <list>
#include <vector>
#include <utility>
#include "imgui.h"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui_internal.h"
#include "rsc/fonts/IconsFontAwesome5.h"
namespace ImGuiToolkit
@@ -13,30 +18,41 @@ namespace ImGuiToolkit
// Icons from resource icon.dds
void Icon (int i, int j, bool enabled = true);
bool IconButton (int i, int j, const char *tooltips = nullptr);
bool IconButton (const char* icon = ICON_FA_EXCLAMATION_CIRCLE, const char *tooltips = nullptr);
bool IconButton (const char* icon, const char *tooltips = nullptr);
bool IconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[] = nullptr);
void ShowIconsWindow(bool* p_open);
// icon buttons
// buttons and gui items with icon
bool ButtonIcon (int i, int j, const char* tooltip = nullptr);
bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle);
bool ButtonIconMultistate (std::vector<std::pair<int, int> > icons, int* state);
bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltip = nullptr);
bool ButtonIconMultistate (std::vector<std::pair<int, int> > icons, int* state, const char* tooltip = nullptr);
bool MenuItemIcon (int i, int j, const char* label, bool selected = false, bool enabled = true);
bool SelectableIcon(const char* label, int i, int j, bool selected = false);
bool ComboIcon (std::vector<std::pair<int, int> > icons, std::vector<std::string> labels, int* state);
bool ComboIcon (const char* label, std::vector<std::pair<int, int> > icons, std::vector<std::string> items, int* i);
// utility buttons
bool ButtonToggle (const char* label, bool* toggle);
bool ButtonSwitch (const char* label, bool* toggle , const char *help = nullptr);
// buttons
bool ButtonToggle (const char* label, bool* toggle, const char *tooltip = nullptr);
bool ButtonSwitch (const char* label, bool* toggle, const char *shortcut = nullptr);
void ButtonOpenUrl (const char* label, const char* url, const ImVec2& size_arg = ImVec2(0,0));
// tooltip and mouse over help
void setToolTipsEnabled (bool on);
bool toolTipsEnabled ();
void ToolTip (const char* desc, const char* shortcut = nullptr);
void HelpMarker (const char* desc, const char* icon = ICON_FA_QUESTION_CIRCLE, const char* shortcut = nullptr);
void HelpIcon (const char* desc, int i = 19, int j = 5, const char* shortcut = nullptr);
void HelpToolTip(const char* desc, const char* shortcut = nullptr);
void Indication (const char* desc, const char* icon, const char* shortcut = nullptr);
void Indication (const char* desc, int i, int j, const char* shortcut = nullptr);
// utility sliders
bool TimelineSlider (const char* label, guint64 *time, guint64 start, guint64 end, guint64 step, const float width);
// sliders
bool SliderTiming (const char* label, uint *ms, uint v_min, uint v_max, uint v_step, const char* text_max = nullptr);
bool TimelineSlider (const char* label, guint64 *time, guint64 begin, guint64 first, guint64 end, guint64 step, const float width, double tempo = 0, double quantum = 0);
void RenderTimeline (struct ImGuiWindow* window, struct ImRect timeline_bbox, guint64 begin, guint64 end, guint64 step, bool verticalflip = false);
void RenderTimelineBPM (struct ImGuiWindow* window, struct ImRect timeline_bbox, double tempo, double quantum, guint64 begin, guint64 end, guint64 step, bool verticalflip = false);
bool InvisibleSliderInt(const char* label, uint *index, uint min, uint max, const ImVec2 size);
bool EditPlotLines(const char* label, float *array, int values_count, float values_min, float values_max, const ImVec2 size);
bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, bool *released, const ImVec2 size);
bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, guint64 begin, guint64 end, bool cut, bool *released, const ImVec2 size);
void ShowPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, const ImVec2 size);
// fonts from ressources 'fonts/'
typedef enum {
@@ -48,13 +64,18 @@ namespace ImGuiToolkit
} font_style;
void SetFont (font_style type, const std::string &ttf_font_name, int pointsize, int oversample = 2);
void PushFont (font_style type);
void ImageGlyph(font_style type, char c, float h = 60);
void Spacing();
void WindowText(const char* window_name, ImVec2 window_pos, const char* text);
bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text);
void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format);
// text input
bool InputText(const char* label, std::string* str);
bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0));
void TextMultiline(const char* label, const std::string &str, float width);
bool InputCodeMultiline(const char* label, std::string *str, const ImVec2& size = ImVec2(0, 0), int *numline = NULL);
void CodeMultiline(const char* label, const std::string &str, float width);
// color of gui items
// accent color of UI
typedef enum {
ACCENT_BLUE =0,
ACCENT_ORANGE,
@@ -63,8 +84,11 @@ namespace ImGuiToolkit
void SetAccentColor (accent_color color);
struct ImVec4 HighlightColor (bool active = true);
bool InputText(const char* label, std::string* str);
bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int linesize = 0);
// varia
void WindowText(const char* window_name, ImVec2 window_pos, const char* text);
bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text);
void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format);
}
#endif // __IMGUI_TOOLKIT_H_

View File

@@ -1,4 +1,22 @@
#include "ImGuiVisitor.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <vector>
#include <algorithm>
@@ -38,13 +56,14 @@
#include "UserInterfaceManager.h"
#include "SystemToolkit.h"
#include "ImGuiVisitor.h"
ImGuiVisitor::ImGuiVisitor()
{
}
void ImGuiVisitor::visit(Node &n)
void ImGuiVisitor::visit(Node &)
{
}
@@ -62,7 +81,7 @@ void ImGuiVisitor::visit(Group &n)
n.scale_.y = 1.f;
Action::manager().store("Geometry Reset");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Geometry");
if (ImGuiToolkit::ButtonIcon(6, 15)) {
@@ -70,7 +89,7 @@ void ImGuiVisitor::visit(Group &n)
n.translation_.y = 0.f;
Action::manager().store("Position 0.0, 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
float translation[2] = { n.translation_.x, n.translation_.y};
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if ( ImGui::SliderFloat2("Position", translation, -5.0, 5.0) )
@@ -88,7 +107,7 @@ void ImGuiVisitor::visit(Group &n)
n.scale_.y = 1.f;
Action::manager().store("Scale 1.0 x 1.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
float scale[2] = { n.scale_.x, n.scale_.y} ;
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if ( ImGui::SliderFloat2("Scale", scale, -MAX_SCALE, MAX_SCALE, "%.2f") )
@@ -106,7 +125,7 @@ void ImGuiVisitor::visit(Group &n)
n.rotation_.z = 0.f;
Action::manager().store("Angle 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderAngle("Angle", &(n.rotation_.z), -180.f, 180.f) ;
if (ImGui::IsItemDeactivatedAfterEdit()) {
@@ -147,20 +166,12 @@ void ImGuiVisitor::visit(Primitive &n)
ImGui::PopID();
}
void ImGuiVisitor::visit(FrameBufferSurface &n)
void ImGuiVisitor::visit(FrameBufferSurface &)
{
ImGui::Text("Framebuffer");
}
void ImGuiVisitor::visit(MediaSurface &n)
{
ImGui::Text("%s", n.path().c_str());
if (n.mediaPlayer())
n.mediaPlayer()->accept(*this);
}
void ImGuiVisitor::visit(MediaPlayer &n)
void ImGuiVisitor::visit(MediaPlayer &)
{
ImGui::Text("Media Player");
}
@@ -174,7 +185,7 @@ void ImGuiVisitor::visit(Shader &n)
// n.blending = Shader::BLEND_OPACITY;
// n.color = glm::vec4(1.f, 1.f, 1.f, 1.f);
// }
// ImGui::SameLine(0, 10);
// ImGui::SameLine(0, IMGUI_SAME_LINE);
// ImGui::ColorEdit3("Color", glm::value_ptr(n.color), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel ) ;
// ImGui::SameLine(0, 5);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
@@ -226,7 +237,7 @@ void ImGuiVisitor::visit(Shader &n)
// // get index of the mask used in this ImageShader
// int item_current = n.mask;
//// if (ImGuiToolkit::ButtonIcon(10, 3)) n.mask = 0;
//// ImGui::SameLine(0, 10);
//// ImGui::SameLine(0, IMGUI_SAME_LINE);
// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
// // combo list of masks
// if ( ImGui::Combo("Mask", &item_current, ImageShader::mask_names, IM_ARRAYSIZE(ImageShader::mask_names) ) )
@@ -246,14 +257,14 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
ImGui::PushID(std::to_string(n.id()).c_str());
ImGuiToolkit::Icon(6, 2);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Filters");
if (ImGuiToolkit::ButtonIcon(6, 4)) {
if (ImGuiToolkit::IconButton(6, 4)) {
n.gamma = glm::vec4(1.f, 1.f, 1.f, 1.f);
Action::manager().store("Gamma & Color");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::ColorEdit3("Gamma Color", glm::value_ptr(n.gamma), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel) ;
if (ImGui::IsItemDeactivatedAfterEdit())
Action::manager().store("Gamma Color changed");
@@ -270,12 +281,12 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
// ImGui::SliderFloat4("Levels", glm::value_ptr(n.levels), 0.0, 1.0);
if (ImGuiToolkit::ButtonIcon(5, 16)) {
if (ImGuiToolkit::IconButton(5, 16)) {
n.brightness = 0.f;
n.contrast = 0.f;
Action::manager().store("B & C 0.0 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
float bc[2] = { n.brightness, n.contrast};
if ( ImGui::SliderFloat2("B & C", bc, -1.0, 1.0) )
@@ -289,11 +300,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(9, 16)) {
if (ImGuiToolkit::IconButton(9, 16)) {
n.saturation = 0.f;
Action::manager().store("Saturation 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderFloat("Saturation", &n.saturation, -1.0, 1.0);
if (ImGui::IsItemDeactivatedAfterEdit()){
@@ -302,11 +313,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(12, 4)) {
if (ImGuiToolkit::IconButton(12, 4)) {
n.hueshift = 0.f;
Action::manager().store("Hue shift 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderFloat("Hue shift", &n.hueshift, 0.0, 1.0);
if (ImGui::IsItemDeactivatedAfterEdit()){
@@ -315,11 +326,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(18, 1)) {
if (ImGuiToolkit::IconButton(18, 1)) {
n.nbColors = 0;
Action::manager().store("Posterize None");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderInt("Posterize", &n.nbColors, 0, 16, n.nbColors == 0 ? "None" : "%d colors");
if (ImGui::IsItemDeactivatedAfterEdit()){
@@ -329,11 +340,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(8, 1)) {
if (ImGuiToolkit::IconButton(8, 1)) {
n.threshold = 0.f;
Action::manager().store("Threshold None");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderFloat("Threshold", &n.threshold, 0.0, 1.0, n.threshold < 0.001 ? "None" : "%.2f");
if (ImGui::IsItemDeactivatedAfterEdit()){
@@ -343,11 +354,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(3, 1)) {
if (ImGuiToolkit::IconButton(3, 1)) {
n.lumakey = 0.f;
Action::manager().store("Lumakey 0.0");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
ImGui::SliderFloat("Lumakey", &n.lumakey, 0.0, 1.0);
if (ImGui::IsItemDeactivatedAfterEdit()){
@@ -356,12 +367,12 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(13, 4)) {
if (ImGuiToolkit::IconButton(13, 4)) {
n.chromakey = glm::vec4(0.f, 0.8f, 0.f, 1.f);
n.chromadelta = 0.f;
Action::manager().store("Chromakey & Color Reset");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::ColorEdit3("Chroma color", glm::value_ptr(n.chromakey), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel ) ;
if (ImGui::IsItemDeactivatedAfterEdit())
Action::manager().store("Chroma color changed");
@@ -375,20 +386,20 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
Action::manager().store(oss.str());
}
if (ImGuiToolkit::ButtonIcon(6, 16)) {
if (ImGuiToolkit::IconButton(6, 16)) {
n.invert = 0;
Action::manager().store("Invert None");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if (ImGui::Combo("Invert", &n.invert, "None\0Invert Color\0Invert Luminance\0"))
Action::manager().store("Invert " + std::string(n.invert<1 ? "None": (n.invert>1 ? "Luminance" : "Color")));
if (ImGuiToolkit::ButtonIcon(1, 7)) {
if (ImGuiToolkit::IconButton(1, 7)) {
n.filterid = 0;
Action::manager().store("Filter None");
}
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if (ImGui::Combo("Filter", &n.filterid, ImageProcessingShader::filter_names, IM_ARRAYSIZE(ImageProcessingShader::filter_names) ) )
Action::manager().store("Filter " + std::string(ImageProcessingShader::filter_names[n.filterid]));
@@ -424,21 +435,21 @@ void ImGuiVisitor::visit (Source& s)
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y ) );
if (s.active()) {
if (s.blendingShader()->color.a > 0.f)
ImGuiToolkit::HelpMarker("Visible", ICON_FA_EYE);
ImGuiToolkit::Indication("Visible", ICON_FA_EYE);
else
ImGuiToolkit::HelpMarker("Not visible", ICON_FA_EYE_SLASH);
ImGuiToolkit::Indication("Not visible", ICON_FA_EYE_SLASH);
}
else
ImGuiToolkit::HelpMarker("Inactive", ICON_FA_SNOWFLAKE);
ImGuiToolkit::Indication("Inactive", ICON_FA_SNOWFLAKE);
// Inform on workspace
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y + ImGui::GetFrameHeightWithSpacing()) );
if (s.workspace() == Source::BACKGROUND)
ImGuiToolkit::HelpIcon("in Background",10, 16);
ImGuiToolkit::Indication("in Background",10, 16);
else if (s.workspace() == Source::FOREGROUND)
ImGuiToolkit::HelpIcon("in Foreground",12, 16);
ImGuiToolkit::Indication("in Foreground",12, 16);
else
ImGuiToolkit::HelpIcon("in Workspace",11, 16);
ImGuiToolkit::Indication("in Workspace",11, 16);
// locking
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y + 2.f * ImGui::GetFrameHeightWithSpacing()) );
@@ -459,7 +470,7 @@ void ImGuiVisitor::visit (Source& s)
// toggle enable/disable image processing
bool on = s.imageProcessingEnabled();
ImGui::SetCursorPos( ImVec2(preview_width + 15, pos.y + 3.5f * ImGui::GetFrameHeightWithSpacing()) );
if ( ImGuiToolkit::ButtonToggle(ICON_FA_MAGIC, &on) ){
if ( ImGuiToolkit::ButtonIconToggle(6, 2, 6, 2, &on, "Filters") ){
std::ostringstream oss;
oss << s.name() << ": " << ( on ? "Enable Filter" : "Disable Filter");
Action::manager().store(oss.str());
@@ -530,7 +541,7 @@ void ImGuiVisitor::visit (Source& s)
if (s.processingshader_link_.connected()) {
ImGuiToolkit::Icon(6, 2);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Filters");
Source *target = s.processingshader_link_.source();
ImGui::Text("Following");
@@ -547,17 +558,31 @@ void ImGuiVisitor::visit (Source& s)
void ImGuiVisitor::visit (MediaSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
if ( s.mediaplayer()->isImage() )
ImGui::Text("Image File");
else
ImGui::Text("Video File");
if ( ImGui::Button(IMGUI_TITLE_MEDIAPLAYER, ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
UserInterface::manager().showMediaPlayer( s.mediaplayer());
// Media info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
// folder
std::string path = SystemToolkit::path_filename(s.path());
std::string label = BaseToolkit::trunc_string(path, 25);
std::string label = BaseToolkit::truncated(path, 25);
label = BaseToolkit::transliterate(label);
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
@@ -571,36 +596,53 @@ void ImGuiVisitor::visit (SessionFileSource& s)
return;
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Session File");
// ImGui::Text("%s", SystemToolkit::base_filename(s.path()).c_str());
if (ImGuiToolkit::ButtonIcon(3, 2)) s.session()->setFading(0.f);
// info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
if ( ImGui::Button( ICON_FA_FILE_EXPORT " Import", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
Mixer::manager().import( &s );
ImGui::SameLine();
ImGui::Text("Sources");
if (ImGuiToolkit::ButtonIcon(3, 2)) s.session()->setFadingTarget(0.f);
float f = s.session()->fading();
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if (ImGui::SliderFloat("Fading", &f, 0.0, 1.0, f < 0.001 ? "None" : "%.2f") )
s.session()->setFading(f);
s.session()->setFadingTarget(f);
if (ImGui::IsItemDeactivatedAfterEdit()){
std::ostringstream oss;
oss << s.name() << ": Fading " << std::setprecision(2) << f;
Action::manager().store(oss.str());
}
if ( ImGui::Button( ICON_FA_FILE_UPLOAD " Open Session", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
if ( ImGui::Button( ICON_FA_FILE_UPLOAD " Open", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
Mixer::manager().set( s.detach() );
ImGui::SameLine();
ImGui::Text("File");
std::string path = SystemToolkit::path_filename(s.path());
std::string label = BaseToolkit::trunc_string(path, 25);
std::string label = BaseToolkit::truncated(path, 25);
label = BaseToolkit::transliterate(label);
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
ImGui::SameLine();
ImGui::Text("Folder");
ImGui::Text("Contains %d sources.", s.session()->numSource());
if ( ImGui::Button( ICON_FA_FILE_EXPORT " Import", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
Mixer::manager().import( &s );
}
void ImGuiVisitor::visit (SessionGroupSource& s)
@@ -609,20 +651,34 @@ void ImGuiVisitor::visit (SessionGroupSource& s)
return;
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Flat Sesion group");
ImGui::Text("Contains %d sources.", s.session()->numSource());
// info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
if ( ImGui::Button( ICON_FA_UPLOAD " Expand", ImVec2(IMGUI_RIGHT_ALIGN, 0)) ){
Mixer::manager().import( &s );
}
}
void ImGuiVisitor::visit (RenderSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Rendering Output");
if ( ImGui::Button(IMGUI_TITLE_PREVIEW, ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
Settings::application.widget.preview = true;
@@ -631,7 +687,7 @@ void ImGuiVisitor::visit (RenderSource& s)
void ImGuiVisitor::visit (CloneSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Clone");
if ( ImGui::Button(s.origin()->name().c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
Mixer::manager().setCurrentSource(s.origin());
@@ -642,17 +698,34 @@ void ImGuiVisitor::visit (CloneSource& s)
void ImGuiVisitor::visit (PatternSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Pattern");
// stream info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if (ImGui::BeginCombo("##Patterns", Pattern::pattern_types[s.pattern()->type()].c_str()) )
if (ImGui::BeginCombo("##Patterns", Pattern::get(s.pattern()->type()).label.c_str()) )
{
for (uint p = 0; p < Pattern::pattern_types.size(); ++p){
if (ImGui::Selectable( Pattern::pattern_types[p].c_str() )) {
for (uint p = 0; p < Pattern::count(); ++p){
if (ImGui::Selectable( Pattern::get(p).label.c_str() )) {
s.setPattern(p, s.pattern()->resolution());
info.reset();
std::ostringstream oss;
oss << s.name() << ": Pattern " << Pattern::pattern_types[p];
oss << s.name() << ": Pattern " << Pattern::get(p).label;
Action::manager().store(oss.str());
}
}
@@ -665,9 +738,24 @@ void ImGuiVisitor::visit (PatternSource& s)
void ImGuiVisitor::visit (DeviceSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Device");
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
if (ImGui::BeginCombo("##Hardware", s.device().c_str()))
{
@@ -675,6 +763,7 @@ void ImGuiVisitor::visit (DeviceSource& s)
std::string namedev = Device::manager().name(d);
if (ImGui::Selectable( namedev.c_str() )) {
s.setDevice(namedev);
info.reset();
std::ostringstream oss;
oss << s.name() << " Device " << namedev;
Action::manager().store(oss.str());
@@ -682,48 +771,65 @@ void ImGuiVisitor::visit (DeviceSource& s)
}
ImGui::EndCombo();
}
DeviceConfigSet confs = Device::manager().config( Device::manager().index(s.device().c_str()));
if ( !confs.empty()) {
DeviceConfig best = *confs.rbegin();
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
ImGui::Text("%s %s %dx%d@%.1ffps", best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
}
}
void ImGuiVisitor::visit (NetworkSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Network stream");
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_STREAM, 0.9f));
ImGui::Text("%s", s.connection().c_str());
ImGui::PopStyleColor(1);
NetworkStream *ns = s.networkStream();
ImGui::Text(" - %s (%dx%d)\n - Server address %s", NetworkToolkit::protocol_name[ns->protocol()],
ns->resolution().x, ns->resolution().y, ns->serverAddress().c_str());
// network info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
if ( ImGui::Button( ICON_FA_REPLY " Reconnect", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
{
// TODO : reload ?
s.setConnection(s.connection());
info.reset();
}
}
void ImGuiVisitor::visit (MultiFileSource& s)
{
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Images sequence");
static int64_t id = s.id();
static uint64_t id = 0;
// information text
std::ostringstream msg;
msg << "Sequence of " << s.sequence().max - s.sequence().min + 1 << " ";
msg << s.sequence().codec << " images";
ImGui::Text("%s", msg.str().c_str());
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
// change range
static int _begin = -1;
@@ -758,11 +864,50 @@ void ImGuiVisitor::visit (MultiFileSource& s)
// offer to open file browser at location
std::string path = SystemToolkit::path_filename(s.sequence().location);
std::string label = BaseToolkit::trunc_string(path, 25);
std::string label = BaseToolkit::truncated(path, 25);
label = BaseToolkit::transliterate(label);
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
ImGui::SameLine();
ImGui::Text("Folder");
id = s.id();
if (id != s.id())
id = s.id();
}
void ImGuiVisitor::visit (GenericStreamSource& s)
{
float w = ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN;
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::Text("Custom");
// stream info
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + w);
s.accept(info);
ImGui::Text("%s", info.str().c_str());
ImGui::PopTextWrapPos();
// icon (>) to open player
if ( s.playable() ) {
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SameLine(0, 0);
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
UserInterface::manager().showSourceEditor(&s);
ImGui::SetCursorPos(pos);
}
// Prepare display pipeline text
static int numlines = 0;
const ImGuiContext& g = *GImGui;
ImVec2 fieldsize(w, MAX(3, numlines) * g.FontSize + g.Style.ItemSpacing.y + g.Style.FramePadding.y);
// Editor
std::string _description = s.description();
if ( ImGuiToolkit::InputCodeMultiline("Pipeline", &_description, fieldsize, &numlines) ) {
s.setDescription(_description);
Action::manager().store( s.name() + ": Change pipeline");
}
}

View File

@@ -2,9 +2,12 @@
#define IMGUIVISITOR_H
#include "Visitor.h"
#include "InfoVisitor.h"
class ImGuiVisitor: public Visitor
{
InfoVisitor info;
public:
ImGuiVisitor();
@@ -14,7 +17,6 @@ public:
void visit (Group& n) override;
void visit (Switch& n) override;
void visit (Primitive& n) override;
void visit (MediaSurface& n) override;
void visit (FrameBufferSurface& n) override;
// Elements with attributes
@@ -31,6 +33,7 @@ public:
void visit (DeviceSource& s) override;
void visit (NetworkSource& s) override;
void visit (MultiFileSource& s) override;
void visit (GenericStreamSource& s) override;
};
#endif // IMGUIVISITOR_H

View File

@@ -1,6 +1,26 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "defines.h"
#include "Visitor.h"
#include "Log.h"
#include "ImageProcessingShader.h"
ShadingProgram imageProcessingShadingProgram("shaders/image.vs", "shaders/imageprocessing.fs");
@@ -12,7 +32,7 @@ const char* ImageProcessingShader::filter_names[12] = { "None", "Blur", "Sharpen
ImageProcessingShader::ImageProcessingShader(): Shader()
{
program_ = &imageProcessingShadingProgram;
reset();
ImageProcessingShader::reset();
}
void ImageProcessingShader::use()

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glad/glad.h>
#include "defines.h"
@@ -27,7 +46,7 @@ ImageShader::ImageShader(): Shader(), stipple(0.f), mask_texture(0)
// static program shader
program_ = &imageShadingProgram;
// reset instance
reset();
ImageShader::reset();
}
void ImageShader::use()
@@ -85,7 +104,7 @@ AlphaShader::AlphaShader(): ImageShader()
MaskShader::MaskShader(): Shader(), mode(0)
{
// reset instance
reset();
MaskShader::reset();
// static program shader
program_ = &maskPrograms[0];
}

298
InfoVisitor.cpp Normal file
View File

@@ -0,0 +1,298 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <vector>
#include <algorithm>
#include <sstream>
#include <iomanip>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <tinyxml2.h>
#include "tinyxml2Toolkit.h"
#include "defines.h"
#include "Log.h"
#include "Scene.h"
#include "Primitives.h"
#include "ImageShader.h"
#include "ImageProcessingShader.h"
#include "MediaPlayer.h"
#include "MediaSource.h"
#include "SessionSource.h"
#include "PatternSource.h"
#include "DeviceSource.h"
#include "NetworkSource.h"
#include "MultiFileSource.h"
#include "SessionCreator.h"
#include "SessionVisitor.h"
#include "Settings.h"
#include "Mixer.h"
#include "ActionManager.h"
#include "BaseToolkit.h"
#include "UserInterfaceManager.h"
#include "SystemToolkit.h"
#include "InfoVisitor.h"
InfoVisitor::InfoVisitor() : brief_(true), current_id_(0)
{
}
void InfoVisitor::visit(Node &)
{
}
void InfoVisitor::visit(Group &)
{
}
void InfoVisitor::visit(Switch &)
{
}
void InfoVisitor::visit(Scene &)
{
}
void InfoVisitor::visit(Primitive &)
{
}
void InfoVisitor::visit(MediaPlayer &mp)
{
// do not ask twice
if (current_id_ == mp.id())
return;
std::ostringstream oss;
if (brief_) {
oss << SystemToolkit::filename(mp.filename()) << std::endl;
oss << mp.width() << " x " << mp.height() << ", ";
oss << mp.media().codec_name.substr(0, mp.media().codec_name.find_first_of(" (,"));
if (!mp.isImage())
oss << ", " << std::fixed << std::setprecision(1) << mp.frameRate() << " fps";
}
else {
oss << mp.filename() << std::endl;
oss << mp.media().codec_name << std::endl;
oss << mp.width() << " x " << mp.height() ;
if (!mp.isImage())
oss << ", " << std::fixed << std::setprecision(1) << mp.frameRate() << " fps";
}
information_ = oss.str();
// remember (except if codec was not identified yet)
if ( !mp.media().codec_name.empty() )
current_id_ = mp.id();
}
void InfoVisitor::visit(Stream &n)
{
std::ostringstream oss;
if (brief_) {
oss << BaseToolkit::splitted(n.description(), '!').front();
}
else {
oss << n.description();
}
information_ = oss.str();
}
void InfoVisitor::visit (MediaSource& s)
{
s.mediaplayer()->accept(*this);
}
void InfoVisitor::visit (SessionFileSource& s)
{
if (current_id_ == s.id() || s.session() == nullptr)
return;
std::ostringstream oss;
if (brief_) {
oss << SystemToolkit::filename(s.path()) << " (";
oss << s.session()->numSource() << " sources)" << std::endl;
}
else
oss << s.path() << std::endl;
if (s.session()->frame()){
oss << s.session()->frame()->width() << " x " << s.session()->frame()->height() << ", ";
oss << "RGB";
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (SessionGroupSource& s)
{
if (current_id_ == s.id() || s.session() == nullptr)
return;
std::ostringstream oss;
oss << s.session()->numSource() << " sources in group" << std::endl;
if (s.session()->frame()){
oss << s.session()->frame()->width() << " x " << s.session()->frame()->height() << ", ";
oss << "RGB";
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (RenderSource& s)
{
if (current_id_ == s.id())
return;
information_ = "Rendering Output";
current_id_ = s.id();
}
void InfoVisitor::visit (CloneSource& s)
{
if (current_id_ == s.id())
return;
information_ = "Clone of " + s.origin()->name();
current_id_ = s.id();
}
void InfoVisitor::visit (PatternSource& s)
{
if (current_id_ == s.id())
return;
std::ostringstream oss;
oss << Pattern::get(s.pattern()->type()).label << std::endl;
if (s.pattern()) {
oss << s.pattern()->width() << " x " << s.pattern()->height();
oss << ", RGB";
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (DeviceSource& s)
{
if (current_id_ == s.id())
return;
std::ostringstream oss;
DeviceConfigSet confs = Device::manager().config( Device::manager().index(s.device().c_str()));
if ( !confs.empty()) {
DeviceConfig best = *confs.rbegin();
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
if (brief_) {
oss << best.width << " x " << best.height << ", ";
oss << best.stream << " " << best.format << ", ";
oss << std::fixed << std::setprecision(1) << fps << " fps";
}
else {
oss << s.device() << std::endl;
oss << best.width << " x " << best.height << ", ";
oss << best.stream << " " << best.format << ", ";
oss << std::fixed << std::setprecision(1) << fps << " fps";
}
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (NetworkSource& s)
{
if (current_id_ == s.id())
return;
NetworkStream *ns = s.networkStream();
std::ostringstream oss;
if (brief_) {
oss << ns->resolution().x << " x " << ns->resolution().y << ", ";
oss << NetworkToolkit::protocol_name[ns->protocol()] << std::endl;
oss << "IP " << ns->serverAddress();
}
else {
oss << s.connection() << " (IP " << ns->serverAddress() << ")" << std::endl;
oss << ns->resolution().x << " x " << ns->resolution().y << ", ";
oss << NetworkToolkit::protocol_name[ns->protocol()];
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (MultiFileSource& s)
{
if (current_id_ == s.id())
return;
std::ostringstream oss;
if (brief_) {
oss << s.sequence().width << " x " << s.sequence().height << ", ";
oss << s.sequence().codec << std::endl;
oss << s.sequence().max - s.sequence().min + 1 << " images [";
oss << s.sequence().min << " - " << s.sequence().max << "]";
}
else {
oss << s.sequence().location << " [";
oss << s.sequence().min << " - " << s.sequence().max << "]" << std::endl;
oss << s.sequence().width << " x " << s.sequence().height << ", ";
oss << s.sequence().codec << " (";
oss << s.sequence().max - s.sequence().min + 1 << " images)";
}
information_ = oss.str();
current_id_ = s.id();
}
void InfoVisitor::visit (GenericStreamSource& s)
{
if (current_id_ == s.id())
return;
std::ostringstream oss;
if (s.stream()) {
std::string src_element = s.gstElements().front();
src_element = src_element.substr(0, src_element.find(" "));
oss << "gstreamer '" << src_element << "'" << std::endl;
oss << s.stream()->width() << " x " << s.stream()->height();
oss << ", RGB";
}
else
oss << "Undefined";
information_ = oss.str();
current_id_ = s.id();
}

41
InfoVisitor.h Normal file
View File

@@ -0,0 +1,41 @@
#ifndef INFOVISITOR_H
#define INFOVISITOR_H
#include "Visitor.h"
class InfoVisitor : public Visitor
{
std::string information_;
bool brief_;
uint64_t current_id_;
public:
InfoVisitor();
inline void setBriefStringMode () { brief_ = true; current_id_ = 0; }
inline void setExtendedStringMode () { brief_ = false; current_id_ = 0; }
inline void reset () { current_id_ = 0; }
inline std::string str () const { return information_; }
// Elements of Scene
void visit (Scene& n) override;
void visit (Node& n) override;
void visit (Group& n) override;
void visit (Switch& n) override;
void visit (Primitive& n) override;
// Elements with attributes
void visit (Stream& n) override;
void visit (MediaPlayer& n) override;
void visit (MediaSource& s) override;
void visit (SessionFileSource& s) override;
void visit (SessionGroupSource& s) override;
void visit (RenderSource& s) override;
void visit (CloneSource& s) override;
void visit (PatternSource& s) override;
void visit (DeviceSource& s) override;
void visit (NetworkSource& s) override;
void visit (MultiFileSource& s) override;
void visit (GenericStreamSource& s) override;
};
#endif // INFOVISITOR_H

View File

@@ -1,6 +1,24 @@
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "defines.h"
#include "Log.h"
@@ -132,14 +150,19 @@ Interpolator::Interpolator()
}
Interpolator::~Interpolator()
{
clear();
}
void Interpolator::clear()
{
for (auto i = interpolators_.begin(); i != interpolators_.end(); ) {
delete *i;
i = interpolators_.erase(i);
}
}
void Interpolator::add (Source *s, const SourceCore &target)
{
SourceInterpolator *i = new SourceInterpolator(s, target);

View File

@@ -30,6 +30,7 @@ public:
Interpolator();
~Interpolator();
void clear ();
void add (Source *s, const SourceCore &target );
void apply (float percent);

View File

@@ -1,11 +1,28 @@
// Opengl
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtx/vector_angle.hpp>
#include "imgui.h"
#include "ImGuiToolkit.h"
#include <string>
@@ -121,10 +138,9 @@ void LayerView::draw()
float depth_inc = (dsl.back()->depth() - depth) / static_cast<float>(Mixer::selection().size()-1);
for (++it; it != dsl.end(); ++it) {
depth += depth_inc;
(*it)->setDepth(depth);
(*it)->call( new SetDepth(depth, 80.f) );
}
Action::manager().store(std::string("Selection: Layer Distribute"));
++View::need_deep_update_;
}
if (ImGui::Selectable( ICON_FA_RULER_HORIZONTAL " Compress" )){
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
@@ -132,20 +148,18 @@ void LayerView::draw()
float depth = (*it)->depth();
for (++it; it != dsl.end(); ++it) {
depth += LAYER_STEP;
(*it)->setDepth(depth);
(*it)->call( new SetDepth(depth, 80.f) );
}
Action::manager().store(std::string("Selection: Layer Compress"));
++View::need_deep_update_;
}
if (ImGui::Selectable( ICON_FA_EXCHANGE_ALT " Reverse order" )){
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
SourceList::iterator it = dsl.begin();
SourceList::reverse_iterator rit = dsl.rbegin();
for (; it != dsl.end(); ++it, ++rit) {
(*it)->setDepth((*rit)->depth());
(*it)->call( new SetDepth((*rit)->depth(), 80.f) );
}
Action::manager().store(std::string("Selection: Layer Reverse order"));
++View::need_deep_update_;
}
ImGui::PopStyleColor(2);
@@ -200,9 +214,8 @@ void LayerView::resize ( int scale )
scene.root()->scale_.y = z;
// Clamp translation to acceptable area
glm::vec3 border_left(scene.root()->scale_.x * -2.f, scene.root()->scale_.y * -1.f, 0.f);
glm::vec3 border_right(scene.root()->scale_.x * 8.f, scene.root()->scale_.y * 8.f, 0.f);
scene.root()->translation_ = glm::clamp(scene.root()->translation_, border_left, border_right);
glm::vec3 border(2.f, 1.f, 0.f);
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border * 2.f);
}
int LayerView::size ()
@@ -292,7 +305,7 @@ float LayerView::setDepth(Source *s, float d)
return sourceNode->translation_.z;
}
View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2> pick)
View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2>)
{
if (!s)
return Cursor();
@@ -311,11 +324,9 @@ View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair
// apply change
float d = setDepth( s, MAX( -dest_translation.x, 0.f) );
// store action in history
std::ostringstream info;
info << "Depth " << std::fixed << std::setprecision(2) << d << " ";
// info << (s->locked() ? ICON_FA_LOCK : ICON_FA_LOCK_OPEN); // TODO static not locked
// store action in history
current_action_ = s->name() + ": " + info.str();
return Cursor(Cursor_ResizeNESW, info.str() );
@@ -323,8 +334,8 @@ View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair
void LayerView::arrow (glm::vec2 movement)
{
static int accumulator = 0;
accumulator++;
static float accumulator = 0.f;
accumulator += dt_;
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
glm::vec3 gl_Position_to = Rendering::manager().unProject(glm::vec2(movement.x-movement.y, 0.f), scene.root()->transform_);
@@ -347,17 +358,18 @@ void LayerView::arrow (glm::vec2 movement)
// + ALT : discrete displacement
if (UserInterface::manager().altModifier()) {
if (accumulator > 10) {
if (accumulator > 100.f) {
dest_translation += glm::sign(gl_delta) * 0.21f;
dest_translation.x = ROUND(dest_translation.x, 10.f);
accumulator = 0;
accumulator = 0.f;
}
else
break;
}
else {
// normal case: dest += delta
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
accumulator = 0.f;
}
// store action in history

171
Log.cpp
View File

@@ -1,14 +1,27 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <string>
#include <list>
#include <mutex>
using namespace std;
#include "imgui.h"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui_internal.h"
#include "defines.h"
#include "ImGuiToolkit.h"
#include "DialogToolkit.h"
@@ -66,7 +79,7 @@ struct AppLog
// window
ImGui::SameLine(0, 0);
static bool numbering = true;
ImGuiToolkit::ButtonToggle( ICON_FA_SORT_NUMERIC_DOWN, &numbering );
ImGuiToolkit::ButtonIconToggle(4, 12, 4, 12, &numbering );
ImGui::SameLine();
bool clear = ImGui::Button( ICON_FA_BACKSPACE " Clear");
ImGui::SameLine();
@@ -75,76 +88,82 @@ struct AppLog
Filter.Draw("Filter", -60.0f);
ImGui::Separator();
if ( ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) )
if ( !ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) )
{
if (clear)
Clear();
if (copy)
ImGui::LogToClipboard();
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
mtx.lock();
const char* buf = Buf.begin();
const char* buf_end = Buf.end();
if (Filter.IsActive())
{
// In this example we don't use the clipper when Filter is enabled.
// This is because we don't have a random access on the result on our filter.
// A real application processing logs with ten of thousands of entries may want to store the result of search/filter.
// especially if the filtering function is not trivial (e.g. reg-exp).
for (int line_no = 0; line_no < LineOffsets.Size; line_no++)
{
const char* line_start = buf + LineOffsets[line_no];
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
if (Filter.PassFilter(line_start, line_end))
ImGui::TextUnformatted(line_start, line_end);
}
}
else
{
// The simplest and easy way to display the entire buffer:
// ImGui::TextUnformatted(buf_begin, buf_end);
// And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines.
// Here we instead demonstrate using the clipper to only process lines that are within the visible area.
// If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended.
// Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height,
// both of which we can handle since we an array pointing to the beginning of each line of text.
// When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper.
// Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries)
ImGuiListClipper clipper;
clipper.Begin(LineOffsets.Size);
while (clipper.Step())
{
for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++)
{
const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6);
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
ImGui::TextUnformatted(line_start, line_end);
}
}
clipper.End();
}
mtx.unlock();
ImGui::PopStyleVar();
ImGui::PopFont();
// Auto scroll
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
ImGui::End();
return;
}
if (clear)
Clear();
if (copy)
ImGui::LogToClipboard();
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
mtx.lock();
const char* buf = Buf.begin();
const char* buf_end = Buf.end();
if (Filter.IsActive())
{
// In this example we don't use the clipper when Filter is enabled.
// This is because we don't have a random access on the result on our filter.
// A real application processing logs with ten of thousands of entries may want to store the result of search/filter.
// especially if the filtering function is not trivial (e.g. reg-exp).
for (int line_no = 0; line_no < LineOffsets.Size; line_no++)
{
const char* line_start = buf + LineOffsets[line_no];
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
if (Filter.PassFilter(line_start, line_end))
ImGui::TextUnformatted(line_start, line_end);
}
}
else
{
// The simplest and easy way to display the entire buffer:
// ImGui::TextUnformatted(buf_begin, buf_end);
// And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines.
// Here we instead demonstrate using the clipper to only process lines that are within the visible area.
// If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended.
// Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height,
// both of which we can handle since we an array pointing to the beginning of each line of text.
// When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper.
// Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries)
ImGuiListClipper clipper;
clipper.Begin(LineOffsets.Size);
while (clipper.Step())
{
for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++)
{
const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6);
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
ImGui::TextUnformatted(line_start, line_end);
}
}
clipper.End();
}
mtx.unlock();
ImGui::PopStyleVar();
ImGui::PopFont();
// Auto scroll
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
ImGui::End();
}
};
static AppLog logs;
AppLog logs;
list<string> notifications;
list<string> warnings;
float notifications_timeout = 0.f;
void Log::Info(const char* fmt, ...)
{
@@ -157,12 +176,9 @@ void Log::Info(const char* fmt, ...)
void Log::ShowLogWindow(bool* p_open)
{
ImGui::SetNextWindowSize(ImVec2(700, 600), ImGuiCond_FirstUseEver);
logs.Draw( ICON_FA_LIST_UL " Logs", p_open);
logs.Draw( IMGUI_TITLE_LOGS, p_open);
}
static list<string> notifications;
static float notifications_timeout = 0.f;
void Log::Notify(const char* fmt, ...)
{
ImGuiTextBuffer buf;
@@ -177,12 +193,9 @@ void Log::Notify(const char* fmt, ...)
notifications_timeout = 0.f;
// always log
Log::Info("%s", buf.c_str());
Log::Info(ICON_FA_INFO_CIRCLE " %s", buf.c_str());
}
static list<string> warnings;
void Log::Warning(const char* fmt, ...)
{
ImGuiTextBuffer buf;
@@ -196,7 +209,7 @@ void Log::Warning(const char* fmt, ...)
warnings.push_back(buf.c_str());
// always log
Log::Info("Warning - %s\n", buf.c_str());
Log::Info(ICON_FA_EXCLAMATION_TRIANGLE " Warning - %s", buf.c_str());
}
void Log::Render(bool *showWarnings)
@@ -248,7 +261,7 @@ void Log::Render(bool *showWarnings)
if (ImGui::BeginPopupModal("Warning", NULL, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGuiToolkit::Icon(9, 4);
ImGui::SameLine(0, 10);
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(width);
ImGui::TextColored(ImVec4(1.0f,0.6f,0.0f,1.0f), "%ld error(s) occured.\n\n", warnings.size());
ImGui::Dummy(ImVec2(width, 0));

View File

@@ -1,4 +1,21 @@
#include <thread>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
// Desktop OpenGL function loader
#include <glad/glad.h>
@@ -151,20 +168,17 @@ bool Loopback::systemLoopbackInitialized()
Loopback::Loopback() : FrameGrabber()
{
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 60);
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 30); // fixed 30 FPS
}
void Loopback::init(GstCaps *caps)
std::string Loopback::init(GstCaps *caps)
{
// ignore
if (caps == nullptr)
return;
return std::string("Invalid caps");
if (!Loopback::systemLoopbackInitialized()){
Log::Warning("Loopback system shall be initialized first.");
finished_ = true;
return;
return std::string("Loopback system shall be initialized first.");
}
// create a gstreamer pipeline
@@ -174,10 +188,9 @@ void Loopback::init(GstCaps *caps)
GError *error = NULL;
pipeline_ = gst_parse_launch (description.c_str(), &error);
if (error != NULL) {
Log::Warning("Loopback Could not construct pipeline %s:\n%s", description.c_str(), error->message);
std::string msg = std::string("Loopback : Could not construct pipeline ") + description + "\n" + std::string(error->message);
g_clear_error (&error);
finished_ = true;
return;
return msg;
}
// setup device sink
@@ -190,49 +203,51 @@ void Loopback::init(GstCaps *caps)
if (src_) {
g_object_set (G_OBJECT (src_),
"stream-type", GST_APP_STREAM_TYPE_STREAM,
"is-live", TRUE,
"format", GST_FORMAT_TIME,
// "do-timestamp", TRUE,
NULL);
// Direct encoding (no buffering)
gst_app_src_set_max_bytes( src_, 0 );
// configure stream
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
gst_app_src_set_latency( src_, -1, 0);
// instruct src to use the required caps
caps_ = gst_caps_copy( caps );
// Set buffer size
gst_app_src_set_max_bytes( src_, buffering_size_ );
// specify streaming framerate in the given caps
GstCaps *tmp = gst_caps_copy( caps );
GValue v = { 0, };
g_value_init (&v, GST_TYPE_FRACTION);
gst_value_set_fraction (&v, 30, 1); // fixed 30 FPS
gst_caps_set_value(tmp, "framerate", &v);
g_value_unset (&v);
// instruct src to use the caps
caps_ = gst_caps_copy( tmp );
gst_app_src_set_caps (src_, caps_);
gst_caps_unref (tmp);
// setup callbacks
GstAppSrcCallbacks callbacks;
callbacks.need_data = FrameGrabber::callback_need_data;
callbacks.enough_data = FrameGrabber::callback_enough_data;
callbacks.seek_data = NULL; // stream type is not seekable
gst_app_src_set_callbacks (src_, &callbacks, this, NULL);
gst_app_src_set_callbacks( src_, &callbacks, this, NULL);
}
else {
Log::Warning("Loopback Could not configure source");
finished_ = true;
return;
return std::string("Loopback : Could not configure source.");
}
// start recording
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
Log::Warning("Loopback Could not open %s", Loopback::system_loopback_name.c_str());
finished_ = true;
return;
return std::string("Loopback : Could not open ") + Loopback::system_loopback_name;
}
// all good
#if defined(LINUX)
Log::Notify("Loopback started (v4l2loopback on %s)", Loopback::system_loopback_name.c_str());
#else
Log::Notify("Loopback started (%s)", Loopback::system_loopback_name.c_str());
#endif
// start
active_ = true;
initialized_ = true;
return std::string("Loopback started on ") + Loopback::system_loopback_name;
}
void Loopback::terminate()

View File

@@ -15,7 +15,7 @@ class Loopback : public FrameGrabber
static std::string system_loopback_name;
static bool system_loopback_initialized;
void init(GstCaps *caps) override;
std::string init(GstCaps *caps) override;
void terminate() override;
public:

View File

@@ -1,12 +1,25 @@
#include <thread>
using namespace std;
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
// Desktop OpenGL function loader
#include <glad/glad.h>
// vmix
#include "defines.h"
#include "Log.h"
#include "Resource.h"
@@ -15,6 +28,7 @@ using namespace std;
#include "BaseToolkit.h"
#include "GstToolkit.h"
#include "RenderingManager.h"
#include "Metronome.h"
#include "MediaPlayer.h"
@@ -36,9 +50,13 @@ MediaPlayer::MediaPlayer()
desired_state_ = GST_STATE_PAUSED;
failed_ = false;
pending_ = false;
metro_sync_ = Metronome::SYNC_NONE;
force_update_ = false;
seeking_ = false;
rewind_on_disable_ = false;
force_software_decoding_ = false;
hardware_decoder_ = "";
decoder_name_ = "";
rate_ = 1.0;
position_ = GST_CLOCK_TIME_NONE;
loop_ = LoopMode::LOOP_REWIND;
@@ -166,7 +184,8 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
video_stream_info.framerate_n = gst_discoverer_video_info_get_framerate_num(vinfo);
video_stream_info.framerate_d = gst_discoverer_video_info_get_framerate_denom(vinfo);
if (video_stream_info.framerate_n == 0 || video_stream_info.framerate_d == 0) {
video_stream_info.framerate_n = 25;
Log::Info("'%s': No framerate indicated in the file; using default 30fps", uri.c_str());
video_stream_info.framerate_n = 30;
video_stream_info.framerate_d = 1;
}
video_stream_info.dt = ( (GST_SECOND * static_cast<guint64>(video_stream_info.framerate_d)) / (static_cast<guint64>(video_stream_info.framerate_n)) );
@@ -186,7 +205,7 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
if ( tags ) {
gchar *container = NULL;
if ( gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container) )
video_stream_info.codec_name += " " + std::string(container);
video_stream_info.codec_name += ", " + std::string(container);
if (container)
g_free(container);
}
@@ -220,7 +239,7 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
return video_stream_info;
}
void MediaPlayer::open (const std::string & filename, const string &uri)
void MediaPlayer::open (const std::string & filename, const std::string &uri)
{
// set path
filename_ = BaseToolkit::transliterate( filename );
@@ -231,6 +250,9 @@ void MediaPlayer::open (const std::string & filename, const string &uri)
else
uri_ = uri;
if (uri_.empty())
failed_ = true;
// close before re-openning
if (isOpen())
close();
@@ -260,13 +282,13 @@ void MediaPlayer::reopen()
}
}
void MediaPlayer::execute_open()
{
void MediaPlayer::execute_open()
{
// Create gstreamer pipeline :
// "uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! appsink "
// equivalent to command line
// "gst-launch-1.0 uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! ximagesink"
string description = "uridecodebin name=decoder uri=" + uri_ + " ! queue max-size-time=0 ! ";
std::string description = "uridecodebin name=decoder uri=" + uri_ + " ! queue max-size-time=0 ! ";
// NB: queue adds some control over the buffer, thereby limiting the frame delay. zero size means no buffering
// string description = "uridecodebin name=decoder uri=" + uri_ + " decoder. ! ";
@@ -319,8 +341,8 @@ void MediaPlayer::execute_open()
g_object_set(G_OBJECT(pipeline_), "name", std::to_string(id_).c_str(), NULL);
gst_pipeline_set_auto_flush_bus( GST_PIPELINE(pipeline_), true);
// GstCaps *caps = gst_static_caps_get (&frame_render_caps);
string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
// GstCaps *caps = gst_static_caps_get (&frame_render_caps);
std::string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
",height=" + std::to_string(media_.height);
GstCaps *caps = gst_caps_from_string(capstring.c_str());
if (!gst_video_info_from_caps (&v_frame_video_info_, caps)) {
@@ -381,7 +403,7 @@ void MediaPlayer::execute_open()
gst_caps_unref (caps);
#ifdef USE_GST_OPENGL_SYNC_HANDLER
// capture bus signals to force a unique opengl context for all GST elements
// capture bus signals to force a unique opengl context for all GST elements
Rendering::LinkPipeline(GST_PIPELINE (pipeline_));
#endif
@@ -402,7 +424,7 @@ void MediaPlayer::execute_open()
// all good
Log::Info("MediaPlayer %s Opened '%s' (%s %d x %d)", std::to_string(id_).c_str(),
uri_.c_str(), media_.codec_name.c_str(), media_.width, media_.height);
SystemToolkit::filename(uri_).c_str(), media_.codec_name.c_str(), media_.width, media_.height);
Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());
@@ -448,14 +470,13 @@ void MediaPlayer::close()
if (pipeline_ != nullptr) {
// force flush
GstState state;
gst_element_send_event(pipeline_, gst_event_new_seek (1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
GST_SEEK_TYPE_NONE, 0, GST_SEEK_TYPE_NONE, 0) );
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
// end pipeline
gst_element_set_state (pipeline_, GST_STATE_NULL);
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
gst_object_unref (pipeline_);
pipeline_ = nullptr;
@@ -513,15 +534,19 @@ void MediaPlayer::enable(bool on)
if ( enabled_ != on ) {
// option to automatically rewind each time the player is disabled
if (!on && rewind_on_disable_ && desired_state_ == GST_STATE_PLAYING)
rewind(true);
// apply change
enabled_ = on;
// default to pause
GstState requested_state = GST_STATE_PAUSED;
// unpause only if enabled
if (enabled_) {
if (enabled_)
requested_state = desired_state_;
}
// apply state change
GstStateChangeReturn ret = gst_element_set_state (pipeline_, requested_state);
@@ -543,9 +568,18 @@ bool MediaPlayer::isImage() const
return media_.isimage;
}
std::string MediaPlayer::hardwareDecoderName()
std::string MediaPlayer::decoderName()
{
return hardware_decoder_;
// decoder_name_ not initialized
if (decoder_name_.empty()) {
// try to know if it is a hardware decoder
decoder_name_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
// nope, then it is a sofware decoder
if (decoder_name_.empty())
decoder_name_ = "software";
}
return decoder_name_;
}
bool MediaPlayer::softwareDecodingForced()
@@ -559,19 +593,16 @@ void MediaPlayer::setSoftwareDecodingForced(bool on)
// set parameter
force_software_decoding_ = on;
decoder_name_ = "";
// changing state requires reload
if (need_reload)
reopen();
}
void MediaPlayer::play(bool on)
void MediaPlayer::execute_play_command(bool on)
{
// ignore if disabled, and cannot play an image
if (!enabled_ || media_.isimage)
return;
// request state
// request state
GstState requested_state = on ? GST_STATE_PLAYING : GST_STATE_PAUSED;
// ignore if requesting twice same state
@@ -587,9 +618,10 @@ void MediaPlayer::play(bool on)
// requesting to play, but stopped at end of stream : rewind first !
if ( desired_state_ == GST_STATE_PLAYING) {
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
rewind();
if (rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()))
execute_seek_command(timeline_.next(0));
else if ( rate_ < 0.0 && position_ <= timeline_.next(0) )
execute_seek_command(timeline_.previous(timeline_.last()));
}
// all ready, apply state change immediately
@@ -597,17 +629,37 @@ void MediaPlayer::play(bool on)
if (ret == GST_STATE_CHANGE_FAILURE) {
Log::Warning("MediaPlayer %s Failed to play", std::to_string(id_).c_str());
failed_ = true;
}
}
#ifdef MEDIA_PLAYER_DEBUG
else if (on)
Log::Info("MediaPlayer %s Start", std::to_string(id_).c_str());
else
Log::Info("MediaPlayer %s Stop [%ld]", std::to_string(id_).c_str(), position());
#endif
}
// reset time counter
timecount_.reset();
void MediaPlayer::play(bool on)
{
// ignore if disabled, and cannot play an image
if (!enabled_ || media_.isimage || pending_)
return;
// Metronome
if (metro_sync_ > Metronome::SYNC_NONE) {
// busy with this delayed action
pending_ = true;
// delayed execution function
std::function<void()> playlater = std::bind([](MediaPlayer *p, bool o) {
p->execute_play_command(o); p->pending_=false; }, this, on);
// Execute: sync to Metronome
if (metro_sync_ > Metronome::SYNC_BEAT)
Metronome::manager().executeAtPhase( playlater );
else
Metronome::manager().executeAtBeat( playlater );
}
else
// execute immediately
execute_play_command( on );
}
bool MediaPlayer::isPlaying(bool testpipeline) const
@@ -631,44 +683,77 @@ MediaPlayer::LoopMode MediaPlayer::loop() const
{
return loop_;
}
void MediaPlayer::setLoop(MediaPlayer::LoopMode mode)
{
loop_ = mode;
}
void MediaPlayer::rewind()
//void
void MediaPlayer::rewind(bool force)
{
if (!enabled_ || !media_.seekable)
if (!enabled_ || !media_.seekable || pending_)
return;
// playing forward, loop to begin
if (rate_ > 0.0) {
// begin is the end of a gab which includes the first PTS (if exists)
// normal case, begin is zero
execute_seek_command( timeline_.next(0) );
}
// playing forward, loop to begin;
// begin is the end of a gab which includes the first PTS (if exists)
// normal case, begin is zero
// playing backward, loop to endTimeInterval gap;
else {
// end is the start of a gab which includes the last PTS (if exists)
// normal case, end is last frame
execute_seek_command( timeline_.previous(timeline_.last()) );
// end is the start of a gab which includes the last PTS (if exists)
// normal case, end is last frame
GstClockTime target = (rate_ > 0.0) ? timeline_.next(0) : timeline_.previous(timeline_.last());
// Metronome
if (metro_sync_) {
// busy with this delayed action
pending_ = true;
// delayed execution function
std::function<void()> rewindlater = std::bind([](MediaPlayer *p, GstClockTime t, bool f) {
p->execute_seek_command( t, f ); p->pending_=false; }, this, target, force);
// Execute: sync to Metronome
if (metro_sync_ > Metronome::SYNC_BEAT)
Metronome::manager().executeAtPhase( rewindlater );
else
Metronome::manager().executeAtBeat( rewindlater );
}
else
// execute immediately
execute_seek_command( target, force );
}
void MediaPlayer::step()
{
// useful only when Paused
if (!enabled_ || isPlaying())
if (!enabled_ || isPlaying() || pending_)
return;
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
rewind();
else {
// step event
GstEvent *stepevent = gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE);
// step
gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE));
// Metronome
if (metro_sync_) {
// busy with this delayed action
pending_ = true;
// delayed execution function
std::function<void()> steplater = std::bind([](MediaPlayer *p, GstEvent *e) {
gst_element_send_event(p->pipeline_, e); p->pending_=false; }, this, stepevent) ;
// Execute: sync to Metronome
if (metro_sync_ > Metronome::SYNC_BEAT)
Metronome::manager().executeAtPhase( steplater );
else
Metronome::manager().executeAtBeat( steplater );
}
else
// execute immediately
gst_element_send_event (pipeline_, stepevent);
}
}
bool MediaPlayer::go_to(GstClockTime pos)
@@ -679,7 +764,7 @@ bool MediaPlayer::go_to(GstClockTime pos)
GstClockTime jumpPts = pos;
if (timeline_.gapAt(pos, gap)) {
if (timeline_.getGapAt(pos, gap)) {
// if in a gap, find closest seek target
if (gap.is_valid()) {
// jump in one or the other direction
@@ -765,14 +850,10 @@ void MediaPlayer::init_texture(guint index)
pbo_index_ = 0;
pbo_next_index_ = 1;
#ifdef MEDIA_PLAYER_DEBUG
Log::Info("MediaPlayer %s Using Pixel Buffer Object texturing.", std::to_string(id_).c_str());
#endif
// now that a frame is ready, and once only, browse into the pipeline
// for possible hadrware decoding plugins used. Empty string means none.
hardware_decoder_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
// initialize decoderName once
Log::Info("MediaPlayer %s Uses %s decoding and OpenGL PBO texturing.", std::to_string(id_).c_str(), decoderName().c_str());
}
glBindTexture(GL_TEXTURE_2D, 0);
}
@@ -800,19 +881,21 @@ void MediaPlayer::fill_texture(guint index)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// bind the next PBO to write pixels
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);
#ifdef USE_GL_BUFFER_SUBDATA
glBufferSubData(GL_PIXEL_UNPACK_BUFFER, 0, pbo_size_, frame_[index].vframe.data[0]);
#else
// update data directly on the mapped buffer
// NB : equivalent but faster than glBufferSubData (memmove instead of memcpy ?)
// See http://www.songho.ca/opengl/gl_pbo.html#map for more details
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
// map the buffer object into client's memory
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if (ptr) {
// update data directly on the mapped buffer
// NB : equivalent but faster (memmove instead of memcpy ?) than
// glNamedBufferSubData(pboIds[nextIndex], 0, imgsize, vp->getBuffer())
memmove(ptr, frame_[index].vframe.data[0], pbo_size_);
// release pointer to mapping buffer
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
#endif
// done with PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
}
@@ -855,7 +938,7 @@ void MediaPlayer::update()
}
// prevent unnecessary updates: disabled or already filled image
if (!enabled_ || (media_.isimage && textureindex_>0 ) )
if ( (!enabled_ && !force_update_) || (media_.isimage && textureindex_>0 ) )
return;
// local variables before trying to update
@@ -866,13 +949,6 @@ void MediaPlayer::update()
index_lock_.lock();
// get the last frame filled from fill_frame()
read_index = last_index_;
// // Do NOT miss and jump directly (after seek) to a pre-roll
// for (guint i = 0; i < N_VFRAME; ++i) {
// if (frame_[i].status == PREROLL) {
// read_index = i;
// break;
// }
// }
// unlock access to index change
index_lock_.unlock();
@@ -915,8 +991,7 @@ void MediaPlayer::update()
// if already seeking (asynch)
if (seeking_) {
// request status update to pipeline (re-sync gst thread)
GstState state;
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
// seek should be resolved next frame
seeking_ = false;
// do NOT do another seek yet
@@ -925,13 +1000,18 @@ void MediaPlayer::update()
else {
// manage timeline: test if position falls into a gap
TimeInterval gap;
if (position_ != GST_CLOCK_TIME_NONE && timeline_.gapAt(position_, gap)) {
if (position_ != GST_CLOCK_TIME_NONE && timeline_.getGapAt(position_, gap)) {
// if in a gap, seek to next section
if (gap.is_valid()) {
// jump in one or the other direction
GstClockTime jumpPts = (rate_>0.f) ? gap.end : gap.begin;
// seek to next valid time (if not beginnig or end of timeline)
GstClockTime jumpPts = timeline_.step(); // round jump time to frame pts
if ( rate_ > 0.f )
jumpPts *= ( gap.end / timeline_.step() ) + 1; // FWD: go to end of gap
else
jumpPts *= ( gap.begin / timeline_.step() ); // BWD: go to begin of gap
// (if not beginnig or end of timeline)
if (jumpPts > timeline_.first() && jumpPts < timeline_.last())
// seek to jump PTS time
seek( jumpPts );
// otherwise, we should loop
else
@@ -944,13 +1024,15 @@ void MediaPlayer::update()
if (need_loop) {
execute_loop_command();
}
force_update_ = false;
}
void MediaPlayer::execute_loop_command()
{
if (loop_==LOOP_REWIND) {
rewind();
}
}
else if (loop_==LOOP_BIDIRECTIONAL) {
rate_ *= - 1.f;
execute_seek_command();
@@ -960,7 +1042,7 @@ void MediaPlayer::execute_loop_command()
}
}
void MediaPlayer::execute_seek_command(GstClockTime target)
void MediaPlayer::execute_seek_command(GstClockTime target, bool force)
{
if ( pipeline_ == nullptr || !media_.seekable )
return;
@@ -969,7 +1051,7 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
GstClockTime seek_pos = target;
// no target given
if (target == GST_CLOCK_TIME_NONE)
if (target == GST_CLOCK_TIME_NONE)
// create seek event with current position (rate changed ?)
seek_pos = position_;
// target is given but useless
@@ -980,9 +1062,12 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
// seek with flush (always)
int seek_flags = GST_SEEK_FLAG_FLUSH;
// seek with trick mode if fast speed
if ( ABS(rate_) > 1.0 )
if ( ABS(rate_) > 1.5 )
seek_flags |= GST_SEEK_FLAG_TRICKMODE;
else
seek_flags |= GST_SEEK_FLAG_ACCURATE;
// create seek event depending on direction
GstEvent *seek_event = nullptr;
@@ -1005,6 +1090,12 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
#endif
}
// Force update
if (force) {
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
force_update_ = true;
}
}
void MediaPlayer::setPlaySpeed(double s)
@@ -1012,12 +1103,12 @@ void MediaPlayer::setPlaySpeed(double s)
if (media_.isimage)
return;
// bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
// bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
rate_ = CLAMP(s, -MAX_PLAY_SPEED, MAX_PLAY_SPEED);
// skip interval [-MIN_PLAY_SPEED MIN_PLAY_SPEED]
if (ABS(rate_) < MIN_PLAY_SPEED)
rate_ = SIGN(rate_) * MIN_PLAY_SPEED;
// apply with seek
execute_seek_command();
}
@@ -1096,7 +1187,9 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
// get the frame from buffer
if ( !gst_video_frame_map (&frame_[write_index_].vframe, &v_frame_video_info_, buf, GST_MAP_READ ) )
{
#ifdef MEDIA_PLAYER_DEBUG
Log::Info("MediaPlayer %s Failed to map the video buffer", std::to_string(id_).c_str());
#endif
// free access to frame & exit
frame_[write_index_].status = INVALID;
frame_[write_index_].access.unlock();
@@ -1113,7 +1206,7 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
frame_[write_index_].position = buf->pts;
// set the start position (i.e. pts of first frame we got)
if (timeline_.begin() == GST_CLOCK_TIME_NONE) {
if (timeline_.first() == GST_CLOCK_TIME_NONE) {
timeline_.setFirst(buf->pts);
}
}
@@ -1123,6 +1216,7 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
#ifdef MEDIA_PLAYER_DEBUG
Log::Info("MediaPlayer %s Received an Invalid frame", std::to_string(id_).c_str());
#endif
// free access to frame & exit
frame_[write_index_].status = INVALID;
frame_[write_index_].access.unlock();
return false;
@@ -1233,54 +1327,27 @@ GstFlowReturn MediaPlayer::callback_new_sample (GstAppSink *sink, gpointer p)
MediaPlayer::TimeCounter::TimeCounter() {
MediaPlayer::TimeCounter::TimeCounter()
{
timer = g_timer_new ();
}
reset();
MediaPlayer::TimeCounter::~TimeCounter()
{
g_free(timer);
}
void MediaPlayer::TimeCounter::tic ()
{
// how long since last time
GstClockTime t = gst_util_get_timestamp ();
GstClockTime dt = t - last_time;
const double dt = g_timer_elapsed (timer, NULL) * 1000.0;
// one more frame since last time
nbFrames++;
// calculate instantaneous framerate
// Exponential moving averate with previous framerate to filter jitter (50/50)
// The divition of frame/time is done on long integer GstClockTime, counting in microsecond
// NB: factor 100 to get 0.01 precision
fps = 0.5 * fps + 0.005 * static_cast<double>( ( 100 * GST_SECOND * nbFrames ) / dt );
// reset counter every second
if ( dt >= GST_SECOND)
{
last_time = t;
nbFrames = 0;
// ignore refresh after too little time
if (dt > 3.0){
// restart timer
g_timer_start(timer);
// calculate instantaneous framerate
// Exponential moving averate with previous framerate to filter jitter
fps = CLAMP( 0.5 * fps + 500.0 / dt, 0.0, 1000.0);
}
}
GstClockTime MediaPlayer::TimeCounter::dt ()
{
GstClockTime t = gst_util_get_timestamp ();
GstClockTime dt = t - tic_time;
tic_time = t;
// return the instantaneous delta t
return dt;
}
void MediaPlayer::TimeCounter::reset ()
{
last_time = gst_util_get_timestamp ();;
tic_time = last_time;
nbFrames = 0;
fps = 0.0;
}
double MediaPlayer::TimeCounter::frameRate() const
{
return fps;
}

View File

@@ -12,6 +12,7 @@
#include <gst/app/gstappsink.h>
#include "Timeline.h"
#include "Metronome.h"
// Forward declare classes referenced
class Visitor;
@@ -42,7 +43,7 @@ struct MediaInfo {
bitrate = 0;
framerate_n = 1;
framerate_d = 25;
codec_name = "unknown";
codec_name = "";
isimage = false;
interlaced = false;
seekable = false;
@@ -187,7 +188,15 @@ public:
/**
* Seek to zero
* */
void rewind();
void rewind(bool force = false);
/**
* pending
* */
bool pending() const { return pending_; }
/**
* Get position time
* */
GstClockTime position();
/**
* go to a valid position in media timeline
* pos in nanoseconds.
@@ -210,10 +219,6 @@ public:
void setTimeline(const Timeline &tl);
float currentTimelineFading();
/**
* Get position time
* */
GstClockTime position();
/**
* Get framerate of the media
* */
@@ -232,7 +237,7 @@ public:
* */
guint height() const;
/**
* Get frames displayt aspect ratio
* Get frames display aspect ratio
* NB: can be different than width() / height()
* */
float aspectRatio() const;
@@ -242,19 +247,31 @@ public:
* */
guint texture() const;
/**
* Get the name of the hardware decoder used
* Empty string if none (i.e. software decoding)
* Get the name of the decoder used,
* return 'software' if no hardware decoder is used
* NB: perform request on pipeline on first call
* */
std::string hardwareDecoderName();
std::string decoderName();
/**
* Forces open using software decoding
* (i.e. without hadrware decoding)
* NB: this reopens the video and reset decoder name
* */
void setSoftwareDecodingForced(bool on);
bool softwareDecodingForced();
/**
* Option to automatically rewind each time the player is disabled
* (i.e. when enable(false) is called )
* */
inline void setRewindOnDisabled(bool on) { rewind_on_disable_ = on; }
inline bool rewindOnDisabled() const { return rewind_on_disable_; }
/**
* Option to synchronize with metronome
* */
inline void setSyncToMetronome(Metronome::Synchronicity s) { metro_sync_ = s; }
inline Metronome::Synchronicity syncToMetronome() const { return metro_sync_; }
/**
* Accept visitors
* Used for saving session file
* */
void accept(Visitor& v);
/**
@@ -289,24 +306,24 @@ private:
GstVideoInfo v_frame_video_info_;
std::atomic<bool> opened_;
std::atomic<bool> failed_;
bool force_update_;
bool pending_;
bool seeking_;
bool enabled_;
bool rewind_on_disable_;
bool force_software_decoding_;
std::string hardware_decoder_;
std::string decoder_name_;
Metronome::Synchronicity metro_sync_;
// fps counter
struct TimeCounter {
GstClockTime last_time;
GstClockTime tic_time;
int nbFrames;
GTimer *timer;
gdouble fps;
public:
TimeCounter();
GstClockTime dt();
~TimeCounter();
void tic();
void reset();
gdouble frameRate() const;
inline gdouble frameRate() const { return fps; }
};
TimeCounter timecount_;
@@ -344,8 +361,9 @@ private:
// gst pipeline control
void execute_open();
void execute_play_command(bool on);
void execute_loop_command();
void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE);
void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE, bool force = false);
// gst frame filling
void init_texture(guint index);

View File

@@ -1,6 +1,24 @@
#include <glm/gtc/matrix_transform.hpp>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "MediaSource.h"
#include <glm/gtc/matrix_transform.hpp>
#include "defines.h"
#include "ImageShader.h"
@@ -11,6 +29,8 @@
#include "Visitor.h"
#include "Log.h"
#include "MediaSource.h"
MediaSource::MediaSource(uint64_t id) : Source(id), path_("")
{
// create media player
@@ -26,7 +46,6 @@ MediaSource::~MediaSource()
void MediaSource::setPath(const std::string &p)
{
path_ = p;
Log::Notify("Creating Source with media '%s'", path_.c_str());
// open gstreamer
mediaplayer_->open(path_);
@@ -49,9 +68,14 @@ MediaPlayer *MediaSource::mediaplayer() const
glm::ivec2 MediaSource::icon() const
{
if (mediaplayer_->isImage())
return glm::ivec2(4, 9);
return glm::ivec2(ICON_SOURCE_IMAGE);
else
return glm::ivec2(18, 13);
return glm::ivec2(ICON_SOURCE_VIDEO);
}
std::string MediaSource::info() const
{
return std::string("media '") + path_ + "'";
}
bool MediaSource::failed() const
@@ -108,14 +132,48 @@ void MediaSource::setActive (bool on)
{
bool was_active = active_;
// try to activate (may fail if source is cloned)
Source::setActive(on);
// change status of media player (only if status changed)
if ( active_ != was_active ) {
if ( active_ != was_active )
mediaplayer_->enable(active_);
// change visibility of active surface (show preview of media when inactive)
if (activesurface_) {
if (active_)
activesurface_->setTextureIndex(Resource::getTextureTransparent());
else
activesurface_->setTextureIndex(mediaplayer_->texture());
}
}
bool MediaSource::playing () const
{
return mediaplayer_->isPlaying();
}
void MediaSource::play (bool on)
{
mediaplayer_->play(on);
}
bool MediaSource::playable () const
{
return !mediaplayer_->isImage();
}
void MediaSource::replay ()
{
mediaplayer_->rewind();
}
guint64 MediaSource::playtime () const
{
return mediaplayer_->position();
}
void MediaSource::update(float dt)
{
Source::update(dt);

View File

@@ -14,6 +14,11 @@ public:
// implementation of source API
void update (float dt) override;
void setActive (bool on) override;
bool playing () const override;
void play (bool) override;
bool playable () const override;
void replay () override;
guint64 playtime () const override;
void render() override;
bool failed() const override;
uint texture() const override;
@@ -25,6 +30,7 @@ public:
MediaPlayer *mediaplayer() const;
glm::ivec2 icon() const override;
std::string info() const override;
protected:

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <iostream>
#include <sstream>
#include <istream>
@@ -16,9 +35,10 @@
#include "Resource.h"
#include "ImageShader.h"
#include "Visitor.h"
#include "Log.h"
#include "Mesh.h"
#include "GlmToolkit.h"
#include "Log.h"
#include "Mesh.h"
using namespace std;
using namespace glm;
@@ -227,7 +247,6 @@ bool parsePLY(string ascii,
// a numerical property
if ( ! prop.is_list ) {
float value;
switch ( prop.name[0] ) {
case 'x':
point.x = parseValue<float>(stringstream);
@@ -263,7 +282,7 @@ bool parsePLY(string ascii,
break;
default:
// ignore normals or other types
value = parseValue<float>(stringstream);
parseValue<float>(stringstream);
break;
}
}

253
Metronome.cpp Normal file
View File

@@ -0,0 +1,253 @@
#include <atomic>
#include <iomanip>
#include <iostream>
#include <thread>
/// Ableton Link is a technology that synchronizes musical beat, tempo,
/// and phase across multiple applications running on one or more devices.
/// Applications on devices connected to a local network discover each other
/// automatically and form a musical session in which each participant can
/// perform independently: anyone can start or stop while still staying in time.
/// Anyone can change the tempo, the others will follow.
/// Anyone can join or leave without disrupting the session.
///
/// https://ableton.github.io/link/
///
#include <ableton/Link.hpp>
#include "Settings.h"
#include "Metronome.h"
#include "Log.h"
namespace ableton
{
/// Inspired from Dummy audio platform example
/// https://github.com/Ableton/link/blob/master/examples/linkaudio/AudioPlatform_Dummy.hpp
class Engine
{
public:
Engine(Link& link)
: mLink(link)
, mQuantum(4.)
{
}
void startPlaying()
{
auto sessionState = mLink.captureAppSessionState();
sessionState.setIsPlayingAndRequestBeatAtTime(true, now(), 0., mQuantum);
mLink.commitAppSessionState(sessionState);
}
void stopPlaying()
{
auto sessionState = mLink.captureAppSessionState();
sessionState.setIsPlaying(false, now());
mLink.commitAppSessionState(sessionState);
}
bool isPlaying() const
{
return mLink.captureAppSessionState().isPlaying();
}
double beatTime() const
{
auto sessionState = mLink.captureAppSessionState();
return sessionState.beatAtTime(now(), mQuantum);
}
std::chrono::microseconds timeNextBeat() const
{
auto sessionState = mLink.captureAppSessionState();
double beat = ceil(sessionState.beatAtTime(now(), mQuantum));
return sessionState.timeAtBeat(beat, mQuantum);
}
double phaseTime() const
{
auto sessionState = mLink.captureAppSessionState();
return sessionState.phaseAtTime(now(), mQuantum);
}
std::chrono::microseconds timeNextPhase() const
{
auto sessionState = mLink.captureAppSessionState();
double phase = ceil(sessionState.phaseAtTime(now(), mQuantum));
double beat = ceil(sessionState.beatAtTime(now(), mQuantum));
return sessionState.timeAtBeat(beat + (mQuantum-phase), mQuantum);
}
double tempo() const
{
auto sessionState = mLink.captureAppSessionState();
return sessionState.tempo();
}
double setTempo(double tempo)
{
auto sessionState = mLink.captureAppSessionState();
sessionState.setTempo(tempo, now());
mLink.commitAppSessionState(sessionState);
return sessionState.tempo();
}
double quantum() const
{
return mQuantum;
}
void setQuantum(double quantum)
{
mQuantum = quantum;
}
bool isStartStopSyncEnabled() const
{
return mLink.isStartStopSyncEnabled();
}
void setStartStopSyncEnabled(bool enabled)
{
mLink.enableStartStopSync(enabled);
}
std::chrono::microseconds now() const
{
return mLink.clock().micros();
}
private:
Link& mLink;
double mQuantum;
};
} // namespace ableton
ableton::Link link_(120.);
ableton::Engine engine_(link_);
Metronome::Metronome()
{
}
bool Metronome::init()
{
// set parameters
setEnabled(Settings::application.timer.link_enabled);
setTempo(Settings::application.timer.link_tempo);
setQuantum(Settings::application.timer.link_quantum);
setStartStopSync(Settings::application.timer.link_start_stop_sync);
// no reason for failure?
return true;
}
void Metronome::terminate()
{
// save current tempo
Settings::application.timer.link_tempo = tempo();
// disconnect
link_.enable(false);
}
void Metronome::setEnabled (bool on)
{
link_.enable(on);
Settings::application.timer.link_enabled = link_.isEnabled();
Log::Info("Metronome Ableton Link %s", Settings::application.timer.link_enabled ? "Enabled" : "Disabled");
}
bool Metronome::enabled () const
{
return link_.isEnabled();
}
double Metronome::beats() const
{
return engine_.beatTime();
}
double Metronome::phase() const
{
return engine_.phaseTime();
}
void Metronome::setQuantum(double q)
{
engine_.setQuantum(q);
Settings::application.timer.link_quantum = engine_.quantum();
}
double Metronome::quantum() const
{
return engine_.quantum();
}
void Metronome::setTempo(double t)
{
// set the tempo to t
// OR
// adopt the last tempo value that have been proposed on the network
Settings::application.timer.link_tempo = engine_.setTempo(t);
}
double Metronome::tempo() const
{
return engine_.tempo();
}
void Metronome::setStartStopSync (bool on)
{
engine_.setStartStopSyncEnabled(on);
Settings::application.timer.link_start_stop_sync = engine_.isStartStopSyncEnabled();
Log::Info("Metronome Ableton Link start & stop sync %s", Settings::application.timer.link_start_stop_sync ? "Enabled" : "Disabled");
}
bool Metronome::startStopSync () const
{
return engine_.isStartStopSyncEnabled();
}
void Metronome::restart()
{
engine_.startPlaying();
}
std::chrono::microseconds Metronome::timeToBeat()
{
return engine_.timeNextBeat() - engine_.now();
}
std::chrono::microseconds Metronome::timeToPhase()
{
return engine_.timeNextPhase() - engine_.now();
}
void delay(std::function<void()> f, std::chrono::microseconds us)
{
std::this_thread::sleep_for(us);
f();
}
void Metronome::executeAtBeat( std::function<void()> f )
{
std::thread( delay, f, timeToBeat() ).detach();
}
void Metronome::executeAtPhase( std::function<void()> f )
{
std::thread( delay, f, timeToPhase() ).detach();
}
size_t Metronome::peers() const
{
return link_.numPeers();
}

72
Metronome.h Normal file
View File

@@ -0,0 +1,72 @@
#ifndef METRONOME_H
#define METRONOME_H
#include <chrono>
#include <functional>
class Metronome
{
// Private Constructor
Metronome();
Metronome(Metronome const& copy) = delete;
Metronome& operator=(Metronome const& copy) = delete;
public:
typedef enum {
SYNC_NONE = 0,
SYNC_BEAT,
SYNC_PHASE
} Synchronicity;
static Metronome& manager ()
{
// The only instance
static Metronome _instance;
return _instance;
}
bool init ();
void terminate ();
void setEnabled (bool on);
bool enabled () const;
void setTempo (double t);
double tempo () const;
void setQuantum (double q);
double quantum () const;
void setStartStopSync (bool on);
bool startStopSync () const;
void restart();
// get beat and phase
double beats () const;
double phase () const;
// mechanisms to delay execution to next beat
std::chrono::microseconds timeToBeat();
void executeAtBeat( std::function<void()> f );
// mechanisms to delay execution to next phase
std::chrono::microseconds timeToPhase();
void executeAtPhase( std::function<void()> f );
size_t peers () const;
};
/// Example calls to executeAtBeat
///
/// With a Lamda function calling a member function of an object
/// - without parameter
/// Metronome::manager().executeAtBeat( std::bind([](MediaPlayer *p) { p->rewind(); }, mediaplayer_) );
///
/// - with parameter
/// Metronome::manager().executeAtBeat( std::bind([](MediaPlayer *p, bool o) { p->play(o); }, mediaplayer_, on) );
///
#endif // METRONOME_H

131
Mixer.cpp
View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <thread>
#include <atomic>
@@ -36,15 +55,19 @@
#include "Mixer.h"
#define THREADED_LOADING
static std::vector< std::future<Session *> > sessionLoaders_;
static std::vector< std::future<Session *> > sessionImporters_;
static std::vector< SessionSource * > sessionSourceToImport_;
std::vector< std::future<Session *> > sessionLoaders_;
std::vector< std::future<Session *> > sessionImporters_;
std::vector< SessionSource * > sessionSourceToImport_;
const std::chrono::milliseconds timeout_ = std::chrono::milliseconds(4);
// static multithreaded session saving
static void saveSession(const std::string& filename, Session *session)
static void saveSession(const std::string& filename, Session *session, bool with_version)
{
// capture a snapshot of current version if requested
if (with_version)
Action::manager().snapshot( SystemToolkit::date_time_string());
// lock access while saving
session->lock();
@@ -54,7 +77,7 @@ static void saveSession(const std::string& filename, Session *session)
// set session filename
session->setFilename(filename);
// cosmetics saved ok
Rendering::manager().mainWindow().setTitle(filename);
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(filename));
Settings::application.recentSessions.push(filename);
Log::Notify("Session %s saved.", filename.c_str());
@@ -67,7 +90,8 @@ static void saveSession(const std::string& filename, Session *session)
session->unlock();
}
Mixer::Mixer() : session_(nullptr), back_session_(nullptr), current_view_(nullptr), dt_(0.f), dt__(0.f)
Mixer::Mixer() : session_(nullptr), back_session_(nullptr), sessionSwapRequested_(false),
current_view_(nullptr), dt_(16.f), dt__(16.f)
{
// unsused initial empty session
session_ = new Session;
@@ -116,7 +140,8 @@ void Mixer::update()
// check status of loader: did it finish ?
if (sessionLoaders_.back().wait_for(timeout_) == std::future_status::ready ) {
// get the session loaded by this loader
set( sessionLoaders_.back().get() );
if (sessionLoaders_.back().valid())
set( sessionLoaders_.back().get() );
// done with this session loader
sessionLoaders_.pop_back();
}
@@ -141,9 +166,13 @@ void Mixer::update()
// swap front and back sessions
swap();
++View::need_deep_update_;
// set session filename
Rendering::manager().mainWindow().setTitle(session_->filename());
Settings::application.recentSessions.push(session_->filename());
// inform new session filename
if (session_->filename().empty()) {
Rendering::manager().setMainWindowTitle(Settings::application.windows[0].name);
} else {
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(session_->filename()));
Settings::application.recentSessions.push(session_->filename());
}
}
}
@@ -166,7 +195,7 @@ void Mixer::update()
session_->update(dt_);
// grab frames to recorders & streamers
FrameGrabbing::manager().grabFrame(session_->frame(), dt_);
FrameGrabbing::manager().grabFrame(session_->frame());
// delete sources which failed update (one by one)
Source *failure = session()->failedSource();
@@ -229,11 +258,6 @@ Source * Mixer::createSourceFile(const std::string &path)
ms->setPath(path);
s = ms;
}
// remember in recent media
Settings::application.recentImport.push(path);
Settings::application.recentImport.path = SystemToolkit::path_filename(path);
// propose a new name based on uri
s->setName(SystemToolkit::base_filename(path));
@@ -263,9 +287,6 @@ Source * Mixer::createSourceMultifile(const std::list<std::string> &list_files,
mfs->setSequence(sequence, fps);
s = mfs;
// remember in recent media
Settings::application.recentImport.path = SystemToolkit::path_filename(list_files.front());
// propose a new name
s->setName( SystemToolkit::base_filename( BaseToolkit::common_prefix(list_files) ) );
}
@@ -311,7 +332,7 @@ Source * Mixer::createSourcePattern(uint pattern, glm::ivec2 res)
s->setPattern(pattern, res);
// propose a new name based on pattern name
std::string name = Pattern::pattern_types[pattern];
std::string name = Pattern::get(pattern).label;
name = name.substr(0, name.find(" "));
s->setName(name);
@@ -400,7 +421,13 @@ void Mixer::insertSource(Source *s, View::Mode m)
attach(s);
// new state in history manager
Action::manager().store(s->name() + std::string(" source inserted"));
Action::manager().store(s->name() + std::string(": source inserted"));
// notify creation of source
Log::Notify("Added source '%s' with %s", s->name().c_str(), s->info().c_str());
MediaSource *ms = dynamic_cast<MediaSource *>(s);
if (ms)
Settings::application.recentImport.push(ms->path());
// if requested to show the source in a given view
// (known to work for View::MIXING et TRANSITION: other views untested)
@@ -678,9 +705,10 @@ void Mixer::groupSelection()
info << sessiongroup->name() << " inserted: " << sessiongroup->session()->numSource() << " sources flatten.";
Action::manager().store(info.str());
Log::Notify("Added source '%s' with %s", sessiongroup->name().c_str(), sessiongroup->info().c_str());
// give the hand to the user
Mixer::manager().setCurrentSource(sessiongroup);
Log::Notify(info.str().c_str());
}
void Mixer::renameSource(Source *s, const std::string &newname)
@@ -770,6 +798,17 @@ SourceList Mixer::findSources (float depth_from, float depth_to)
return found;
}
SourceList Mixer::validate (const SourceList &list)
{
SourceList sl;
for( auto sit = list.begin(); sit != list.end(); ++sit) {
SourceList::iterator it = session_->find( *sit );
if (it != session_->end())
sl.push_back(*sit);
}
return sl;
}
void Mixer::setCurrentSource(uint64_t id)
{
setCurrentSource( session_->find(id) );
@@ -792,6 +831,14 @@ void Mixer::setCurrentSource(Source *s)
setCurrentSource( session_->find(s) );
}
Source *Mixer::sourceAtIndex (int index)
{
SourceList::iterator s = session_->at(index);
if (s!=session_->end())
return *s;
return nullptr;
}
void Mixer::setCurrentIndex(int index)
{
setCurrentSource( session_->at(index) );
@@ -862,11 +909,16 @@ void Mixer::unsetCurrentSource()
}
}
int Mixer::indexCurrentSource()
int Mixer::indexCurrentSource() const
{
return current_source_index_;
}
int Mixer::count() const
{
return (int) session_->numSource();
}
Source *Mixer::currentSource()
{
if ( current_source_ != session_->end() )
@@ -886,7 +938,7 @@ void Mixer::setView(View::Mode m)
if ( se != nullptr )
set ( se );
else
Log::Info("Transition interrupted: Session source added.");
Log::Info("Transition interrupted.");
}
switch (m) {
@@ -939,13 +991,13 @@ View *Mixer::view(View::Mode m)
}
}
void Mixer::save()
void Mixer::save(bool with_version)
{
if (!session_->filename().empty())
saveas(session_->filename());
saveas(session_->filename(), with_version);
}
void Mixer::saveas(const std::string& filename)
void Mixer::saveas(const std::string& filename, bool with_version)
{
// optional copy of views config
session_->config(View::MIXING)->copyTransform( mixing_.scene.root() );
@@ -954,7 +1006,7 @@ void Mixer::saveas(const std::string& filename)
session_->config(View::TEXTURE)->copyTransform( appearance_.scene.root() );
// launch a thread to save the session
std::thread (saveSession, filename, session_).detach();
std::thread (saveSession, filename, session_, with_version).detach();
}
void Mixer::load(const std::string& filename)
@@ -1094,7 +1146,7 @@ void Mixer::merge(SessionSource *source)
SourceList::iterator it = to_be_moved.begin();
for (; it != to_be_moved.end(); ++it) {
float scale_depth = (MAX_DEPTH-(*it)->depth()) / (MAX_DEPTH-next_depth);
(*it)->setDepth( (*it)->depth() + scale_depth );
(*it)->call( new SetDepth( (*it)->depth() + scale_depth ) );
}
}
}
@@ -1106,10 +1158,10 @@ void Mixer::merge(SessionSource *source)
renameSource(s);
// scale alpha
s->setAlpha( s->alpha() * source->alpha() );
s->call( new SetAlpha(s->alpha() * source->alpha()));
// set depth (proportional to depth of s, adjusted by needed space)
s->setDepth( target_depth + ( (s->depth()-start_depth)/ need_depth) );
s->call( new SetDepth( target_depth + ( (s->depth()-start_depth)/ need_depth) ) );
// set location
// a. transform of node to import
@@ -1154,7 +1206,7 @@ void Mixer::merge(SessionSource *source)
void Mixer::swap()
{
if (!back_session_)
if (!back_session_ || !session_)
return;
if (session_) {
@@ -1188,7 +1240,7 @@ void Mixer::swap()
session_->setResolution( session_->config(View::RENDERING)->scale_ );
// transfer fading
session_->setFading( MAX(back_session_->fading(), session_->fading()), true );
session_->setFadingTarget( MAX(back_session_->fadingTarget(), session_->fadingTarget()));
// no current source
current_source_ = session_->end();
@@ -1237,6 +1289,7 @@ void Mixer::clear()
// need to deeply update view to apply eventual changes
++View::need_deep_update_;
Settings::application.recentSessions.front_is_valid = false;
Log::Info("New session ready.");
}
@@ -1256,6 +1309,17 @@ void Mixer::set(Session *s)
sessionSwapRequested_ = true;
}
void Mixer::setResolution(glm::vec3 res)
{
if (session_) {
session_->setResolution(res);
++View::need_deep_update_;
std::ostringstream info;
info << "Session resolution changed to " << res.x << "x" << res.y;
Log::Info("%s", info.str().c_str());
}
}
void Mixer::paste(const std::string& clipboard)
{
tinyxml2::XMLDocument xmlDoc;
@@ -1283,7 +1347,6 @@ void Mixer::restore(tinyxml2::XMLElement *sessionNode)
//
// source lists
//
// sessionsources contains list of ids of all sources currently in the session (before loading)
SourceIdList session_sources = session_->getIdList();
// for( auto it = sessionsources.begin(); it != sessionsources.end(); it++)

14
Mixer.h
View File

@@ -40,7 +40,7 @@ public:
// update session and all views
void update();
inline float dt() const { return dt_;}
inline float dt() const { return dt_;} // in miliseconds
inline int fps() const { return int(roundf(1000.f/dt__));}
// draw session and current view
@@ -77,15 +77,18 @@ public:
void setCurrentPrevious ();
void unsetCurrentSource ();
Source *sourceAtIndex (int index);
void setCurrentIndex (int index);
void moveIndex (int current_index, int target_index);
int indexCurrentSource ();
int indexCurrentSource () const;
int count() const;
// browsing into sources
Source * findSource (Node *node);
Source * findSource (std::string name);
Source * findSource (uint64_t id);
SourceList findSources (float depth_from, float depth_to);
SourceList validate(const SourceList &list);
// management of view
View *view (View::Mode m = View::INVALID);
@@ -98,14 +101,15 @@ public:
// manipulate, load and save sessions
inline Session *session () const { return session_; }
void clear ();
void save ();
void saveas (const std::string& filename);
void save (bool with_version = false);
void saveas (const std::string& filename, bool with_version = false);
void load (const std::string& filename);
void import (const std::string& filename);
void import (SessionSource *source);
void merge (Session *session);
void merge (SessionSource *source);
void set (Session *session);
void setResolution(glm::vec3 res);
// operations depending on transition mode
void close (bool smooth = false);
@@ -113,6 +117,8 @@ public:
// create sources if clipboard contains well-formed xml text
void paste (const std::string& clipboard);
// version and undo management
void restore(tinyxml2::XMLElement *sessionNode);
protected:

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <glm/gtx/vector_angle.hpp>

View File

@@ -17,7 +17,7 @@ public:
// non assignable class
MixingGroup(MixingGroup const&) = delete;
MixingGroup& operator=(MixingGroup const&) = delete;
~MixingGroup ();
virtual ~MixingGroup ();
// Get unique id
inline uint64_t id () const { return id_; }

View File

@@ -1,17 +1,34 @@
// Opengl
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <string>
#include <sstream>
#include <iomanip>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtx/vector_angle.hpp>
#include "imgui.h"
#include "ImGuiToolkit.h"
#include <string>
#include <sstream>
#include <iomanip>
#include "Mixer.h"
#include "defines.h"
#include "Source.h"
@@ -31,7 +48,7 @@ uint textureMixingQuadratic();
MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_MIN_THRESHOLD)
{
scene.root()->scale_ = glm::vec3(MIXING_DEFAULT_SCALE, MIXING_DEFAULT_SCALE, 1.0f);
scene.root()->translation_ = glm::vec3(0.0f, 0.0f, 0.0f);
@@ -45,10 +62,30 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
restoreSettings();
// Mixing scene background
Mesh *tmp = new Mesh("mesh/disk.ply");
tmp->scale_ = glm::vec3(limbo_scale_, limbo_scale_, 1.f);
tmp->shader()->color = glm::vec4( COLOR_LIMBO_CIRCLE, 0.6f );
scene.bg()->attach(tmp);
limbo_ = new Mesh("mesh/disk.ply");
limbo_->scale_ = glm::vec3(limbo_scale_, limbo_scale_, 1.f);
limbo_->shader()->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
scene.bg()->attach(limbo_);
// interactive limbo scaling slider
limbo_slider_root_ = new Group;
limbo_slider_root_->translation_ = glm::vec3(0.0f, limbo_scale_, 0.f);
scene.bg()->attach(limbo_slider_root_);
limbo_slider_ = new Disk();
limbo_slider_->translation_ = glm::vec3(0.f, -0.01f, 0.f);
limbo_slider_->scale_ = glm::vec3(0.1f, 0.1f, 1.f);
limbo_slider_->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
limbo_slider_root_->attach(limbo_slider_);
limbo_up_ = new Mesh("mesh/triangle_point.ply");
limbo_up_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
limbo_slider_root_->attach(limbo_up_);
limbo_down_ = new Mesh("mesh/triangle_point.ply");
limbo_down_->translation_ = glm::vec3(0.f, -0.02f, 0.f);
limbo_down_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
limbo_down_->rotation_ = glm::vec3(0.f, 0.f, M_PI);
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
limbo_slider_root_->attach(limbo_down_);
mixingCircle_ = new Mesh("mesh/disk.ply");
mixingCircle_->shader()->color = glm::vec4( 1.f, 1.f, 1.f, 1.f );
@@ -60,8 +97,8 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
// Mixing scene foreground
// button frame
tmp = new Mesh("mesh/disk.ply");
// button frame (non interactive; no picking detected on Mesh)
Mesh *tmp = new Mesh("mesh/disk.ply");
tmp->scale_ = glm::vec3(0.033f, 0.033f, 1.f);
tmp->translation_ = glm::vec3(0.f, 1.f, 0.f);
tmp->shader()->color = glm::vec4( COLOR_CIRCLE, 0.9f );
@@ -101,10 +138,10 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
slider_root_->attach(tmp);
stashCircle_ = new Disk();
stashCircle_->scale_ = glm::vec3(0.5f, 0.5f, 1.f);
stashCircle_->translation_ = glm::vec3(2.f, -1.0f, 0.f);
stashCircle_->color = glm::vec4( COLOR_STASH_CIRCLE, 0.6f );
// stashCircle_ = new Disk();
// stashCircle_->scale_ = glm::vec3(0.5f, 0.5f, 1.f);
// stashCircle_->translation_ = glm::vec3(2.f, -1.0f, 0.f);
// stashCircle_->color = glm::vec4( COLOR_STASH_CIRCLE, 0.6f );
// scene.bg()->attach(stashCircle_);
}
@@ -175,7 +212,21 @@ void MixingView::draw()
}
Action::manager().store(std::string("Selection: Mixing Center"));
}
if (ImGui::Selectable( ICON_FA_HAYKAL " Distribute" )){
if (ImGui::Selectable( ICON_FA_HAYKAL " Dispatch" )){
glm::vec2 center = glm::vec2(0.f, 0.f);
// distribute with equal angle
float angle = 0.f;
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
glm::vec2 P = center + glm::rotate(glm::vec2(0.f, 1.2), angle);
(*it)->group(View::MIXING)->translation_.x = P.x;
(*it)->group(View::MIXING)->translation_.y = P.y;
(*it)->touch();
angle -= glm::two_pi<float>() / float(Mixer::selection().size());
}
Action::manager().store(std::string("Selection: Mixing Dispatch"));
}
if (ImGui::Selectable( ICON_FA_FAN " Distribute" )){
SourceList list;
glm::vec2 center = glm::vec2(0.f, 0.f);
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
@@ -202,19 +253,40 @@ void MixingView::draw()
(*it)->touch();
angle -= glm::two_pi<float>() / float(list.size());
}
Action::manager().store(std::string("Selection: Mixing Distribute"));
Action::manager().store(std::string("Selection: Mixing Distribute in circle"));
}
if (ImGui::Selectable( ICON_FA_ELLIPSIS_V " Align & Distribute" )){
SourceList list;
glm::vec2 center = glm::vec2(0.f, 0.f);
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
list.push_back(*it);
// compute barycenter (1)
center += glm::vec2((*it)->group(View::MIXING)->translation_);
}
// compute barycenter (2)
center /= list.size();
// distribute with equal angle
float i = 0.f;
float sign = -1.f;
for (SourceList::iterator it = list.begin(); it != list.end(); ++it, sign*=-1.f) {
(*it)->group(View::MIXING)->translation_.x = center.x;
(*it)->group(View::MIXING)->translation_.y = center.y + sign * i;
if (sign<0) i+=0.32f;
(*it)->touch();
}
Action::manager().store(std::string("Selection: Mixing Align & Distribute"));
}
if (ImGui::Selectable( ICON_FA_CLOUD_SUN " Expand & hide" )){
SourceList::iterator it = Mixer::selection().begin();
for (; it != Mixer::selection().end(); ++it) {
(*it)->setAlpha(0.f);
(*it)->call( new SetAlpha(0.f) );
}
Action::manager().store(std::string("Selection: Mixing Expand & hide"));
}
if (ImGui::Selectable( ICON_FA_SUN " Compress & show" )){
SourceList::iterator it = Mixer::selection().begin();
for (; it != Mixer::selection().end(); ++it) {
(*it)->setAlpha(0.99f);
(*it)->call( new SetAlpha(0.999f) );
}
Action::manager().store(std::string("Selection: Mixing Compress & show"));
}
@@ -233,7 +305,8 @@ void MixingView::resize ( int scale )
scene.root()->scale_.y = z;
// Clamp translation to acceptable area
glm::vec3 border(scene.root()->scale_.x * 1.f, scene.root()->scale_.y * 1.f, 0.f);
glm::vec2 res = resolution();
glm::vec3 border(2.3f * res.x/res.y, 2.3f, 0.f);
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border);
}
@@ -276,43 +349,34 @@ void MixingView::update(float dt)
if (View::need_deep_update_ > 0) {
//
// Set slider to match the actual fading of the session
// Set limbo scale according to session
//
float f = Mixer::manager().session()->empty() ? 0.f : Mixer::manager().session()->fading();
// reverse calculate angle from fading & move slider
slider_root_->rotation_.z = SIGN(slider_root_->rotation_.z) * asin(f) * 2.f;
// visual feedback on mixing circle
f = 1.f - f;
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
const float p = CLAMP(Mixer::manager().session()->activationThreshold(), MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
limbo_slider_root_->translation_.y = p;
limbo_->scale_ = glm::vec3(p, p, 1.f);
//
// prevent invalid scaling
//
float s = CLAMP(scene.root()->scale_.x, MIXING_MIN_SCALE, MIXING_MAX_SCALE);
scene.root()->scale_.x = s;
scene.root()->scale_.y = s;
}
// the current view is the mixing view
if (Mixer::manager().view() == this )
{
if (Mixer::manager().view() == this ){
//
// Set session fading to match the slider angle (during animation)
// Set slider to match the actual fading of the session
//
float f = Mixer::manager().session()->fading();
// calculate fading from angle
float f = sin( ABS(slider_root_->rotation_.z) * 0.5f);
// reverse calculate angle from fading & move slider
slider_root_->rotation_.z = SIGN(-slider_root_->rotation_.z) * asin(f) * -2.f;
// apply fading
if ( ABS_DIFF( f, Mixer::manager().session()->fading()) > EPSILON )
{
// apply fading to session
Mixer::manager().session()->setFading(f);
// visual feedback on mixing circle
f = 1.f - f;
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
}
// visual feedback on mixing circle
f = 1.f - f;
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
// update the selection overlay
updateSelectionOverlay();
@@ -329,18 +393,14 @@ std::pair<Node *, glm::vec2> MixingView::pick(glm::vec2 P)
// deal with internal interactive objects
if ( pick.first == button_white_ || pick.first == button_black_ ) {
RotateToCallback *anim = nullptr;
if (pick.first == button_white_)
anim = new RotateToCallback(0.f, 500.f);
else
anim = new RotateToCallback(SIGN(slider_root_->rotation_.z) * M_PI, 500.f);
// animate clic
pick.first->update_callbacks_.push_back(new BounceScaleCallback(0.3f));
// reset & start animation
slider_root_->update_callbacks_.clear();
slider_root_->update_callbacks_.push_back(anim);
// animated fading in session
if (pick.first == button_white_)
Mixer::manager().session()->setFadingTarget(0.f, 500.f);
else
Mixer::manager().session()->setFadingTarget(1.f, 500.f);
}
else if ( overlay_selection_icon_ != nullptr && pick.first == overlay_selection_icon_ ) {
@@ -425,10 +485,30 @@ View::Cursor MixingView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pai
// animate slider (rotation angle on its parent)
slider_root_->rotation_.z = angle;
// calculate fading from angle
float f = sin( ABS(angle) * 0.5f);
Mixer::manager().session()->setFadingTarget(f);
// cursor feedback
slider_->color = glm::vec4( COLOR_CIRCLE_OVER, 0.9f );
std::ostringstream info;
info << "Global opacity " << 100 - int(Mixer::manager().session()->fading() * 100.0) << " %";
info << "Output " << 100 - int(f * 100.0) << " %";
return Cursor(Cursor_Hand, info.str() );
}
else if (pick.first == limbo_slider_) {
// move slider scaling limbo area
const float p = CLAMP(gl_Position_to.y, MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
limbo_slider_root_->translation_.y = p;
limbo_->scale_ = glm::vec3(p, p, 1.f);
Mixer::manager().session()->setActivationThreshold(p);
// color change of arrow indicators
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p < MIXING_MAX_THRESHOLD ? 0.15f : 0.01f );
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p > MIXING_MIN_THRESHOLD ? 0.15f : 0.01f );
std::ostringstream info;
info << ICON_FA_SNOWFLAKE " Deactivation limit";
return Cursor(Cursor_Hand, info.str() );
}
@@ -514,14 +594,24 @@ View::Cursor MixingView::over (glm::vec2 pos)
else
slider_->color = glm::vec4( COLOR_CIRCLE, 0.9f );
if ( pick.first == limbo_slider_ ) {
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y < MIXING_MAX_THRESHOLD ? 0.1f : 0.01f );
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y > MIXING_MIN_THRESHOLD ? 0.1f : 0.01f );
ret.type = Cursor_Hand;
}
else {
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
}
return ret;
}
void MixingView::arrow (glm::vec2 movement)
{
static int accumulator = 0;
accumulator++;
static float accumulator = 0.f;
accumulator += dt_;
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
glm::vec3 gl_Position_to = Rendering::manager().unProject(movement, scene.root()->transform_);
@@ -544,18 +634,19 @@ void MixingView::arrow (glm::vec2 movement)
// + ALT : discrete displacement
if (UserInterface::manager().altModifier()) {
if (accumulator > 10) {
dest_translation += glm::sign(gl_delta) * 0.11f;
if (accumulator > 100.f) {
dest_translation += glm::sign(gl_delta) * 0.1f;
dest_translation.x = ROUND(dest_translation.x, 10.f);
dest_translation.y = ROUND(dest_translation.y, 10.f);
accumulator = 0;
accumulator = 0.f;
}
else
break;
}
else {
// normal case: dest += delta
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
accumulator = 0.f;
}
// store action in history
@@ -625,7 +716,7 @@ void MixingView::updateSelectionOverlay()
}
#define CIRCLE_PIXELS 64
#define CIRCLE_PIXEL_RADIUS 1024.0
#define CIRCLE_PIXEL_RADIUS 1024.f
//#define CIRCLE_PIXELS 256
//#define CIRCLE_PIXEL_RADIUS 16384.0
//#define CIRCLE_PIXELS 1024

View File

@@ -26,7 +26,6 @@ public:
void arrow (glm::vec2) override;
void setAlpha (Source *s);
inline float limboScale() { return limbo_scale_; }
private:
void updateSelectionOverlay() override;
@@ -37,9 +36,13 @@ private:
Disk *slider_;
Disk *button_white_;
Disk *button_black_;
Disk *stashCircle_;
// Disk *stashCircle_;
Mesh *mixingCircle_;
Mesh *circle_;
Mesh *limbo_;
Group *limbo_slider_root_;
Mesh *limbo_up_, *limbo_down_;
Disk *limbo_slider_;
};

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <sstream>
#include <algorithm>
@@ -35,7 +54,8 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
// sanity check: the location pattern looks like a filename and seems consecutive numbered
if ( SystemToolkit::extension_filename(location).empty() ||
SystemToolkit::path_filename(location) != SystemToolkit::path_filename(list_files.front()) ||
list_files.size() != max - min + 1 ) {
list_files.size() != (size_t) (max - min) + 1 ) {
Log::Info("MultiFileSequence '%s' invalid.", location.c_str());
location.clear();
}
@@ -47,6 +67,8 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
width = media.width;
height = media.height;
}
else
Log::Info("MultiFileSequence '%s' does not list images.", location.c_str());
}
}
@@ -115,6 +137,22 @@ void MultiFile::close ()
Stream::close();
}
void MultiFile::setIndex(int val)
{
if (src_) {
g_object_set (src_, "index", val, NULL);
}
}
int MultiFile::index()
{
int val = 0;
if (src_) {
g_object_get (src_, "index", &val, NULL);
}
return val;
}
void MultiFile::setProperties (int begin, int end, int loop)
{
if (src_) {
@@ -187,6 +225,27 @@ void MultiFileSource::setRange (int begin, int end)
multifile()->setProperties (begin_, end_, loop_);
}
void MultiFileSource::replay ()
{
if (multifile()) {
multifile()->setIndex (begin_);
stream_->rewind();
}
}
guint64 MultiFileSource::playtime () const
{
guint64 time = 0;
if (multifile())
time += multifile()->index();
time *= GST_SECOND;
time /= framerate_;
return time;
}
void MultiFileSource::accept (Visitor& v)
{
Source::accept(v);
@@ -200,3 +259,14 @@ MultiFile *MultiFileSource::multifile () const
}
glm::ivec2 MultiFileSource::icon () const
{
return glm::ivec2(ICON_SOURCE_SEQUENCE);
}
std::string MultiFileSource::info() const
{
return std::string("sequence '") + sequence_.location + "'";
}

View File

@@ -31,6 +31,10 @@ public:
// dynamic change of gstreamer multifile source properties
void setProperties(int begin, int end, int loop);
// image index
int index();
void setIndex(int val);
protected:
GstElement *src_ ;
};
@@ -42,10 +46,13 @@ public:
// Source interface
void accept (Visitor& v) override;
void replay () override;
guint64 playtime () const override;
// StreamSource interface
Stream *stream () const override { return stream_; }
glm::ivec2 icon () const override { return glm::ivec2(3, 9); }
glm::ivec2 icon() const override;
std::string info() const override;
// specific interface
void setFiles (const std::list<std::string> &list_files, uint framerate);

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <sstream>
#include <thread>
@@ -5,17 +24,17 @@
#include <future>
#include <glm/gtc/matrix_transform.hpp>
#include <gst/pbutils/pbutils.h>
#include <gst/gst.h>
#include "osc/OscOutboundPacketStream.h"
#include "SystemToolkit.h"
#include "defines.h"
#include "Stream.h"
#include "Decorations.h"
#include "Visitor.h"
#include "Log.h"
#include "Connection.h"
#include "NetworkSource.h"
@@ -25,7 +44,7 @@
// this is called when receiving an answer for streaming request
void StreamerResponseListener::ProcessMessage( const osc::ReceivedMessage& m,
void NetworkStream::ResponseListener::ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint )
{
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
@@ -242,22 +261,34 @@ void NetworkStream::update()
// general case : create pipeline and open
if (!failed_) {
// build the pipeline depending on stream info
std::ostringstream pipeline;
// get generic pipeline string
std::string pipelinestring = NetworkToolkit::protocol_receive_pipeline[config_.protocol];
// find placeholder for PORT
int xxxx = pipelinestring.find("XXXX");
// keep beginning of pipeline
pipeline << pipelinestring.substr(0, xxxx);
// Replace 'XXXX' by info on port config
pipeline << parameter;
// keep ending of pipeline
pipeline << pipelinestring.substr(xxxx + 4);
// add a videoconverter
pipeline << " ! videoconvert";
// find placeholder for PORT or SHH socket
size_t xxxx = pipelinestring.find("XXXX");
if (xxxx != std::string::npos)
// Replace 'XXXX' by info on port config
pipelinestring.replace(xxxx, 4, parameter);
// find placeholder for WIDTH
size_t wwww = pipelinestring.find("WWWW");
if (wwww != std::string::npos)
// Replace 'WWWW' by width
pipelinestring.replace(wwww, 4, std::to_string(config_.width) );
// find placeholder for HEIGHT
size_t hhhh = pipelinestring.find("HHHH");
if (hhhh != std::string::npos)
// Replace 'WWWW' by height
pipelinestring.replace(hhhh, 4, std::to_string(config_.height) );
// add a videoconverter
pipelinestring.append(" ! videoconvert ");
#ifdef NETWORK_DEBUG
Log::Info("Openning pipeline %s", pipelinestring.c_str());
#endif
// open the pipeline with generic stream class
Stream::open(pipeline.str(), config_.width, config_.height);
Stream::open(pipelinestring, config_.width, config_.height);
}
}
else {
@@ -292,7 +323,6 @@ NetworkStream *NetworkSource::networkStream() const
void NetworkSource::setConnection(const std::string &nameconnection)
{
connection_name_ = nameconnection;
Log::Notify("Network Source connecting to '%s'", connection_name_.c_str());
// open network stream
networkStream()->connect( connection_name_ );
@@ -315,5 +345,13 @@ void NetworkSource::accept(Visitor& v)
v.visit(*this);
}
glm::ivec2 NetworkSource::icon() const
{
return glm::ivec2(ICON_SOURCE_NETWORK);
}
std::string NetworkSource::info() const
{
return std::string("connected to '") + connection_name_ + "'";
}

View File

@@ -1,33 +1,13 @@
#ifndef NETWORKSOURCE_H
#define NETWORKSOURCE_H
#include "osc/OscReceivedElements.h"
#include "osc/OscPacketListener.h"
#include "osc/OscOutboundPacketStream.h"
#include "ip/UdpSocket.h"
#include "NetworkToolkit.h"
#include "Connection.h"
#include "StreamSource.h"
class NetworkStream;
class StreamerResponseListener : public osc::OscPacketListener
{
protected:
class NetworkStream *parent_;
virtual void ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint );
public:
inline void setParent(NetworkStream *s) { parent_ = s; }
StreamerResponseListener() : parent_(nullptr) {}
};
class NetworkStream : public Stream
{
friend class StreamerResponseListener;
public:
NetworkStream();
@@ -43,10 +23,22 @@ public:
std::string clientAddress() const;
std::string serverAddress() const;
protected:
class ResponseListener : public osc::OscPacketListener
{
protected:
class NetworkStream *parent_;
virtual void ProcessMessage( const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint );
public:
inline void setParent(NetworkStream *s) { parent_ = s; }
ResponseListener() : parent_(nullptr) {}
};
private:
// connection information
ConnectionInfo streamer_;
StreamerResponseListener listener_;
ResponseListener listener_;
UdpListeningReceiveSocket *receiver_;
std::atomic<bool> received_config_;
std::atomic<bool> connected_;
@@ -74,7 +66,8 @@ public:
void setConnection(const std::string &nameconnection);
std::string connection() const;
glm::ivec2 icon() const override { return glm::ivec2(18, 11); }
glm::ivec2 icon() const override;
std::string info() const override;
};

View File

@@ -1,3 +1,21 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
@@ -60,32 +78,48 @@
* RCV
* gst-launch-1.0 shmsrc is-live=true socket-path=/tmp/blah ! video/x-raw, format=RGB, framerate=30/1, width=320, height=240 ! videoconvert ! autovideosink
*
* RTP UDP JPEG
*
* SND
* gst-launch-1.0 videotestsrc is-live=true ! video/x-raw, format=RGB, framerate=30/1 ! videoconvert ! video/x-raw, format=I420 ! jpegenc quality=95 ! rtpjpegpay ! udpsink port=5000 host=127.0.0
* RCV
* gst-launch-1.0 udpsrc buffer-size=200000 port=5000 ! application/x-rtp,encoding-name=JPEG ! rtpjpegdepay ! queue max-size-buffers=10 ! jpegdec ! videoconvert ! video/x-raw, format=RGB ! autovideosink
*
* */
const char* NetworkToolkit::protocol_name[NetworkToolkit::DEFAULT] = {
"Shared Memory",
"RTP JPEG Stream",
"RTP H264 Stream",
"RTP JPEG Broadcast",
"RTP H264 Broadcast"
"RAW Images",
"JPEG Stream",
"H264 Stream",
"JPEG Broadcast",
"H264 Broadcast",
"RGB Shared Memory"
};
const std::vector<std::string> NetworkToolkit::protocol_send_pipeline {
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10 ! shmsink buffer-time=100000 wait-for-connection=true name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! jpegenc ! rtpjpegpay ! udpsink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! udpsink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! jpegenc ! rtpjpegpay ! rtpstreampay ! tcpserversink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! rtpstreampay ! tcpserversink name=sink"
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=3 ! rtpvrawpay ! application/x-rtp,sampling=RGB ! udpsink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! jpegenc quality=85 ! rtpjpegpay ! udpsink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! rtph264pay aggregate-mode=1 ! udpsink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! jpegenc idct-method=float ! rtpjpegpay ! rtpstreampay ! tcpserversink name=sink",
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! rtpstreampay ! tcpserversink name=sink",
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10 ! shmsink buffer-time=100000 wait-for-connection=true name=sink"
};
const std::vector<std::string> NetworkToolkit::protocol_receive_pipeline {
"udpsrc buffer-size=200000 port=XXXX caps=\"application/x-rtp,media=(string)video,encoding-name=(string)RAW,sampling=(string)RGB,width=(string)WWWW,height=(string)HHHH\" ! rtpvrawdepay ! queue max-size-buffers=10",
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=JPEG ! rtpjpegdepay ! queue max-size-buffers=10 ! jpegdec",
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=H264 ! rtph264depay ! queue max-size-buffers=10 ! avdec_h264",
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=JPEG ! rtpstreamdepay ! rtpjpegdepay ! jpegdec",
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=H264 ! rtpstreamdepay ! rtph264depay ! avdec_h264",
"shmsrc socket-path=XXXX ! video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10",
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=JPEG,payload=26,clock-rate=90000 ! queue max-size-buffers=10 ! rtpjpegdepay ! jpegdec",
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=H264,payload=96,clock-rate=90000 ! queue ! rtph264depay ! avdec_h264",
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=JPEG,payload=26,clock-rate=90000 ! rtpstreamdepay ! rtpjpegdepay ! jpegdec",
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=H264,payload=96,clock-rate=90000 ! rtpstreamdepay ! rtph264depay ! avdec_h264"
};
const std::vector< std::pair<std::string, std::string> > NetworkToolkit::protocol_h264_send_pipeline {
// {"vtenc_h264_hw", "video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! rtph264pay aggregate-mode=1 ! udpsink name=sink"},
{"nvh264enc", "video/x-raw, format=RGBA, framerate=30/1 ! queue max-size-buffers=10 ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! rtph264pay aggregate-mode=1 ! udpsink name=sink"},
{"vaapih264enc", "video/x-raw, format=NV12, framerate=30/1 ! queue max-size-buffers=10 ! vaapih264enc rate-control=cqp init-qp=26 ! video/x-h264, profile=(string)main ! rtph264pay aggregate-mode=1 ! udpsink name=sink"}
};
bool initialized_ = false;
@@ -99,8 +133,7 @@ void add_interface(int fd, const char *name) {
strncpy(ifreq.ifr_name, name, IFNAMSIZ);
if(ioctl(fd, SIOCGIFADDR, &ifreq)==0) {
char host[128];
int family;
switch(family=ifreq.ifr_addr.sa_family) {
switch(ifreq.ifr_addr.sa_family) {
case AF_INET:
case AF_INET6:
getnameinfo(&ifreq.ifr_addr, sizeof ifreq.ifr_addr, host, sizeof host, 0, 0, NI_NUMERICHOST);

View File

@@ -4,6 +4,11 @@
#include <string>
#include <vector>
#include "osc/OscReceivedElements.h"
#include "osc/OscPacketListener.h"
#include "ip/UdpSocket.h"
#define OSC_SEPARATOR '/'
#define OSC_PREFIX "/vimix"
#define OSC_PING "/ping"
#define OSC_PONG "/pong"
@@ -12,22 +17,18 @@
#define OSC_STREAM_REJECT "/reject"
#define OSC_STREAM_DISCONNECT "/disconnect"
#define MAX_HANDSHAKE 20
#define HANDSHAKE_PORT 71310
#define STREAM_REQUEST_PORT 71510
#define OSC_DIALOG_PORT 71010
#define IP_MTU_SIZE 1536
namespace NetworkToolkit
{
typedef enum {
SHM_RAW = 0,
UDP_RAW = 0,
UDP_JPEG,
UDP_H264,
TCP_JPEG,
TCP_H264,
SHM_RAW,
DEFAULT
} Protocol;
@@ -66,6 +67,7 @@ struct StreamConfig {
extern const char* protocol_name[DEFAULT];
extern const std::vector<std::string> protocol_send_pipeline;
extern const std::vector< std::pair<std::string, std::string> > protocol_h264_send_pipeline;
extern const std::vector<std::string> protocol_receive_pipeline;
std::string hostname();

View File

@@ -1,8 +1,25 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
#include "PatternSource.h"
#include "defines.h"
#include "ImageShader.h"
#include "Resource.h"
@@ -10,94 +27,70 @@
#include "Stream.h"
#include "Visitor.h"
#include "Log.h"
#include "GstToolkit.h"
#define MAX_PATTERN 24
#include "PatternSource.h"
// smpte (0) SMPTE 100%% color bars
// snow (1) Random (television snow)
// black (2) 100%% Black
// white (3) 100%% White
// red (4) Red
// green (5) Green
// blue (6) Blue
// checkers-1 (7) Checkers 1px
// checkers-2 (8) Checkers 2px
// checkers-4 (9) Checkers 4px
// checkers-8 (10) Checkers 8px
// circular (11) Circular
// blink (12) Blink
// smpte75 (13) SMPTE 75%% color bars
// zone-plate (14) Zone plate
// gamut (15) Gamut checkers
// chroma-zone-plate (16) Chroma zone plate
// solid-color (17) Solid color
// ball (18) Moving ball
// smpte100 (19) SMPTE 100%% color bars
// bar (20) Bar
// pinwheel (21) Pinwheel
// spokes (22) Spokes
// gradient (23) Gradient
// colors (24) Colors
const char* pattern_internal_[MAX_PATTERN] = { "videotestsrc pattern=black",
"videotestsrc pattern=white",
"videotestsrc pattern=gradient",
"videotestsrc pattern=checkers-1 ! video/x-raw,format=GRAY8 ! videoconvert",
"videotestsrc pattern=checkers-8 ! video/x-raw,format=GRAY8 ! videoconvert",
"videotestsrc pattern=circular",
"frei0r-src-lissajous0r ratiox=0.001 ratioy=0.999 ! videoconvert",
"videotestsrc pattern=pinwheel",
"videotestsrc pattern=spokes",
"videotestsrc pattern=red",
"videotestsrc pattern=green",
"videotestsrc pattern=blue",
"videotestsrc pattern=smpte100",
"videotestsrc pattern=colors",
"videotestsrc pattern=smpte",
"videotestsrc pattern=snow",
"videotestsrc pattern=blink",
"videotestsrc pattern=zone-plate",
"videotestsrc pattern=chroma-zone-plate",
"videotestsrc pattern=bar horizontal-speed=5",
"videotestsrc pattern=ball",
"frei0r-src-ising0r",
"videotestsrc pattern=black ! timeoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ",
"videotestsrc pattern=black ! clockoverlay halignment=center valignment=center font-desc=\"Sans, 72\" "
};
//
// Fill the list of patterns videotestsrc
//
// Label (for display), feature (for test), pipeline (for gstreamer), animated (true/false), available (false by default)
std::vector<pattern_descriptor> Pattern::patterns_ = {
{ "Black", "videotestsrc", "videotestsrc pattern=black", false, false },
{ "White", "videotestsrc", "videotestsrc pattern=white", false, false },
{ "Gradient", "videotestsrc", "videotestsrc pattern=gradient", false, false },
{ "Checkers 1x1 px", "videotestsrc", "videotestsrc pattern=checkers-1 ! videobalance saturation=0 contrast=1.5", false, false },
{ "Checkers 8x8 px", "videotestsrc", "videotestsrc pattern=checkers-8 ! videobalance saturation=0 contrast=1.5", false, false },
{ "Circles", "videotestsrc", "videotestsrc pattern=circular", false, false },
{ "Lissajous", "frei0r-src-lissajous0r", "frei0r-src-lissajous0r ratiox=0.001 ratioy=0.999 ! videoconvert", false, false },
{ "Pinwheel", "videotestsrc", "videotestsrc pattern=pinwheel", false, false },
{ "Spokes", "videotestsrc", "videotestsrc pattern=spokes", false, false },
{ "Red", "videotestsrc", "videotestsrc pattern=red", false, false },
{ "Green", "videotestsrc", "videotestsrc pattern=green", false, false },
{ "Blue", "videotestsrc", "videotestsrc pattern=blue", false, false },
{ "Color bars", "videotestsrc", "videotestsrc pattern=smpte100", false, false },
{ "RGB grid", "videotestsrc", "videotestsrc pattern=colors", false, false },
{ "SMPTE test pattern", "videotestsrc", "videotestsrc pattern=smpte", true, false },
{ "Television snow", "videotestsrc", "videotestsrc pattern=snow", true, false },
{ "Blink", "videotestsrc", "videotestsrc pattern=blink", true, false },
{ "Fresnel zone plate", "videotestsrc", "videotestsrc pattern=zone-plate kx2=XXX ky2=YYY kt=4", true, false },
{ "Chroma zone plate", "videotestsrc", "videotestsrc pattern=chroma-zone-plate kx2=XXX ky2=YYY kt=4", true, false },
{ "Bar moving", "videotestsrc", "videotestsrc pattern=bar horizontal-speed=5", true, false },
{ "Ball bouncing", "videotestsrc", "videotestsrc pattern=ball", true, false },
{ "Blob", "frei0r-src-ising0r", "frei0r-src-ising0r", true, false },
{ "Timer", "timeoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! timeoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ", true, false },
{ "Clock", "clockoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! clockoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ", true, false },
{ "Resolution", "textoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! textoverlay text=\"XXXX x YYYY px\" halignment=center valignment=center font-desc=\"Sans, 52\" ", false, false },
{ "Frame", "videobox", "videotestsrc pattern=solid-color foreground-color=0 ! videobox fill=white top=-10 bottom=-10 left=-10 right=-10", false, false },
{ "Cross", "textoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! textoverlay text=\"+\" halignment=center valignment=center font-desc=\"Sans, 22\" ", false, false },
{ "Grid", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.35", false, false },
{ "Point Grid", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.4", false, false },
{ "Ruler", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.9", false, false },
{ "RGB noise", "frei0r-filter-rgbnoise", "videotestsrc pattern=black ! frei0r-filter-rgbnoise noise=0.6", true, false },
{ "Philips test pattern", "frei0r-src-test-pat-b", "frei0r-src-test-pat-b type=0.7 ", false, false }
};
std::vector<std::string> Pattern::pattern_types = { "Black",
"White",
"Gradient",
"Checkers 1x1 px",
"Checkers 8x8 px",
"Circles",
"Lissajous",
"Pinwheel",
"Spokes",
"Red",
"Green",
"Blue",
"Color bars",
"RGB grid",
"SMPTE test pattern",
"Television snow",
"Blink",
"Fresnel zone plate",
"Chroma zone plate",
"Bar moving",
"Ball bouncing"
#if GST_VERSION_MINOR > 17
,
"Blob",
"Timer",
"Clock"
#endif
};
Pattern::Pattern() : Stream(), type_(MAX_PATTERN) // invalid pattern
Pattern::Pattern() : Stream(), type_(UINT_MAX) // invalid pattern
{
}
pattern_descriptor Pattern::get(uint type)
{
// check availability of feature to use this pattern
if (!patterns_[type].available)
patterns_[type].available = GstToolkit::has_feature(patterns_[type].feature);
// return struct
return patterns_[type];
}
uint Pattern::count()
{
return patterns_.size();
}
glm::ivec2 Pattern::resolution()
{
return glm::ivec2( width_, height_);
@@ -106,26 +99,34 @@ glm::ivec2 Pattern::resolution()
void Pattern::open( uint pattern, glm::ivec2 res )
{
type_ = MIN(pattern, MAX_PATTERN-1);
std::string gstreamer_pattern = pattern_internal_[type_];
// clamp type to be sure
type_ = MIN(pattern, Pattern::patterns_.size()-1);
std::string gstreamer_pattern = Pattern::patterns_[type_].pipeline;
// there is always a special case...
switch(type_)
{
case 18: // zone plates
case 17:
{
std::ostringstream oss;
oss << " kx2=" << (int)(res.x * 10.f / res.y) << " ky2=10 kt=4";
gstreamer_pattern += oss.str(); // Zone plate
}
break;
default:
break;
}
//
// pattern string post-processing: replace placeholders by resolution values
// XXXX, YYYY = resolution x and y
// XXX, YYY = resolution x and y / 10
//
// if there is a XXXX parameter to enter
std::string::size_type xxxx = gstreamer_pattern.find("XXXX");
if (xxxx != std::string::npos)
gstreamer_pattern = gstreamer_pattern.replace(xxxx, 4, std::to_string(res.x));
// if there is a YYYY parameter to enter
std::string::size_type yyyy = gstreamer_pattern.find("YYYY");
if (yyyy != std::string::npos)
gstreamer_pattern = gstreamer_pattern.replace(yyyy, 4, std::to_string(res.y));
// if there is a XXX parameter to enter
std::string::size_type xxx = gstreamer_pattern.find("XXX");
if (xxx != std::string::npos)
gstreamer_pattern = gstreamer_pattern.replace(xxx, 3, std::to_string(res.x/10));
// if there is a YYY parameter to enter
std::string::size_type yyy = gstreamer_pattern.find("YYY");
if (yyy != std::string::npos)
gstreamer_pattern = gstreamer_pattern.replace(yyy, 3, std::to_string(res.y/10));
// all patterns before 'SMPTE test pattern' are single frames (not animated)
single_frame_ = type_ < 14;
// remember if the pattern is to be updated once or animated
single_frame_ = !Pattern::patterns_[type_].animated;
// (private) open stream
Stream::open(gstreamer_pattern, res.x, res.y);
@@ -143,10 +144,17 @@ PatternSource::PatternSource(uint64_t id) : StreamSource(id)
void PatternSource::setPattern(uint type, glm::ivec2 resolution)
{
Log::Notify("Creating Source with pattern '%s'", Pattern::pattern_types[type].c_str());
// open gstreamer with pattern
if ( Pattern::get(type).available) {
pattern()->open( (uint) type, resolution );
}
// revert to pattern Black if not available
else {
pattern()->open( 0, resolution );
Log::Warning("Pattern '%s' is not available in this version of vimix.", Pattern::get(type).label.c_str());
}
// open gstreamer
pattern()->open( (uint) type, resolution );
// play gstreamer
stream_->play(true);
// will be ready after init and one frame rendered
@@ -166,3 +174,12 @@ Pattern *PatternSource::pattern() const
}
glm::ivec2 PatternSource::icon() const
{
return glm::ivec2(ICON_SOURCE_PATTERN);
}
std::string PatternSource::info() const
{
return std::string("pattern '") + Pattern::get(pattern()->type()).label + "'";
}

View File

@@ -5,10 +5,22 @@
#include "StreamSource.h"
typedef struct pattern_ {
std::string label;
std::string feature;
std::string pipeline;
bool animated;
bool available;
} pattern_descriptor;
class Pattern : public Stream
{
static std::vector<pattern_descriptor> patterns_;
public:
static std::vector<std::string> pattern_types;
static pattern_descriptor get(uint type);
static uint count();
Pattern();
void open( uint pattern, glm::ivec2 res);
@@ -35,7 +47,8 @@ public:
Pattern *pattern() const;
void setPattern(uint type, glm::ivec2 resolution);
glm::ivec2 icon() const override { return glm::ivec2(11, 5); }
glm::ivec2 icon() const override;
std::string info() const override;
};

View File

@@ -1,15 +1,34 @@
#include "PickingVisitor.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "Log.h"
#include "Decorations.h"
#include "GlmToolkit.h"
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/vector_angle.hpp>
#include "Log.h"
#include "Decorations.h"
#include "GlmToolkit.h"
#include "PickingVisitor.h"
PickingVisitor::PickingVisitor(glm::vec3 coordinates, bool force) : Visitor(),
force_(force), modelview_(glm::mat4(1.f))

View File

@@ -18,10 +18,10 @@
*/
class PickingVisitor: public Visitor
{
bool force_;
std::vector<glm::vec3> points_;
glm::mat4 modelview_;
std::vector< std::pair<Node *, glm::vec2> > nodes_;
bool force_;
public:

View File

@@ -1,10 +1,22 @@
#include "Primitives.h"
#include "ImageShader.h"
#include "Resource.h"
#include "FrameBuffer.h"
#include "MediaPlayer.h"
#include "Visitor.h"
#include "Log.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glad/glad.h>
@@ -13,11 +25,17 @@
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/vector_angle.hpp>
#include <glm/gtc/constants.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/rotate_vector.hpp>
#include "ImageShader.h"
#include "Resource.h"
#include "FrameBuffer.h"
#include "MediaPlayer.h"
#include "Visitor.h"
#include "Log.h"
#include "Primitives.h"
Surface::Surface(Shader *s) : Primitive(s), textureindex_(0), mirror_(true)
{
@@ -121,52 +139,6 @@ void ImageSurface::accept(Visitor& v)
v.visit(*this);
}
MediaSurface::MediaSurface(const std::string& p, Shader *s) : Surface(s), path_(p)
{
mediaplayer_ = new MediaPlayer;
}
MediaSurface::~MediaSurface()
{
delete mediaplayer_;
}
void MediaSurface::init()
{
Surface::init();
mediaplayer_->open(path_);
mediaplayer_->play(true);
}
void MediaSurface::draw(glm::mat4 modelview, glm::mat4 projection)
{
if ( !initialized() ) {
init();
// set the texture to the media player once openned
if ( mediaplayer_->isOpen() )
textureindex_ = mediaplayer_->texture();
}
Surface::draw(modelview, projection);
}
void MediaSurface::update( float dt )
{
if ( mediaplayer_->isOpen() ) {
mediaplayer_->update();
scale_.x = mediaplayer_->aspectRatio();
}
Primitive::update( dt );
}
void MediaSurface::accept(Visitor& v)
{
Surface::accept(v);
v.visit(*this);
}
FrameBufferSurface::FrameBufferSurface(FrameBuffer *fb, Shader *s) : Surface(s), frame_buffer_(fb)
{
}
@@ -401,6 +373,24 @@ LineSquare::LineSquare(float linewidth) : Group()
}
LineSquare::LineSquare(const LineSquare &square)
{
top_ = new HLine(square.top_->width);
top_->translation_ = glm::vec3(0.f, 1.f, 0.f);
attach(top_);
bottom_ = new HLine(square.bottom_->width);
bottom_->translation_ = glm::vec3(0.f, -1.f, 0.f);
attach(bottom_);
left_ = new VLine(square.left_->width);
left_->translation_ = glm::vec3(-1.f, 0.f, 0.f);
attach(left_);
right_ = new VLine(square.right_->width);
right_->translation_ = glm::vec3(1.f, 0.f, 0.f);
attach(right_);
setColor(square.color());
}
void LineSquare::setLineWidth(float v)
{
top_->width = v;
@@ -417,7 +407,6 @@ void LineSquare::setColor(glm::vec4 c)
right_->color = c;
}
LineStrip::LineStrip(const std::vector<glm::vec2> &path, float linewidth) : Primitive(new Shader),
arrayBuffer_(0), path_(path)
{

View File

@@ -62,31 +62,6 @@ protected:
};
/**
* @brief The MediaSurface class is a Surface to draw a video
*
* URI is passed to a Media Player to handle the video playback
* Height = 1.0, Width is set by the aspect ratio of the image
*/
class MediaSurface : public Surface {
public:
MediaSurface(const std::string& p, Shader *s = new ImageShader);
~MediaSurface();
void init () override;
void draw (glm::mat4 modelview, glm::mat4 projection) override;
void accept (Visitor& v) override;
void update (float dt) override;
inline std::string path() const { return path_; }
inline MediaPlayer *mediaPlayer() const { return mediaplayer_; }
protected:
std::string path_;
MediaPlayer *mediaplayer_;
};
/**
* @brief The FrameBufferSurface class is a Surface to draw a framebuffer
*
@@ -101,7 +76,8 @@ public:
void draw (glm::mat4 modelview, glm::mat4 projection) override;
void accept (Visitor& v) override;
inline FrameBuffer *getFrameBuffer() const { return frame_buffer_; }
inline void setFrameBuffer(FrameBuffer *fb) { frame_buffer_ = fb; }
inline FrameBuffer *frameBuffer() const { return frame_buffer_; }
protected:
FrameBuffer *frame_buffer_;
@@ -166,6 +142,7 @@ class LineSquare : public Group {
public:
LineSquare(float linewidth = 1.f);
LineSquare(const LineSquare &square);
void setLineWidth(float v);
inline float lineWidth() const { return top_->width; }

View File

@@ -12,6 +12,11 @@ monitor or a projector, but can be recorded live (no audio).
vimix is the successor for GLMixer - https://sourceforge.net/projects/glmixer/
# License
GPL-3.0-or-later
See [LICENSE](https://github.com/brunoherbelin/vimix/blob/master/LICENSE)
# Install
Check the [Quick Installation Guide](https://github.com/brunoherbelin/vimix/wiki/Quick-Installation-Guide)
@@ -64,7 +69,7 @@ Compile (or re-compile after pull):
**Libraries:**
- gstreamer
- gst-plugins : base, good, bad & ugly
- gst-plugins : libav, base, good, bad & ugly
- libglfw3
- libicu
@@ -72,7 +77,7 @@ Compile (or re-compile after pull):
**Ubuntu**
$ apt-get install build-essential cmake libpng-dev libglfw3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libicu-dev libgtk-3-dev
$ apt-get install build-essential cmake libpng-dev libglfw3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-libav libicu-dev libgtk-3-dev
**OSX with Brew**

View File

@@ -1,4 +1,25 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <thread>
#include<algorithm> // for copy() and assign()
#include<iterator> // for back_inserter
// Desktop OpenGL function loader
#include <glad/glad.h>
@@ -24,11 +45,11 @@ PNGRecorder::PNGRecorder() : FrameGrabber()
{
}
void PNGRecorder::init(GstCaps *caps)
std::string PNGRecorder::init(GstCaps *caps)
{
// ignore
if (caps == nullptr)
return;
return std::string("Invalid caps");
// create a gstreamer pipeline
std::string description = "appsrc name=src ! videoconvert ! pngenc ! filesink name=sink";
@@ -37,10 +58,9 @@ void PNGRecorder::init(GstCaps *caps)
GError *error = NULL;
pipeline_ = gst_parse_launch (description.c_str(), &error);
if (error != NULL) {
Log::Warning("PNG Capture Could not construct pipeline %s:\n%s", description.c_str(), error->message);
std::string msg = std::string("PNG Capture Could not construct pipeline ") + description + "\n" + std::string(error->message);
g_clear_error (&error);
finished_ = true;
return;
return msg;
}
// verify location path (path is always terminated by the OS dependent separator)
@@ -60,12 +80,13 @@ void PNGRecorder::init(GstCaps *caps)
if (src_) {
g_object_set (G_OBJECT (src_),
"stream-type", GST_APP_STREAM_TYPE_STREAM,
"is-live", TRUE,
"format", GST_FORMAT_TIME,
// "do-timestamp", TRUE,
NULL);
// configure stream
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
gst_app_src_set_latency( src_, -1, 0);
// Direct encoding (no buffering)
gst_app_src_set_max_bytes( src_, 0 );
@@ -82,38 +103,35 @@ void PNGRecorder::init(GstCaps *caps)
}
else {
Log::Warning("PNG Capture Could not configure source");
finished_ = true;
return;
return std::string("PNG Capture : Failed to configure frame grabber.");
}
// start pipeline
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
Log::Warning("PNG Capture Could not record %s", filename_.c_str());
finished_ = true;
return;
return std::string("PNG Capture : Failed to start frame grabber.");
}
// all good
Log::Info("PNG Capture started.");
initialized_ = true;
// start recording !!
active_ = true;
return std::string("PNG Capture started ");
}
void PNGRecorder::terminate()
{
// remember and inform
Settings::application.recentRecordings.push(filename_);
Log::Notify("PNG Capture %s is ready.", filename_.c_str());
}
void PNGRecorder::addFrame(GstBuffer *buffer, GstCaps *caps, float dt)
void PNGRecorder::addFrame(GstBuffer *buffer, GstCaps *caps)
{
FrameGrabber::addFrame(buffer, caps, dt);
FrameGrabber::addFrame(buffer, caps);
// PNG Recorder specific :
// stop after one frame
if (timestamp_ > 0) {
if (frame_count_ > 0) {
stop();
}
}
@@ -123,12 +141,13 @@ const char* VideoRecorder::profile_name[VideoRecorder::DEFAULT] = {
"H264 (Realtime)",
"H264 (High 4:4:4)",
"H265 (Realtime)",
"H265 (HQ Animation)",
"H265 (HQ)",
"ProRes (Standard)",
"ProRes (HQ 4444)",
"WebM VP8 (2MB/s)",
"WebM VP8 (Realtime)",
"Multiple JPEG"
};
const std::vector<std::string> VideoRecorder::profile_description {
// Control x264 encoder quality :
// pass
@@ -138,19 +157,17 @@ const std::vector<std::string> VideoRecorder::profile_description {
// The total range is from 0 to 51, where 0 is lossless, 18 can be considered visually lossless,
// and 51 is terrible quality. A sane range is 18-26, and the default is 23.
// speed-preset
// ultrafast (1)
// superfast (2)
// veryfast (3)
// faster (4)
// fast (5)
#ifndef APPLE
// "video/x-raw, format=I420 ! x264enc pass=4 quantizer=26 speed-preset=3 threads=4 ! video/x-h264, profile=baseline ! h264parse ! ",
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" threads=4 ! video/x-h264, profile=baseline ! h264parse ! ",
#else
"video/x-raw, format=I420 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! h264parse ! ",
#endif
"video/x-raw, format=Y444_10LE ! x264enc pass=4 quantizer=16 speed-preset=4 threads=4 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! video/x-h264, profile=baseline ! h264parse ! ",
"video/x-raw, format=Y444_10LE ! x264enc pass=4 quantizer=18 speed-preset=3 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
// Control x265 encoder quality :
// NB: apparently x265 only accepts I420 format :(
// speed-preset
// superfast (2)
// veryfast (3)
// faster (4)
// fast (5)
@@ -162,56 +179,176 @@ const std::vector<std::string> VideoRecorder::profile_description {
// fastdecode (5)
// animation (6) optimize the encode quality for animation content without impacting the encode speed
// crf Quality-controlled variable bitrate [0 51]
// default 28
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
"video/x-raw, format=I420 ! x265enc tune=4 speed-preset=3 ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=I420 ! x265enc tune=6 speed-preset=4 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
// default 28
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
"video/x-raw, format=I420 ! x265enc tune=2 speed-preset=2 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=I420 ! x265enc tune=6 speed-preset=2 option-string=\"crf=12\" ! video/x-h265, profile=(string)main ! h265parse ! ",
// Apple ProRes encoding parameters
// pass
// cbr (0) Constant Bitrate Encoding
// quant (2) Constant Quantizer
// pass1 (512) VBR Encoding - Pass 1
// profile
// 0 proxy
// 1 lt
// 2 standard
// 3 hq
// 4 4444
"avenc_prores_ks pass=2 profile=2 quantizer=26 ! ",
"video/x-raw, format=Y444_10LE ! avenc_prores_ks pass=2 profile=4 quantizer=12 ! ",
// 0 proxy 45Mbps YUV 4:2:2
// 1 lt 102Mbps YUV 4:2:2
// 2 standard 147Mbps YUV 4:2:2
// 3 hq 220Mbps YUV 4:2:2
// 4 4444 330Mbps YUVA 4:4:4:4
// quant-mat
// -1 auto
// 0 proxy
// 2 lt
// 3 standard
// 4 hq
// 6 default
"video/x-raw, format=I422_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=2 quant-mat=6 quantizer=8 ! ",
"video/x-raw, format=Y444_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=4 quant-mat=6 quantizer=4 ! ",
// VP8 WebM encoding
"vp8enc end-usage=vbr cpu-used=8 max-quantizer=35 deadline=100000 target-bitrate=200000 keyframe-max-dist=360 token-partitions=2 static-threshold=100 ! ",
"jpegenc ! "
// deadline per frame (usec)
// 0=best,
// 1=realtime
// see https://www.webmproject.org/docs/encoder-parameters/
// "vp8enc end-usage=cbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 undershoot=95 "
// "buffer-size=6000 buffer-initial-size=4000 buffer-optimal-size=5000 "
// "keyframe-max-dist=999999 min-quantizer=4 max-quantizer=50 ! ",
"vp8enc end-usage=vbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 keyframe-max-dist=360 "
"token-partitions=2 static-threshold=1000 min-quantizer=4 max-quantizer=20 ! ",
// JPEG encoding
"jpegenc idct-method=float ! "
};
// Too slow
//// WebM VP9 encoding parameters
//// https://www.webmproject.org/docs/encoder-parameters/
//// https://developers.google.com/media/vp9/settings/vod/
//"vp9enc end-usage=vbr end-usage=vbr cpu-used=3 max-quantizer=35 target-bitrate=200000 keyframe-max-dist=360 token-partitions=2 static-threshold=1000 ! "
// FAILED
// x265 encoder quality
// string description = "appsrc name=src ! videoconvert ! "
// "x265enc tune=4 speed-preset=2 option-string='crf=28' ! h265parse ! "
// "qtmux ! filesink name=sink";
#if GST_GL_HAVE_PLATFORM_GLX
// under GLX (Linux), gstreamer might have nvidia or vaapi encoders
// the hardware encoder will be filled at first instanciation of VideoRecorder
std::vector<std::string> VideoRecorder::hardware_encoder;
std::vector<std::string> VideoRecorder::hardware_profile_description;
std::vector<std::string> nvidia_encoder = {
"nvh264enc",
"nvh264enc",
"nvh265enc",
"nvh265enc",
"", "", "", ""
};
std::vector<std::string> nvidia_profile_description {
// qp-const Constant quantizer (-1 = from NVENC preset)
// Range: -1 - 51 Default: -1
// rc-mode Rate Control Mode
// (0): default - Default
// (1): constqp - Constant Quantization
// (2): cbr - Constant Bit Rate
// (3): vbr - Variable Bit Rate
// (4): vbr-minqp - Variable Bit Rate (with minimum quantization parameter, DEPRECATED)
// (5): cbr-ld-hq - Low-Delay CBR, High Quality
// (6): cbr-hq - CBR, High Quality (slower)
// (7): vbr-hq - VBR, High Quality (slower)
// Control nvh264enc encoder
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! h264parse ! ",
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 qp-const=18 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
// Control nvh265enc encoder
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main-10 ! h265parse ! ",
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
"", "", "", ""
};
std::vector<std::string> vaapi_encoder = {
"vaapih264enc",
"vaapih264enc",
"vaapih265enc",
"vaapih265enc",
"", "", "", ""
};
std::vector<std::string> vaapi_profile_description {
// Control vaapih264enc encoder
"video/x-raw, format=NV12 ! vaapih264enc rate-control=cqp init-qp=26 ! video/x-h264, profile=(string)main ! h264parse ! ",
"video/x-raw, format=NV12 ! vaapih264enc rate-control=cqp init-qp=14 quality-level=4 keyframe-period=0 max-bframes=2 ! video/x-h264, profile=(string)high ! h264parse ! ",
// Control vaapih265enc encoder
"video/x-raw, format=NV12 ! vaapih265enc ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=NV12 ! vaapih265enc rate-control=cqp init-qp=14 quality-level=4 keyframe-period=0 max-bframes=2 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
"", "", "", ""
};
#elif GST_GL_HAVE_PLATFORM_CGL
// under CGL (Mac), gstreamer might have the VideoToolbox
std::vector<std::string> VideoRecorder::hardware_encoder = {
"vtenc_h264_hw",
"vtenc_h264_hw",
"", "", "", "", "", ""
};
std::vector<std::string> VideoRecorder::hardware_profile_description {
// Control vtenc_h264_hw encoder
"video/x-raw, format=I420 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! h264parse ! ",
"video/x-raw, format=UYVY ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 quality=0.9 ! h264parse ! ",
"", "", "", "", "", ""
};
#else
// in other platforms, no hardware encoder
std::vector<std::string> VideoRecorder::hardware_encoder;
std::vector<std::string> VideoRecorder::hardware_profile_description;
#endif
const char* VideoRecorder::buffering_preset_name[6] = { "Minimum", "100 MB", "200 MB", "500 MB", "1 GB", "2 GB" };
const guint64 VideoRecorder::buffering_preset_value[6] = { MIN_BUFFER_SIZE, 104857600, 209715200, 524288000, 1073741824, 2147483648 };
const char* VideoRecorder::framerate_preset_name[3] = { "15 FPS", "25 FPS", "30 FPS" };
const gint VideoRecorder::framerate_preset_value[3] = { 15, 25, 30 };
VideoRecorder::VideoRecorder() : FrameGrabber()
{
// first run initialization of hardware encoders in linux
#if GST_GL_HAVE_PLATFORM_GLX
if (hardware_encoder.size() < 1) {
// test nvidia encoder
if ( GstToolkit::has_feature(nvidia_encoder[0] ) ) {
// consider that if first nvidia encoder is valid, all others should also be available
hardware_encoder.assign(nvidia_encoder.begin(), nvidia_encoder.end());
hardware_profile_description.assign(nvidia_profile_description.begin(), nvidia_profile_description.end());
}
// test vaapi encoder
else if ( GstToolkit::has_feature(vaapi_encoder[0] ) ) {
hardware_encoder.assign(vaapi_encoder.begin(), vaapi_encoder.end());
hardware_profile_description.assign(vaapi_profile_description.begin(), vaapi_profile_description.end());
}
}
#endif
}
void VideoRecorder::init(GstCaps *caps)
std::string VideoRecorder::init(GstCaps *caps)
{
// ignore
if (caps == nullptr)
return;
return std::string("Invalid caps");
// apply settings
buffering_size_ = MAX( MIN_BUFFER_SIZE, buffering_preset_value[Settings::application.record.buffering_mode]);
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, framerate_preset_value[Settings::application.record.framerate_mode]);
timestamp_on_clock_ = Settings::application.record.priority_mode < 1;
// create a gstreamer pipeline
std::string description = "appsrc name=src ! videoconvert ! ";
if (Settings::application.record.profile < 0 || Settings::application.record.profile >= DEFAULT)
Settings::application.record.profile = H264_STANDARD;
description += profile_description[Settings::application.record.profile];
// test for a hardware accelerated encoder
if (Settings::application.render.gpu_decoding && (int) hardware_encoder.size() > 0 &&
GstToolkit::has_feature(hardware_encoder[Settings::application.record.profile]) ) {
description += hardware_profile_description[Settings::application.record.profile];
Log::Info("Video Recording using hardware accelerated encoder (%s)", hardware_encoder[Settings::application.record.profile].c_str());
}
// revert to software encoder
else
description += profile_description[Settings::application.record.profile];
// verify location path (path is always terminated by the OS dependent separator)
std::string path = SystemToolkit::path_directory(Settings::application.record.path);
@@ -238,10 +375,9 @@ void VideoRecorder::init(GstCaps *caps)
GError *error = NULL;
pipeline_ = gst_parse_launch (description.c_str(), &error);
if (error != NULL) {
Log::Warning("VideoRecorder Could not construct pipeline %s:\n%s", description.c_str(), error->message);
std::string msg = std::string("Video Recording : Could not construct pipeline ") + description + "\n" + std::string(error->message);
g_clear_error (&error);
finished_ = true;
return;
return msg;
}
// setup file sink
@@ -255,18 +391,32 @@ void VideoRecorder::init(GstCaps *caps)
if (src_) {
g_object_set (G_OBJECT (src_),
"stream-type", GST_APP_STREAM_TYPE_STREAM,
"is-live", TRUE,
"format", GST_FORMAT_TIME,
// "do-timestamp", TRUE,
NULL);
// Direct encoding (no buffering)
gst_app_src_set_max_bytes( src_, 0 );
if (timestamp_on_clock_)
g_object_set (G_OBJECT (src_),"do-timestamp", TRUE,NULL);
// instruct src to use the required caps
caps_ = gst_caps_copy( caps );
// configure stream
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
gst_app_src_set_latency( src_, -1, 0);
// Set buffer size
gst_app_src_set_max_bytes( src_, buffering_size_);
// specify recorder framerate in the given caps
GstCaps *tmp = gst_caps_copy( caps );
GValue v = { 0, };
g_value_init (&v, GST_TYPE_FRACTION);
gst_value_set_fraction (&v, framerate_preset_value[Settings::application.record.framerate_mode], 1);
gst_caps_set_value(tmp, "framerate", &v);
g_value_unset (&v);
// instruct src to use the caps
caps_ = gst_caps_copy( tmp );
gst_app_src_set_caps (src_, caps_);
gst_caps_unref (tmp);
// setup callbacks
GstAppSrcCallbacks callbacks;
@@ -277,35 +427,52 @@ void VideoRecorder::init(GstCaps *caps)
}
else {
Log::Warning("VideoRecorder Could not configure source");
finished_ = true;
return;
return std::string("Video Recording : Failed to configure frame grabber.");
}
// start recording
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
Log::Warning("VideoRecorder Could not record %s", filename_.c_str());
finished_ = true;
return;
return std::string("Video Recording : Failed to start frame grabber.");
}
// all good
Log::Info("Video Recording started (%s)", profile_name[Settings::application.record.profile]);
initialized_ = true;
return std::string("Video Recording started ") + profile_name[Settings::application.record.profile];
// start recording !!
active_ = true;
}
void VideoRecorder::terminate()
{
// stop the pipeline (again)
gst_element_set_state (pipeline_, GST_STATE_NULL);
// statistics on expected number of frames
guint64 N = MAX( (guint64) duration_ / (guint64) frame_duration_, frame_count_);
float loss = 100.f * ((float) (N - frame_count_) ) / (float) N;
Log::Info("Video Recording : %ld frames captured in %s (aming for %ld, %.0f%% lost)",
frame_count_, GstToolkit::time_to_string(duration_, GstToolkit::TIME_STRING_READABLE).c_str(), N, loss);
// warn user if more than 10% lost
if (loss > 10.f) {
if (timestamp_on_clock_)
Log::Warning("Video Recording lost %.0f%% of frames: framerate could not be maintained at %ld FPS.", loss, GST_SECOND / frame_duration_);
else
Log::Warning("Video Recording lost %.0f%% of frames: video is only %s long.",
loss, GstToolkit::time_to_string(timestamp_, GstToolkit::TIME_STRING_READABLE).c_str());
Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec.");
}
// remember and inform
Settings::application.recentRecordings.push(filename_);
Log::Notify("Video Recording %s is ready.", filename_.c_str());
}
std::string VideoRecorder::info() const
{
if (active_)
return GstToolkit::time_to_string(timestamp_);
else
if (initialized_ && !active_ && !endofstream_)
return "Saving file...";
return FrameGrabber::info();
}

View File

@@ -2,6 +2,8 @@
#define RECORDER_H
#include <vector>
#include <string>
#include <gst/pbutils/pbutils.h>
#include <gst/app/gstappsrc.h>
@@ -10,26 +12,26 @@
class PNGRecorder : public FrameGrabber
{
std::string filename_;
std::string filename_;
public:
PNGRecorder();
std::string filename() const { return filename_; }
protected:
void init(GstCaps *caps) override;
std::string init(GstCaps *caps) override;
void terminate() override;
void addFrame(GstBuffer *buffer, GstCaps *caps, float dt) override;
void addFrame(GstBuffer *buffer, GstCaps *caps) override;
};
class VideoRecorder : public FrameGrabber
{
std::string filename_;
std::string filename_;
void init(GstCaps *caps) override;
std::string init(GstCaps *caps) override;
void terminate() override;
public:
@@ -45,12 +47,19 @@ public:
JPEG_MULTI,
DEFAULT
} Profile;
static const char* profile_name[DEFAULT];
static const char* profile_name[DEFAULT];
static const std::vector<std::string> profile_description;
static std::vector<std::string> hardware_encoder;
static std::vector<std::string> hardware_profile_description;
static const char* buffering_preset_name[6];
static const guint64 buffering_preset_value[6];
static const char* framerate_preset_name[3];
static const int framerate_preset_value[3];
VideoRecorder();
std::string info() const override;
std::string filename() const { return filename_; }
};

View File

@@ -1,3 +1,24 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <thread>
// Opengl
#include <glad/glad.h>
#include <glm/glm.hpp>
@@ -123,18 +144,22 @@ FrameBufferImage *RenderView::thumbnail ()
// by default null image
FrameBufferImage *img = nullptr;
// this function is always called from a parallel thread
// So we wait for a few frames of rendering before trying to capture a thumbnail
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// create and store a promise for a FrameBufferImage
thumbnailer_.emplace_back( std::promise<FrameBufferImage *>() );
// future will return the primised FrameBufferImage
std::future<FrameBufferImage *> t = thumbnailer_.back().get_future();
// future will return the promised FrameBufferImage
std::future<FrameBufferImage *> ft = thumbnailer_.back().get_future();
try {
// wait for valid return value from promise
img = t.get();
// wait for a valid return value from promise
img = ft.get();
}
// catch any failed promise
catch (std::runtime_error&){
catch (const std::exception&){
}
return img;

View File

@@ -8,6 +8,8 @@
class RenderView : public View
{
friend class Session;
// rendering FBO
FrameBuffer *frame_buffer_;
Surface *fading_overlay_;
@@ -26,12 +28,15 @@ public:
void setResolution (glm::vec3 resolution = glm::vec3(0.f), bool useAlpha = false);
glm::vec3 resolution() const { return frame_buffer_->resolution(); }
void setFading(float f = 0.f);
float fading() const;
// current frame
inline FrameBuffer *frame () const { return frame_buffer_; }
protected:
void setFading(float f = 0.f);
float fading() const;
// get a thumbnail outside of opengl context; wait for a promise to be fullfiled after draw
void drawThumbnail();
FrameBufferImage *thumbnail ();

View File

@@ -1,4 +1,23 @@
#include <cstring>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <cstring>
#include <thread>
#include <mutex>
#include <chrono>
@@ -38,7 +57,7 @@
// standalone image loader
#include <stb_image.h>
// vmix
// vimix
#include "defines.h"
#include "Log.h"
#include "Resource.h"
@@ -48,6 +67,7 @@
#include "SystemToolkit.h"
#include "GstToolkit.h"
#include "UserInterfaceManager.h"
#include "RenderingManager.h"
#ifdef USE_GST_OPENGL_SYNC_HANDLER
@@ -100,7 +120,7 @@ void Rendering::LinkPipeline( GstPipeline *pipeline )
#endif
static std::map<GLFWwindow *, RenderingWindow*> GLFW_window_;
std::map<GLFWwindow *, RenderingWindow*> GLFW_window_;
static void glfw_error_callback(int error, const char* description)
{
@@ -188,7 +208,6 @@ bool Rendering::init()
// additional window callbacks for main window
glfwSetWindowRefreshCallback( main_.window(), WindowRefreshCallback );
glfwSetDropCallback( main_.window(), Rendering::FileDropped);
glfwSetWindowSizeLimits( main_.window(), 800, 500, GLFW_DONT_CARE, GLFW_DONT_CARE);
//
// Gstreamer setup
@@ -200,6 +219,11 @@ bool Rendering::init()
g_setenv ("GST_PLUGIN_SYSTEM_PATH", plugins_path.c_str(), TRUE);
g_setenv ("GST_PLUGIN_SCANNER", plugins_scanner.c_str(), TRUE);
}
std::string frei0r_path = SystemToolkit::cwd_path() + "frei0r-1" ;
if ( SystemToolkit::file_exists(frei0r_path)) {
Log::Info("Found Frei0r plugins in %s", frei0r_path.c_str());
g_setenv ("FREI0R_PATH", frei0r_path.c_str(), TRUE);
}
g_setenv ("GST_GL_API", "opengl3", TRUE);
gst_init (NULL, NULL);
@@ -207,9 +231,10 @@ bool Rendering::init()
std::list<std::string> gpuplugins = GstToolkit::enable_gpu_decoding_plugins(Settings::application.render.gpu_decoding);
if (Settings::application.render.gpu_decoding) {
if (gpuplugins.size() > 0) {
Log::Info("Fond the following GPU decoding plugin(s):");
for(auto it = gpuplugins.begin(); it != gpuplugins.end(); it++)
Log::Info(" - %s", (*it).c_str());
Log::Info("Found the following GPU decoding plugin(s):");
int i = 1;
for(auto it = gpuplugins.rbegin(); it != gpuplugins.rend(); it++, ++i)
Log::Info("%d. %s", i, (*it).c_str());
}
else {
Log::Info("No GPU decoding plugin found.");
@@ -278,24 +303,26 @@ void Rendering::pushBackDrawCallback(RenderingCallback function)
void Rendering::draw()
{
// guint64 _time = gst_util_get_timestamp ();
// change windows fullscreen mode if requested
main_.toggleFullscreen_();
output_.toggleFullscreen_();
// change main window title if requested
if (!main_new_title_.empty()) {
main_.setTitle(main_new_title_);
main_new_title_.clear();
}
// operate on main window context
main_.makeCurrent();
// User Interface step 1
UserInterface::manager().NewFrame();
// Custom draw
// draw
std::list<Rendering::RenderingCallback>::iterator iter;
for (iter=draw_callbacks_.begin(); iter != draw_callbacks_.end(); ++iter)
{
(*iter)();
}
// User Interface step 2
UserInterface::manager().Render();
// perform screenshot if requested
if (request_screenshot_) {
// glfwMakeContextCurrent(main_window_);
@@ -303,28 +330,11 @@ void Rendering::draw()
request_screenshot_ = false;
}
// draw output window (and swap buffer output)
output_.draw( Mixer::manager().session()->frame() );
// swap GL buffers
glfwSwapBuffers(main_.window());
glfwSwapBuffers(output_.window());
// Poll and handle events (inputs, window resize, etc.)
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
glfwPollEvents();
// change windows
main_.toggleFullscreen_();
output_.toggleFullscreen_();
#ifndef USE_GST_APPSINK_CALLBACKS
// no g_main_loop_run(loop) : update global GMainContext
g_main_context_iteration(NULL, FALSE);
#endif
// draw output window (and swap buffer output)
output_.draw( Mixer::manager().session()->frame() );
// software framerate limiter 60FPS if not v-sync
if ( Settings::application.render.vsync < 1 ) {
@@ -335,6 +345,16 @@ void Rendering::draw()
g_timer_start(timer);
}
// Poll and handle events (inputs, window resize, etc.)
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
glfwPollEvents();
// no g_main_loop_run(loop) : update global GMainContext
g_main_context_iteration(NULL, FALSE);
}
@@ -485,9 +505,11 @@ RenderingWindow::~RenderingWindow()
void RenderingWindow::setTitle(const std::string &title)
{
std::string fulltitle = Settings::application.windows[index_].name;
if ( !title.empty() )
fulltitle += " -- " + title;
std::string fulltitle;
if ( title.empty() )
fulltitle = Settings::application.windows[index_].name;
else
fulltitle = title + std::string(" - " APP_NAME);
glfwSetWindowTitle(window_, fulltitle.c_str());
}
@@ -583,7 +605,7 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
// done request
request_toggle_fullscreen_ = false;
// if in fullscreen mode
// disable fullscreen mode
if (mo == nullptr) {
// store fullscreen mode
Settings::application.windows[index_].fullscreen = false;
@@ -595,7 +617,7 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
Settings::application.windows[index_].w,
Settings::application.windows[index_].h, 0 );
}
// not in fullscreen mode
// set fullscreen mode
else {
// store fullscreen mode
Settings::application.windows[index_].fullscreen = true;
@@ -605,12 +627,11 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
const GLFWvidmode * mode = glfwGetVideoMode(mo);
glfwSetInputMode( window_, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
glfwSetWindowMonitor( window_, mo, 0, 0, mode->width, mode->height, mode->refreshRate);
// Enable vsync on output window only (i.e. not 0 if has a master)
// Workaround for disabled vsync in fullscreen (https://github.com/glfw/glfw/issues/1072)
glfwSwapInterval( nullptr == master_ ? 0 : Settings::application.render.vsync);
}
// Enable vsync on output window only (i.e. not 0 if has a master)
// Workaround for disabled vsync in fullscreen (https://github.com/glfw/glfw/issues/1072)
glfwSwapInterval( nullptr == master_ ? 0 : Settings::application.render.vsync);
}
@@ -689,23 +710,25 @@ bool RenderingWindow::init(int index, GLFWwindow *share)
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_AUTO_ICONIFY, GLFW_FALSE);
// create the window normal
// create the window
window_ = glfwCreateWindow(winset.w, winset.h, winset.name.c_str(), NULL, master_);
if (window_ == NULL){
Log::Error("Failed to create GLFW Window %d", index_);
return false;
}
// set position
// ensure minimal window size
glfwSetWindowSizeLimits(window_, 800, 500, GLFW_DONT_CARE, GLFW_DONT_CARE);
// set initial position
glfwSetWindowPos(window_, winset.x, winset.y);
/// CALLBACKS
// store global ref to pointers (used by callbacks)
GLFW_window_[window_] = this;
// window position and resize callbacks
glfwSetWindowSizeCallback( window_, WindowResizeCallback );
// glfwSetFramebufferSizeCallback( window_, WindowResizeCallback );
glfwSetWindowPosCallback( window_, WindowMoveCallback );
glfwSetWindowSizeCallback( window_, WindowResizeCallback );
// take opengl context ownership
glfwMakeContextCurrent(window_);
@@ -826,39 +849,45 @@ void RenderingWindow::draw(FrameBuffer *fb)
// attach the 2D texture to local FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureid_, 0);
#ifndef NDEBUG
Log::Info("Blit to output window enabled.");
#endif
}
// calculate scaling factor of frame buffer inside window
int rx, ry, rw, rh;
float renderingAspectRatio = fb->aspectRatio();
if (aspectRatio() < renderingAspectRatio) {
int nh = (int)( float(window_attributes_.viewport.x) / renderingAspectRatio);
rx = 0;
ry = (window_attributes_.viewport.y - nh) / 2;
rw = window_attributes_.viewport.x;
rh = (window_attributes_.viewport.y + nh) / 2;
} else {
int nw = (int)( float(window_attributes_.viewport.y) * renderingAspectRatio );
rx = (window_attributes_.viewport.x - nw) / 2;
ry = 0;
rw = (window_attributes_.viewport.x + nw) / 2;
rh = window_attributes_.viewport.y;
// if not disabled
if (!Settings::application.render.disabled) {
// calculate scaling factor of frame buffer inside window
int rx, ry, rw, rh;
float renderingAspectRatio = fb->aspectRatio();
if (aspectRatio() < renderingAspectRatio) {
int nh = (int)( float(window_attributes_.viewport.x) / renderingAspectRatio);
rx = 0;
ry = (window_attributes_.viewport.y - nh) / 2;
rw = window_attributes_.viewport.x;
rh = (window_attributes_.viewport.y + nh) / 2;
} else {
int nw = (int)( float(window_attributes_.viewport.y) * renderingAspectRatio );
rx = (window_attributes_.viewport.x - nw) / 2;
ry = 0;
rw = (window_attributes_.viewport.x + nw) / 2;
rh = window_attributes_.viewport.y;
}
// select fbo texture read target
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_);
// select screen target
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// blit operation from fbo (containing texture) to screen
glBlitFramebuffer(0, fb->height(), fb->width(), 0, rx, ry, rw, rh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
// select fbo texture read target
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_);
// select screen target
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// blit operation from fbo (containing texture) to screen
glBlitFramebuffer(0, fb->height(), fb->width(), 0, rx, ry, rw, rh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
// draw geometry
else
else if (!Settings::application.render.disabled)
{
// VAO is not shared between multiple contexts of different windows
// so we have to create a new VAO for rendering the surface in this window
@@ -888,11 +917,13 @@ void RenderingWindow::draw(FrameBuffer *fb)
glBindTexture(GL_TEXTURE_2D, 0);
}
// restore attribs
Rendering::manager().popAttrib();
// swap buffer
glfwSwapBuffers(window_);
}
// restore attribs
Rendering::manager().popAttrib();
// give back context ownership
glfwMakeContextCurrent(master_);
}

View File

@@ -29,6 +29,7 @@ class RenderingWindow
GLFWwindow *window_, *master_;
RenderingAttrib window_attributes_;
std::string title_changed_;
int index_;
float dpi_scale_;
@@ -106,9 +107,8 @@ public:
// Initialization OpenGL and GLFW window creation
bool init();
// show windows and reset views
void show();
// true if active rendering window
bool isActive();
// draw one frame
@@ -131,6 +131,7 @@ public:
// get hold on the windows
inline RenderingWindow& mainWindow() { return main_; }
inline RenderingWindow& outputWindow() { return output_; }
inline void setMainWindowTitle(const std::string t) { main_new_title_ = t; }
// request screenshot
void requestScreenshot();
@@ -160,6 +161,7 @@ private:
std::list<RenderingCallback> draw_callbacks_;
RenderingWindow main_;
std::string main_new_title_;
RenderingWindow output_;
// file drop callback

View File

@@ -1,6 +1,21 @@
#include "defines.h"
#include "Resource.h"
#include "Log.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <fstream>
#include <sstream>
@@ -17,6 +32,10 @@
#include <cmrc/cmrc.hpp>
CMRC_DECLARE(vmix);
#include "defines.h"
#include "Log.h"
#include "Resource.h"
std::map<std::string, uint> textureIndex;
std::map<std::string, float> textureAspectRatio;
@@ -162,9 +181,8 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
uint mipMapCount = *(uint*)&(header[24]);
uint fourCC = *(uint*)&(header[80]);
// how big is it going to be including all mipmaps?
uint bufsize;
bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
// how big is it going to be including all mipmaps?
uint bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
// get the buffer = bytes [128 - ]
const char *buffer = fp + 128;
@@ -188,7 +206,7 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
}
}
if (height == 0){
if (height == 0 || bufsize == 0){
Log::Error("Invalid image in ressource %s", std::string(path).c_str());
return 0;
}
@@ -237,10 +255,9 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
uint Resource::getTextureImage(const std::string& path, float *aspect_ratio)
{
std::string ext = path.substr(path.find_last_of(".") + 1);
if (ext=="dds")
if (ext=="dds"){
return getTextureDDS(path, aspect_ratio);
GLuint textureID = 0;
}
// return previously openned resource if already openned before
if (textureIndex.count(path) > 0) {
@@ -248,6 +265,7 @@ uint Resource::getTextureImage(const std::string& path, float *aspect_ratio)
return textureIndex[path];
}
GLuint textureID = 0;
float ar = 1.0;
int w, h, n;
unsigned char* img = nullptr;

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
@@ -20,7 +39,9 @@
#include "Scene.h"
#define DEBUG_SCENE 0
#if DEBUG_SCENE
static int num_nodes_ = 0;
#endif
// Node
Node::Node() : initialized_(false), visible_(true), refcount_(0)
@@ -70,21 +91,18 @@ void Node::copyTransform(const Node *other)
void Node::update( float dt)
{
std::list<UpdateCallback *>::iterator iter;
for (iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
{
UpdateCallback *callback = *iter;
if (callback->enabled())
callback->update(this, dt);
callback->update(this, dt);
if (callback->finished()) {
iter = update_callbacks_.erase(iter);
delete callback;
}
else {
else
++iter;
}
}
// update transform matrix from attributes

View File

@@ -1,4 +1,21 @@
#include "Screenshot.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <memory.h>
#include <assert.h>
@@ -11,6 +28,7 @@
#include <stb_image.h>
#include <stb_image_write.h>
#include "Screenshot.h"
Screenshot::Screenshot()

View File

@@ -1,12 +1,31 @@
#include <algorithm>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "SearchVisitor.h"
#include <algorithm>
#include "Scene.h"
#include "MediaSource.h"
#include "Session.h"
#include "SessionSource.h"
#include "SearchVisitor.h"
SearchVisitor::SearchVisitor(Node *node) : Visitor(), node_(node), found_(false)
{
@@ -52,7 +71,7 @@ SearchFileVisitor::SearchFileVisitor() : Visitor()
}
void SearchFileVisitor::visit(Node &n)
void SearchFileVisitor::visit(Node &)
{
}

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include "defines.h"

View File

@@ -1,6 +1,7 @@
#ifndef SELECTION_H
#define SELECTION_H
#include <string>
#include "SourceList.h"
class Selection

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include "defines.h"
@@ -5,20 +24,21 @@
#include "Source.h"
#include "Settings.h"
#include "FrameBuffer.h"
#include "Session.h"
#include "FrameGrabber.h"
#include "SessionCreator.h"
#include "SessionSource.h"
#include "MixingGroup.h"
#include "Log.h"
#include "Session.h"
SessionNote::SessionNote(const std::string &t, bool l, int s): label(std::to_string(BaseToolkit::uniqueId())),
text(t), large(l), stick(s), pos(glm::vec2(520.f, 30.f)), size(glm::vec2(220.f, 220.f))
{
}
Session::Session() : active_(true), filename_(""), failedSource_(nullptr), fading_target_(0.f)
Session::Session() : active_(true), activation_threshold_(MIXING_MIN_THRESHOLD),
filename_(""), failedSource_(nullptr), thumbnail_(nullptr)
{
config_[View::RENDERING] = new Group;
config_[View::RENDERING]->scale_ = glm::vec3(0.f);
@@ -40,6 +60,7 @@ Session::Session() : active_(true), filename_(""), failedSource_(nullptr), fadin
config_[View::TEXTURE]->translation_ = Settings::application.views[View::TEXTURE].default_translation;
snapshots_.xmlDoc_ = new tinyxml2::XMLDocument;
start_time_ = gst_util_get_timestamp ();
}
@@ -68,6 +89,11 @@ Session::~Session()
delete snapshots_.xmlDoc_;
}
uint64_t Session::runtime() const
{
return gst_util_get_timestamp () - start_time_;
}
void Session::setActive (bool on)
{
if (active_ != on) {
@@ -85,25 +111,28 @@ void Session::update(float dt)
if ( render_.frame() == nullptr )
return;
// pre-render of all sources
// pre-render all sources
failedSource_ = nullptr;
bool ready = true;
for( SourceList::iterator it = sources_.begin(); it != sources_.end(); ++it){
// ensure the RenderSource is rendering this session
RenderSource *s = dynamic_cast<RenderSource *>( *it );
if ( s!= nullptr && s->session() != this )
s->setSession(this);
// ensure the RenderSource is rendering *this* session
RenderSource *rs = dynamic_cast<RenderSource *>( *it );
if ( rs!= nullptr && rs->session() != this )
rs->setSession(this);
// discard failed source
if ( (*it)->failed() ) {
failedSource_ = (*it);
}
// render normally
else {
if ( !(*it)->ready() )
ready = false;
// render the source
(*it)->render();
// update the source
(*it)->setActive(activation_threshold_);
(*it)->update(dt);
}
}
@@ -121,10 +150,28 @@ void Session::update(float dt)
group_iter = deleteMixingGroup(group_iter);
}
// apply fading (smooth dicotomic reaching)
float f = render_.fading();
if ( ABS_DIFF(f, fading_target_) > EPSILON) {
render_.setFading( f + ( fading_target_ - f ) / 2.f);
// update fading requested
if (fading_.active) {
// animate
fading_.progress += dt;
// update animation
if ( fading_.duration > 0.f && fading_.progress < fading_.duration ) {
// interpolation
float f = fading_.progress / fading_.duration;
f = ( 1.f - f ) * fading_.start + f * fading_.target;
render_.setFading( f );
}
// arrived at target
else {
// set precise value
render_.setFading( fading_.target );
// fading finished
fading_.active = false;
fading_.start = fading_.target;
fading_.duration = fading_.progress = 0.f;
}
}
// update the scene tree
@@ -151,9 +198,9 @@ SourceList::iterator Session::addSource(Source *s)
// insert the source in the rendering
render_.scene.ws()->attach(s->group(View::RENDERING));
// insert the source to the beginning of the list
sources_.push_front(s);
sources_.push_back(s);
// return the iterator to the source created at the beginning
its = sources_.begin();
its = sources_.end()--;
}
// unlock access
@@ -228,6 +275,33 @@ Source *Session::popSource()
return s;
}
static void replaceThumbnail(Session *s)
{
if (s != nullptr) {
FrameBufferImage *t = s->renderThumbnail();
if (t != nullptr) // avoid recursive infinite loop
s->setThumbnail(t);
}
}
void Session::setThumbnail(FrameBufferImage *t)
{
resetThumbnail();
// replace with given image
if (t != nullptr)
thumbnail_ = t;
// no thumbnail image given: capture from rendering in a parallel thread
else
std::thread( replaceThumbnail, this ).detach();
}
void Session::resetThumbnail()
{
if (thumbnail_ != nullptr)
delete thumbnail_;
thumbnail_ = nullptr;
}
void Session::setResolution(glm::vec3 resolution, bool useAlpha)
{
// setup the render view: if not specified the default config resulution will be used
@@ -236,12 +310,17 @@ void Session::setResolution(glm::vec3 resolution, bool useAlpha)
config_[View::RENDERING]->scale_ = render_.resolution();
}
void Session::setFading(float f, bool forcenow)
void Session::setFadingTarget(float f, float duration)
{
if (forcenow)
render_.setFading( f );
fading_target_ = CLAMP(f, 0.f, 1.f);
// targetted fading value
fading_.target = CLAMP(f, 0.f, 1.f);
// starting point for interpolation
fading_.start = fading();
// initiate animation
fading_.progress = 0.f;
fading_.duration = duration;
// activate update
fading_.active = true;
}
SourceList::iterator Session::begin()
@@ -314,7 +393,7 @@ bool Session::empty() const
SourceList::iterator Session::at(int index)
{
if (index<0)
if ( index < 0 || index > (int) sources_.size())
return sources_.end();
int i = 0;
@@ -459,6 +538,59 @@ std::list<MixingGroup *>::iterator Session::endMixingGroup()
return mixing_groups_.end();
}
size_t Session::numPlayGroups() const
{
return play_groups_.size();
}
void Session::addPlayGroup(const SourceIdList &ids)
{
play_groups_.push_back( ids );
}
void Session::addToPlayGroup(size_t i, Source *s)
{
if (i < play_groups_.size() )
{
if ( std::find(play_groups_[i].begin(), play_groups_[i].end(), s->id()) == play_groups_[i].end() )
play_groups_[i].push_back(s->id());
}
}
void Session::removeFromPlayGroup(size_t i, Source *s)
{
if (i < play_groups_.size() )
{
if ( std::find(play_groups_[i].begin(), play_groups_[i].end(), s->id()) != play_groups_[i].end() )
play_groups_[i].remove( s->id() );
}
}
void Session::deletePlayGroup(size_t i)
{
if (i < play_groups_.size() )
play_groups_.erase( play_groups_.begin() + i);
}
SourceList Session::playGroup(size_t i) const
{
SourceList list;
if (i < play_groups_.size() )
{
for (auto sid = play_groups_[i].begin(); sid != play_groups_[i].end(); ++sid){
SourceList::const_iterator it = std::find_if(sources_.begin(), sources_.end(), Source::hasId( *sid));;
if ( it != sources_.end())
list.push_back( *it);
}
}
return list;
}
void Session::lock()
{
access_.lock();

View File

@@ -74,6 +74,7 @@ public:
// update all sources and mark sources which failed
void update (float dt);
uint64_t runtime() const;
// update mode (active or not)
void setActive (bool on);
@@ -85,15 +86,25 @@ public:
// get frame result of render
inline FrameBuffer *frame () const { return render_.frame(); }
// get thumbnail image
inline FrameBufferImage *thumbnail () { return render_.thumbnail(); }
// get an newly rendered thumbnail
inline FrameBufferImage *renderThumbnail () { return render_.thumbnail(); }
// get / set thumbnail image
inline FrameBufferImage *thumbnail () const { return thumbnail_; }
void setThumbnail(FrameBufferImage *t = nullptr);
void resetThumbnail();
// configure rendering resolution
void setResolution (glm::vec3 resolution, bool useAlpha = false);
// manipulate fading of output
void setFading (float f, bool forcenow = false);
inline float fading () const { return fading_target_; }
void setFadingTarget (float f, float duration = 0.f);
inline float fadingTarget () const { return fading_.target; }
inline float fading () const { return render_.fading(); }
// activation threshold for source (mixing distance)
inline void setActivationThreshold(float t) { activation_threshold_ = t; }
inline float activationThreshold() const { return activation_threshold_;}
// configuration for group nodes of views
inline Group *config (View::Mode m) const { return config_.at(m); }
@@ -124,7 +135,16 @@ public:
std::list<MixingGroup *>::iterator deleteMixingGroup (std::list<MixingGroup *>::iterator g);
// snapshots
SessionSnapshots * const snapshots () { return &snapshots_; }
SessionSnapshots * snapshots () { return &snapshots_; }
// playlists
void addPlayGroup(const SourceIdList &ids);
void deletePlayGroup(size_t i);
size_t numPlayGroups() const;
SourceList playGroup(size_t i) const;
void addToPlayGroup(size_t i, Source *s);
void removeFromPlayGroup(size_t i, Source *s);
std::vector<SourceIdList> getPlayGroups() { return play_groups_; }
// lock and unlock access (e.g. while saving)
void lock ();
@@ -132,6 +152,7 @@ public:
protected:
bool active_;
float activation_threshold_;
RenderView render_;
std::string filename_;
Source *failedSource_;
@@ -141,9 +162,28 @@ protected:
std::list<MixingGroup *> mixing_groups_;
std::map<View::Mode, Group*> config_;
SessionSnapshots snapshots_;
float fading_target_;
std::vector<SourceIdList> play_groups_;
std::mutex access_;
FrameBufferImage *thumbnail_;
uint64_t start_time_;
struct Fading
{
bool active;
float start;
float target;
float duration;
float progress;
Fading() {
active = false;
start = 0.f;
target = 0.f;
duration = 0.f;
progress = 0.f;
}
};
Fading fading_;
};

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <sstream>
#include "Log.h"
@@ -13,6 +32,7 @@
#include "DeviceSource.h"
#include "NetworkSource.h"
#include "MultiFileSource.h"
#include "StreamSource.h"
#include "Session.h"
#include "ImageShader.h"
#include "ImageProcessingShader.h"
@@ -54,7 +74,15 @@ SessionInformation SessionCreator::info(const std::string& filename)
}
const XMLElement *session = doc.FirstChildElement("Session");
if (session != nullptr ) {
ret.thumbnail = XMLToImage(session);
const XMLElement *thumbnailelement = session->FirstChildElement("Thumbnail");
// if there is a user defined thumbnail, get it
if (thumbnailelement) {
ret.thumbnail = XMLToImage(thumbnailelement);
ret.user_thumbnail_ = true;
}
// otherwise get the default saved thumbnail in session
else
ret.thumbnail = XMLToImage(session);
}
}
}
@@ -99,7 +127,9 @@ void SessionCreator::load(const std::string& filename)
loadConfig( xmlDoc_.FirstChildElement("Views") );
// ready to read sources
SessionLoader::load( xmlDoc_.FirstChildElement("Session") );
sessionFilePath_ = SystemToolkit::path_filename(filename);
XMLElement *sessionNode = xmlDoc_.FirstChildElement("Session");
SessionLoader::load( sessionNode );
// create groups
std::list< SourceList > groups = getMixingGroups();
@@ -112,6 +142,18 @@ void SessionCreator::load(const std::string& filename)
// load notes
loadNotes( xmlDoc_.FirstChildElement("Notes") );
// load playlists
loadPlayGroups( xmlDoc_.FirstChildElement("PlayGroups") );
// thumbnail
const XMLElement *thumbnailelement = sessionNode->FirstChildElement("Thumbnail");
// if there is a user-defined thumbnail, get it
if (thumbnailelement) {
FrameBufferImage *thumbnail = XMLToImage(thumbnailelement);
if (thumbnail != nullptr)
session_->setThumbnail( thumbnail );
}
// all good
session_->setFilename(filename);
}
@@ -163,7 +205,8 @@ void SessionCreator::loadNotes(XMLElement *notesNode)
XMLElement *sizeNode = note->FirstChildElement("size");
if (sizeNode) tinyxml2::XMLElementToGLM( sizeNode->FirstChildElement("vec2"), N.size);
XMLElement* contentNode = note->FirstChildElement("text");
if (contentNode) N.text = std::string ( contentNode->GetText() );
if (contentNode && contentNode->GetText())
N.text = std::string ( contentNode->GetText() );
session_->addNote(N);
}
@@ -171,6 +214,29 @@ void SessionCreator::loadNotes(XMLElement *notesNode)
}
}
void SessionCreator::loadPlayGroups(tinyxml2::XMLElement *playgroupNode)
{
if (playgroupNode != nullptr && session_ != nullptr) {
XMLElement* playgroup = playgroupNode->FirstChildElement("PlayGroup");
for( ; playgroup ; playgroup = playgroup->NextSiblingElement())
{
SourceIdList playgroup_sources;
XMLElement* playgroupSourceNode = playgroup->FirstChildElement("source");
for ( ; playgroupSourceNode ; playgroupSourceNode = playgroupSourceNode->NextSiblingElement()) {
uint64_t id__ = 0;
playgroupSourceNode->QueryUnsigned64Attribute("id", &id__);
if (sources_id_.count(id__) > 0)
playgroup_sources.push_back( id__ );
}
session_->addPlayGroup( playgroup_sources );
}
}
}
SessionLoader::SessionLoader(): Visitor(),
session_(nullptr), xmlCurrent_(nullptr), recursion_(0)
{
@@ -226,8 +292,18 @@ void SessionLoader::load(XMLElement *sessionNode)
return;
}
if (sessionNode != nullptr && session_ != nullptr) {
if (sessionNode != nullptr && session_ != nullptr)
{
//
// session attributes
//
float t = MIXING_MIN_THRESHOLD;
sessionNode->QueryFloatAttribute("activationThreshold", &t);
session_->setActivationThreshold(t);
//
// source lists
//
XMLElement* sourceNode = sessionNode->FirstChildElement("Source");
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
{
@@ -271,6 +347,9 @@ void SessionLoader::load(XMLElement *sessionNode)
else if ( std::string(pType) == "MultiFileSource") {
load_source = new MultiFileSource(id_xml_);
}
else if ( std::string(pType) == "GenericStreamSource") {
load_source = new GenericStreamSource(id_xml_);
}
// skip failed (including clones)
if (!load_source)
@@ -317,8 +396,13 @@ void SessionLoader::load(XMLElement *sessionNode)
SourceList::iterator origin;
if (id_origin_ > 0)
origin = session_->find(id_origin_);
else
origin = session_->find( std::string ( originNode->GetText() ) );
else {
const char *text = originNode->GetText();
if (text)
origin = session_->find( std::string(text) );
else
origin = session_->end();
}
// found the orign source
if (origin != session_->end()) {
// create a new source of type Clone
@@ -391,6 +475,9 @@ Source *SessionLoader::createSource(tinyxml2::XMLElement *sourceNode, Mode mode)
else if ( std::string(pType) == "MultiFileSource") {
load_source = new MultiFileSource(id__);
}
else if ( std::string(pType) == "GenericStreamSource") {
load_source = new GenericStreamSource(id__);
}
else if ( std::string(pType) == "CloneSource") {
// clone from given origin
XMLElement* originNode = xmlCurrent_->FirstChildElement("origin");
@@ -652,6 +739,14 @@ void SessionLoader::visit(MediaPlayer &n)
mediaplayerNode->QueryBoolAttribute("software_decoding", &gpudisable);
n.setSoftwareDecodingForced(gpudisable);
bool rewind_on_disabled = false;
mediaplayerNode->QueryBoolAttribute("rewind_on_disabled", &rewind_on_disabled);
n.setRewindOnDisabled(rewind_on_disabled);
int sync_to_metronome = 0;
mediaplayerNode->QueryIntAttribute("sync_to_metronome", &sync_to_metronome);
n.setSyncToMetronome( (Metronome::Synchronicity) sync_to_metronome);
bool play = true;
mediaplayerNode->QueryBoolAttribute("play", &play);
n.play(play);
@@ -802,12 +897,27 @@ void SessionLoader::visit (Source& s)
void SessionLoader::visit (MediaSource& s)
{
// set uri
XMLElement* uriNode = xmlCurrent_->FirstChildElement("uri");
if (uriNode) {
std::string uri = std::string ( uriNode->GetText() );
// load only new files
if ( uri != s.path() )
s.setPath(uri);
XMLElement* pathNode = xmlCurrent_->FirstChildElement("uri"); // TODO change to "path" but keep backward compatibility
if (pathNode) {
const char * text = pathNode->GetText();
if (text) {
std::string path(text);
// load only new files
if ( path.compare(s.path()) != 0 ) {
if ( !SystemToolkit::file_exists(path)){
const char * relative;
if ( pathNode->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
std::string rel = SystemToolkit::path_absolute_from_path(std::string( relative ), sessionFilePath_);
Log::Info("File %s not found; Trying %s instead.", path.c_str(), rel.c_str());
path = rel;
}
}
s.setPath(path);
}
}
// ensures the source is initialized even if no valid path is given
else
s.setPath("");
}
// set config media player
@@ -819,14 +929,26 @@ void SessionLoader::visit (SessionFileSource& s)
// set fading
float f = 0.f;
xmlCurrent_->QueryFloatAttribute("fading", &f);
s.session()->setFading(f);
s.session()->setFadingTarget(f);
// set uri
XMLElement* pathNode = xmlCurrent_->FirstChildElement("path");
if (pathNode) {
std::string path = std::string ( pathNode->GetText() );
// load only new files
if ( path != s.path() )
s.load(path, recursion_ + 1);
const char * text = pathNode->GetText();
if (text) {
std::string path(text);
// load only new files
if ( path != s.path() ) {
if ( !SystemToolkit::file_exists(path)){
const char * relative;
if ( pathNode->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
std::string rel = SystemToolkit::path_absolute_from_path(std::string( relative ), sessionFilePath_);
Log::Info("File %s not found; Trying %s instead.", path.c_str(), rel.c_str());
path = rel;
}
}
s.load(path, recursion_ + 1);
}
}
}
}
@@ -894,40 +1016,68 @@ void SessionLoader::visit (MultiFileSource& s)
if (seq) {
MultiFileSequence sequence;
sequence.location = std::string ( seq->GetText() );
seq->QueryIntAttribute("min", &sequence.min);
seq->QueryIntAttribute("max", &sequence.max);
seq->QueryUnsignedAttribute("width", &sequence.width);
seq->QueryUnsignedAttribute("height", &sequence.height);
const char *codec = seq->Attribute("codec");
if (codec)
sequence.codec = std::string(codec);
uint fps = 0;
seq->QueryUnsignedAttribute("fps", &fps);
const char *text = seq->GetText();
if (text) {
sequence.location = std::string (text);
// different sequence
if ( sequence != s.sequence() ) {
s.setSequence( sequence, fps);
// fix path if absolute path is not found
std::string folder = SystemToolkit::path_filename(sequence.location);
std::string dir = SystemToolkit::path_directory(folder);
if ( dir.empty() ){
const char * relative;
if ( seq->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
std::string rel = SystemToolkit::path_absolute_from_path(std::string(relative), sessionFilePath_);
Log::Info("Folder %s not found; Trying %s instead.", folder.c_str(), rel.c_str());
sequence.location = rel;
}
}
// set sequence parameters
seq->QueryIntAttribute("min", &sequence.min);
seq->QueryIntAttribute("max", &sequence.max);
seq->QueryUnsignedAttribute("width", &sequence.width);
seq->QueryUnsignedAttribute("height", &sequence.height);
const char *codec = seq->Attribute("codec");
if (codec)
sequence.codec = std::string(codec);
uint fps = 0;
seq->QueryUnsignedAttribute("fps", &fps);
// different sequence
if ( sequence != s.sequence() ) {
s.setSequence( sequence, fps);
}
// same sequence, different framerate
else if ( fps != s.framerate() ) {
s.setFramerate( fps );
}
int begin = -1;
seq->QueryIntAttribute("begin", &begin);
int end = INT_MAX;
seq->QueryIntAttribute("end", &end);
if ( begin != s.begin() || end != s.end() )
s.setRange(begin, end);
bool loop = true;
seq->QueryBoolAttribute("loop", &loop);
if ( loop != s.loop() )
s.setLoop(loop);
}
// same sequence, different framerate
else if ( fps != s.framerate() ) {
s.setFramerate( fps );
}
int begin = -1;
seq->QueryIntAttribute("begin", &begin);
int end = INT_MAX;
seq->QueryIntAttribute("end", &end);
if ( begin != s.begin() || end != s.end() )
s.setRange(begin, end);
bool loop = true;
seq->QueryBoolAttribute("loop", &loop);
if ( loop != s.loop() )
s.setLoop(loop);
}
}
void SessionLoader::visit (GenericStreamSource& s)
{
XMLElement* desc = xmlCurrent_->FirstChildElement("Description");
if (desc) {
const char * text = desc->GetText();
if (text)
s.setDescription(text);
}
}

View File

@@ -59,6 +59,7 @@ public:
void visit (DeviceSource& s) override;
void visit (NetworkSource& s) override;
void visit (MultiFileSource& s) override;
void visit (GenericStreamSource& s) override;
static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n);
static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s);
@@ -67,6 +68,7 @@ public:
protected:
// result created session
Session *session_;
std::string sessionFilePath_;
// parsing current xml
tinyxml2::XMLElement *xmlCurrent_;
// level of loading recursion
@@ -81,9 +83,11 @@ protected:
struct SessionInformation {
std::string description;
FrameBufferImage *thumbnail;
bool user_thumbnail_;
SessionInformation() {
description = "";
thumbnail = nullptr;
user_thumbnail_ = false;
}
};
@@ -93,6 +97,7 @@ class SessionCreator : public SessionLoader {
void loadConfig(tinyxml2::XMLElement *viewsNode);
void loadNotes(tinyxml2::XMLElement *notesNode);
void loadPlayGroups(tinyxml2::XMLElement *playlistsNode);
void loadSnapshots(tinyxml2::XMLElement *snapshotNode);
public:

129
SessionParser.cpp Normal file
View File

@@ -0,0 +1,129 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "SystemToolkit.h"
#include "tinyxml2Toolkit.h"
using namespace tinyxml2;
#include "SessionParser.h"
SessionParser::SessionParser()
{
}
bool SessionParser::open(const std::string &filename)
{
// if the file exists
if (filename.empty() || !SystemToolkit::file_exists(filename))
return false;
// try to load the file
xmlDoc_.Clear();
XMLError eResult = xmlDoc_.LoadFile(filename.c_str());
// error
if ( XMLResultError(eResult, false) )
return false;
filename_ = filename;
return true;
}
bool SessionParser::save()
{
if (filename_.empty())
return false;
// save file to disk
return ( XMLSaveDoc(&xmlDoc_, filename_) );
}
std::map< uint64_t, std::pair<std::string, bool> > SessionParser::pathList() const
{
std::map< uint64_t, std::pair<std::string, bool> > paths;
// fill path list
const XMLElement *session = xmlDoc_.FirstChildElement("Session");
if (session != nullptr ) {
const XMLElement *sourceNode = session->FirstChildElement("Source");
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
{
// get id
uint64_t sid = 0;
sourceNode->QueryUnsigned64Attribute("id", &sid);
// get path
const XMLElement* pathNode = nullptr;
pathNode = sourceNode->FirstChildElement("uri");
if (!pathNode)
pathNode = sourceNode->FirstChildElement("path");
if (!pathNode)
pathNode = sourceNode->FirstChildElement("Sequence");
if (pathNode) {
const char *text = pathNode->GetText();
if (text) {
bool exists = SystemToolkit::file_exists(text);
paths[sid] = std::pair<std::string, bool>(std::string(text), exists);
}
}
}
}
// return path list
return paths;
}
void SessionParser::replacePath(uint64_t id, const std::string &path)
{
XMLElement *session = xmlDoc_.FirstChildElement("Session");
if (session != nullptr ) {
XMLElement *sourceNode = session->FirstChildElement("Source");
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
{
// get id
uint64_t sid = 0;
sourceNode->QueryUnsigned64Attribute("id", &sid);
if (sid == id) {
// get path
XMLElement* pathNode = nullptr;
pathNode = sourceNode->FirstChildElement("uri");
if (!pathNode)
pathNode = sourceNode->FirstChildElement("path");
if (!pathNode)
pathNode = sourceNode->FirstChildElement("Sequence");
if (pathNode) {
XMLText *text = xmlDoc_.NewText( path.c_str() );
pathNode->InsertEndChild( text );
}
break;
}
}
}
}

40
SessionParser.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef SESSIONPARSER_H
#define SESSIONPARSER_H
#include <map>
#include <string>
#include <tinyxml2.h>
class Session;
//struct SessionInformation {
// std::string description;
// FrameBufferImage *thumbnail;
// bool user_thumbnail_;
// SessionInformation() {
// description = "";
// thumbnail = nullptr;
// user_thumbnail_ = false;
// }
//};
class SessionParser
{
public:
SessionParser();
bool open(const std::string& filename);
bool save();
std::map<uint64_t, std::pair<std::string, bool> > pathList() const;
void replacePath(uint64_t id, const std::string &path);
// static SessionInformation info(const std::string& filename);
private:
tinyxml2::XMLDocument xmlDoc_;
std::string filename_;
};
#endif // SESSIONPARSER_H

View File

@@ -1,9 +1,25 @@
#include <glm/gtc/matrix_transform.hpp>
#include <thread>
#include <chrono>
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include "SessionSource.h"
#include <glm/gtc/matrix_transform.hpp>
#include "defines.h"
#include "Log.h"
@@ -17,8 +33,9 @@
#include "SessionCreator.h"
#include "Mixer.h"
#include "SessionSource.h"
SessionSource::SessionSource(uint64_t id) : Source(id), failed_(false)
SessionSource::SessionSource(uint64_t id) : Source(id), failed_(false), timer_(0), paused_(false)
{
session_ = new Session;
}
@@ -62,12 +79,21 @@ uint SessionSource::texture() const
}
void SessionSource::setActive (bool on)
{
{
Source::setActive(on);
// change status of session (recursive change of internal sources)
if (session_ != nullptr)
if (session_) {
session_->setActive(active_);
// change visibility of active surface (show preview of session when inactive)
if (activesurface_) {
if (active_)
activesurface_->setTextureIndex(Resource::getTextureTransparent());
else
activesurface_->setTextureIndex(session_->frame()->texture());
}
}
}
void SessionSource::update(float dt)
@@ -76,8 +102,10 @@ void SessionSource::update(float dt)
return;
// update content
if (active_)
if (active_ && !paused_) {
session_->update(dt);
timer_ += guint64(dt * 1000.f) * GST_USECOND;
}
// delete a source which failed
if (session_->failedSource() != nullptr) {
@@ -90,6 +118,14 @@ void SessionSource::update(float dt)
Source::update(dt);
}
void SessionSource::replay ()
{
if (session_) {
for( SourceList::iterator it = session_->begin(); it != session_->end(); ++it)
(*it)->replay();
timer_ = 0;
}
}
SessionFileSource::SessionFileSource(uint64_t id) : SessionSource(id), path_(""), initialized_(false), wait_for_sources_(false)
{
@@ -150,6 +186,7 @@ void SessionFileSource::load(const std::string &p, uint recursion)
}
// will be ready after init and one frame rendered
initialized_ = false;
ready_ = false;
}
@@ -242,6 +279,16 @@ void SessionFileSource::accept(Visitor& v)
v.visit(*this);
}
glm::ivec2 SessionFileSource::icon() const
{
return glm::ivec2(ICON_SOURCE_SESSION);
}
std::string SessionFileSource::info() const
{
return std::string("session vimix '") + path_ + "'";
}
SessionGroupSource::SessionGroupSource(uint64_t id) : SessionSource(id), resolution_(glm::vec3(0.f))
{
@@ -298,7 +345,7 @@ void SessionGroupSource::init()
++View::need_deep_update_;
// done init
Log::Info("Source Group (%d x %d).", int(renderbuffer->resolution().x), int(renderbuffer->resolution().y) );
Log::Info("Source Group created (%d x %d).", int(renderbuffer->resolution().x), int(renderbuffer->resolution().y) );
}
}
@@ -323,6 +370,19 @@ void SessionGroupSource::accept(Visitor& v)
v.visit(*this);
}
glm::ivec2 SessionGroupSource::icon() const
{
return glm::ivec2(ICON_SOURCE_GROUP);
}
std::string SessionGroupSource::info() const
{
if (session_)
return std::string("group of ") + std::to_string(session_->numSource()) + " sources";
else
return std::string("undefined group.");
}
RenderSource::RenderSource(uint64_t id) : Source(id), session_(nullptr)
{
// set symbol
@@ -332,7 +392,7 @@ RenderSource::RenderSource(uint64_t id) : Source(id), session_(nullptr)
bool RenderSource::failed() const
{
if ( mode_ > Source::UNINITIALIZED && session_!=nullptr )
if ( renderbuffer_ != nullptr && session_ != nullptr )
return renderbuffer_->resolution() != session_->frame()->resolution();
return false;
@@ -372,7 +432,7 @@ void RenderSource::init()
glm::vec3 RenderSource::resolution() const
{
if (mode_ > Source::UNINITIALIZED)
if (renderbuffer_ != nullptr)
return renderbuffer_->resolution();
else if (session_ && session_->frame())
return session_->frame()->resolution();
@@ -386,3 +446,13 @@ void RenderSource::accept(Visitor& v)
// if (!failed())
v.visit(*this);
}
glm::ivec2 RenderSource::icon() const
{
return glm::ivec2(ICON_SOURCE_RENDER);
}
std::string RenderSource::info() const
{
return std::string("Render loopback");
}

View File

@@ -14,6 +14,11 @@ public:
// implementation of source API
void update (float dt) override;
void setActive (bool on) override;
bool playing () const override { return !paused_; }
void play (bool on) override { paused_ = !on; }
bool playable () const override { return true; }
guint64 playtime () const override { return timer_; }
void replay () override;
bool failed () const override;
uint texture () const override;
@@ -24,6 +29,8 @@ protected:
Session *session_;
std::atomic<bool> failed_;
guint64 timer_;
bool paused_;
};
class SessionFileSource : public SessionSource
@@ -39,7 +46,9 @@ public:
void load(const std::string &p = "", uint recursion = 0);
inline std::string path() const { return path_; }
glm::ivec2 icon() const override { return glm::ivec2(19, 6); }
glm::ivec2 icon() const override;
std::string info() const override;
protected:
@@ -65,7 +74,8 @@ public:
// import a source
bool import(Source *source);
glm::ivec2 icon() const override { return glm::ivec2(10, 6); }
glm::ivec2 icon() const override;
std::string info() const override;
protected:
@@ -80,6 +90,9 @@ public:
RenderSource(uint64_t id = 0);
// implementation of source API
bool playing () const override { return true; }
void play (bool) override {}
bool playable () const override { return false; }
bool failed () const override;
uint texture() const override;
void accept (Visitor& v) override;
@@ -89,7 +102,8 @@ public:
glm::vec3 resolution() const;
glm::ivec2 icon() const override { return glm::ivec2(0, 2); }
glm::ivec2 icon() const override;
std::string info() const override;
protected:

View File

@@ -1,4 +1,27 @@
#include "SessionVisitor.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <iostream>
#include <locale>
#include <tinyxml2.h>
using namespace tinyxml2;
#include "Log.h"
#include "defines.h"
@@ -19,11 +42,7 @@
#include "SystemToolkit.h"
#include "ActionManager.h"
#include <iostream>
#include <locale>
#include <tinyxml2.h>
using namespace tinyxml2;
#include "SessionVisitor.h"
bool SessionVisitor::saveSession(const std::string& filename, Session *session)
@@ -46,75 +65,139 @@ bool SessionVisitor::saveSession(const std::string& filename, Session *session)
XMLElement *sessionNode = xmlDoc.NewElement("Session");
xmlDoc.InsertEndChild(sessionNode);
SessionVisitor sv(&xmlDoc, sessionNode);
sv.sessionFilePath_ = SystemToolkit::path_filename(filename);
for (auto iter = session->begin(); iter != session->end(); ++iter, sv.setRoot(sessionNode) )
// source visitor
(*iter)->accept(sv);
// get the thumbnail
// save session attributes
sessionNode->SetAttribute("activationThreshold", session->activationThreshold());
// save the thumbnail
FrameBufferImage *thumbnail = session->thumbnail();
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
if (imageelement)
sessionNode->InsertEndChild(imageelement);
delete thumbnail;
if (thumbnail != nullptr && thumbnail->width > 0 && thumbnail->height > 0) {
XMLElement *thumbnailelement = xmlDoc.NewElement("Thumbnail");
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
if (imageelement) {
sessionNode->InsertEndChild(thumbnailelement);
thumbnailelement->InsertEndChild(imageelement);
}
}
// if no thumbnail is set by user, capture thumbnail now
else {
thumbnail = session->renderThumbnail();
if (thumbnail) {
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
if (imageelement)
sessionNode->InsertEndChild(imageelement);
delete thumbnail;
}
}
// 2. config of views
XMLElement *views = xmlDoc.NewElement("Views");
xmlDoc.InsertEndChild(views);
{
XMLElement *mixing = xmlDoc.NewElement( "Mixing" );
mixing->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::MIXING), &xmlDoc));
views->InsertEndChild(mixing);
XMLElement *geometry = xmlDoc.NewElement( "Geometry" );
geometry->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::GEOMETRY), &xmlDoc));
views->InsertEndChild(geometry);
XMLElement *layer = xmlDoc.NewElement( "Layer" );
layer->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::LAYER), &xmlDoc));
views->InsertEndChild(layer);
XMLElement *appearance = xmlDoc.NewElement( "Texture" );
appearance->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::TEXTURE), &xmlDoc));
views->InsertEndChild(appearance);
XMLElement *render = xmlDoc.NewElement( "Rendering" );
render->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::RENDERING), &xmlDoc));
views->InsertEndChild(render);
}
saveConfig( &xmlDoc, session );
// 3. snapshots
XMLElement *snapshots = xmlDoc.NewElement("Snapshots");
const XMLElement* N = session->snapshots()->xmlDoc_->FirstChildElement();
for( ; N ; N=N->NextSiblingElement())
snapshots->InsertEndChild( N->DeepClone( &xmlDoc ));
xmlDoc.InsertEndChild(snapshots);
saveSnapshots( &xmlDoc, session );
// 4. optional notes
XMLElement *notes = xmlDoc.NewElement("Notes");
xmlDoc.InsertEndChild(notes);
for (auto nit = session->beginNotes(); nit != session->endNotes(); ++nit) {
XMLElement *note = xmlDoc.NewElement( "Note" );
note->SetAttribute("large", (*nit).large );
note->SetAttribute("stick", (*nit).stick );
XMLElement *pos = xmlDoc.NewElement("pos");
pos->InsertEndChild( XMLElementFromGLM(&xmlDoc, (*nit).pos) );
note->InsertEndChild(pos);
XMLElement *size = xmlDoc.NewElement("size");
size->InsertEndChild( XMLElementFromGLM(&xmlDoc, (*nit).size) );
note->InsertEndChild(size);
XMLElement *content = xmlDoc.NewElement("text");
XMLText *text = xmlDoc.NewText( (*nit).text.c_str() );
content->InsertEndChild( text );
note->InsertEndChild(content);
notes->InsertEndChild(note);
}
saveNotes( &xmlDoc, session );
// 5. optional playlists
savePlayGroups( &xmlDoc, session );
// save file to disk
return ( XMLSaveDoc(&xmlDoc, filename) );
}
void SessionVisitor::saveConfig(tinyxml2::XMLDocument *doc, Session *session)
{
if (doc != nullptr && session != nullptr)
{
XMLElement *views = doc->NewElement("Views");
XMLElement *mixing = doc->NewElement( "Mixing" );
mixing->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::MIXING), doc));
views->InsertEndChild(mixing);
XMLElement *geometry = doc->NewElement( "Geometry" );
geometry->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::GEOMETRY), doc));
views->InsertEndChild(geometry);
XMLElement *layer = doc->NewElement( "Layer" );
layer->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::LAYER), doc));
views->InsertEndChild(layer);
XMLElement *appearance = doc->NewElement( "Texture" );
appearance->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::TEXTURE), doc));
views->InsertEndChild(appearance);
XMLElement *render = doc->NewElement( "Rendering" );
render->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::RENDERING), doc));
views->InsertEndChild(render);
doc->InsertEndChild(views);
}
}
void SessionVisitor::saveSnapshots(tinyxml2::XMLDocument *doc, Session *session)
{
if (doc != nullptr && session != nullptr)
{
XMLElement *snapshots = doc->NewElement("Snapshots");
const XMLElement* N = session->snapshots()->xmlDoc_->FirstChildElement();
for( ; N ; N=N->NextSiblingElement())
snapshots->InsertEndChild( N->DeepClone( doc ));
doc->InsertEndChild(snapshots);
}
}
void SessionVisitor::saveNotes(tinyxml2::XMLDocument *doc, Session *session)
{
if (doc != nullptr && session != nullptr)
{
XMLElement *notes = doc->NewElement("Notes");
for (auto nit = session->beginNotes(); nit != session->endNotes(); ++nit) {
XMLElement *note = doc->NewElement( "Note" );
note->SetAttribute("large", (*nit).large );
note->SetAttribute("stick", (*nit).stick );
XMLElement *pos = doc->NewElement("pos");
pos->InsertEndChild( XMLElementFromGLM(doc, (*nit).pos) );
note->InsertEndChild(pos);
XMLElement *size = doc->NewElement("size");
size->InsertEndChild( XMLElementFromGLM(doc, (*nit).size) );
note->InsertEndChild(size);
XMLElement *content = doc->NewElement("text");
XMLText *text = doc->NewText( (*nit).text.c_str() );
content->InsertEndChild( text );
note->InsertEndChild(content);
notes->InsertEndChild(note);
}
doc->InsertEndChild(notes);
}
}
void SessionVisitor::savePlayGroups(tinyxml2::XMLDocument *doc, Session *session)
{
if (doc != nullptr && session != nullptr)
{
XMLElement *playlistNode = doc->NewElement("PlayGroups");
std::vector<SourceIdList> pl = session->getPlayGroups();
for (auto plit = pl.begin(); plit != pl.end(); ++plit) {
XMLElement *list = doc->NewElement("PlayGroup");
playlistNode->InsertEndChild(list);
for (auto id = plit->begin(); id != plit->end(); ++id) {
XMLElement *sour = doc->NewElement("source");
sour->SetAttribute("id", *id);
list->InsertEndChild(sour);
}
}
doc->InsertEndChild(playlistNode);
}
}
SessionVisitor::SessionVisitor(tinyxml2::XMLDocument *doc,
tinyxml2::XMLElement *root,
bool recursive) : Visitor(), recursive_(recursive), xmlCurrent_(root)
@@ -263,14 +346,6 @@ void SessionVisitor::visit(FrameBufferSurface &)
xmlCurrent_->SetAttribute("type", "FrameBufferSurface");
}
void SessionVisitor::visit(MediaSurface &n)
{
// Node of a different type
xmlCurrent_->SetAttribute("type", "MediaSurface");
n.mediaPlayer()->accept(*this);
}
void SessionVisitor::visit(MediaPlayer &n)
{
XMLElement *newelement = xmlDoc_->NewElement("MediaPlayer");
@@ -281,6 +356,8 @@ void SessionVisitor::visit(MediaPlayer &n)
newelement->SetAttribute("loop", (int) n.loop());
newelement->SetAttribute("speed", n.playSpeed());
newelement->SetAttribute("software_decoding", n.softwareDecodingForced());
newelement->SetAttribute("rewind_on_disabled", n.rewindOnDisabled());
newelement->SetAttribute("sync_to_metronome", (int) n.syncToMetronome());
// timeline
XMLElement *timelineelement = xmlDoc_->NewElement("Timeline");
@@ -511,6 +588,9 @@ void SessionVisitor::visit (MediaSource& s)
XMLText *text = xmlDoc_->NewText( s.path().c_str() );
uri->InsertEndChild( text );
if (!sessionFilePath_.empty())
uri->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.path(), sessionFilePath_).c_str());
s.mediaplayer()->accept(*this);
}
@@ -524,6 +604,9 @@ void SessionVisitor::visit (SessionFileSource& s)
xmlCurrent_->InsertEndChild(path);
XMLText *text = xmlDoc_->NewText( s.path().c_str() );
path->InsertEndChild( text );
if (!sessionFilePath_.empty())
path->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.path(), sessionFilePath_).c_str());
}
void SessionVisitor::visit (SessionGroupSource& s)
@@ -610,12 +693,29 @@ void SessionVisitor::visit (MultiFileSource& s)
sequence->SetAttribute("width", s.sequence().width);
sequence->SetAttribute("height", s.sequence().height);
sequence->SetAttribute("codec", s.sequence().codec.c_str());
if (!sessionFilePath_.empty())
sequence->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.sequence().location, sessionFilePath_).c_str());
XMLText *location = xmlDoc_->NewText( s.sequence().location.c_str() );
sequence->InsertEndChild( location );
xmlCurrent_->InsertEndChild(sequence);
}
void SessionVisitor::visit (GenericStreamSource& s)
{
xmlCurrent_->SetAttribute("type", "GenericStreamSource");
XMLElement *desc = xmlDoc_->NewElement("Description");
XMLText *text = xmlDoc_->NewText( s.description().c_str() );
desc->InsertEndChild( text );
xmlCurrent_->InsertEndChild(desc);
}
std::string SessionVisitor::getClipboard(const SourceList &list)
{
std::string x = "";

View File

@@ -13,6 +13,12 @@ class SessionVisitor : public Visitor {
bool recursive_;
tinyxml2::XMLDocument *xmlDoc_;
tinyxml2::XMLElement *xmlCurrent_;
std::string sessionFilePath_;
static void saveConfig(tinyxml2::XMLDocument *doc, Session *session);
static void saveSnapshots(tinyxml2::XMLDocument *doc, Session *session);
static void saveNotes(tinyxml2::XMLDocument *doc, Session *session);
static void savePlayGroups(tinyxml2::XMLDocument *doc, Session *session);
public:
SessionVisitor(tinyxml2::XMLDocument *doc = nullptr,
@@ -35,7 +41,6 @@ public:
void visit (Primitive& n) override;
void visit (Surface&) override;
void visit (ImageSurface& n) override;
void visit (MediaSurface& n) override;
void visit (FrameBufferSurface&) override;
void visit (LineStrip& n) override;
void visit (LineSquare&) override;
@@ -61,6 +66,7 @@ public:
void visit (NetworkSource& s) override;
void visit (MixingGroup& s) override;
void visit (MultiFileSource& s) override;
void visit (GenericStreamSource& s) override;
static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc);
static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc);

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <iostream>
#include <locale>
@@ -8,15 +27,33 @@ using namespace std;
using namespace tinyxml2;
#include "defines.h"
#include "Settings.h"
#include "SystemToolkit.h"
#include "Settings.h"
Settings::Application Settings::application;
string settingsFilename = "";
static string settingsFilename = "";
void Settings::Save()
XMLElement *save_history(Settings::History &h, const char *nodename, XMLDocument &xmlDoc)
{
XMLElement *pElement = xmlDoc.NewElement( nodename );
pElement->SetAttribute("path", h.path.c_str());
pElement->SetAttribute("autoload", h.load_at_start);
pElement->SetAttribute("autosave", h.save_on_exit);
pElement->SetAttribute("valid", h.front_is_valid);
for(auto it = h.filenames.cbegin();
it != h.filenames.cend(); ++it) {
XMLElement *fileNode = xmlDoc.NewElement("path");
XMLText *text = xmlDoc.NewText( (*it).c_str() );
fileNode->InsertEndChild( text );
pElement->InsertFirstChild(fileNode);
}
return pElement;
}
void Settings::Save(uint64_t runtime)
{
// impose C locale for all app
setlocale(LC_ALL, "C");
@@ -31,6 +68,9 @@ void Settings::Save()
pRoot->SetAttribute("minor", VIMIX_VERSION_MINOR);
xmlDoc.InsertEndChild(pRoot);
#endif
// runtime
if (runtime>0)
pRoot->SetAttribute("runtime", runtime + application.total_runtime);
string comment = "Settings for " + application.name;
XMLComment *pComment = xmlDoc.NewComment(comment.c_str());
@@ -40,13 +80,13 @@ void Settings::Save()
{
XMLElement *windowsNode = xmlDoc.NewElement( "Windows" );
for (int i = 0; i < application.windows.size(); i++)
for (int i = 0; i < (int) application.windows.size(); ++i)
{
const Settings::WindowConfig& w = application.windows[i];
XMLElement *window = xmlDoc.NewElement( "Window" );
window->SetAttribute("id", i);
window->SetAttribute("name", w.name.c_str());
window->SetAttribute("id", i);
window->SetAttribute("x", w.x);
window->SetAttribute("y", w.y);
window->SetAttribute("w", w.w);
@@ -64,18 +104,24 @@ void Settings::Save()
applicationNode->SetAttribute("scale", application.scale);
applicationNode->SetAttribute("accent_color", application.accent_color);
applicationNode->SetAttribute("smooth_transition", application.smooth_transition);
applicationNode->SetAttribute("smooth_snapshot", application.smooth_snapshot);
applicationNode->SetAttribute("save_snapshot", application.save_version_snapshot);
applicationNode->SetAttribute("smooth_cursor", application.smooth_cursor);
applicationNode->SetAttribute("action_history_follow_view", application.action_history_follow_view);
applicationNode->SetAttribute("show_tooptips", application.show_tooptips);
applicationNode->SetAttribute("accept_connections", application.accept_connections);
applicationNode->SetAttribute("pannel_history_mode", application.pannel_history_mode);
applicationNode->SetAttribute("pannel_history_mode", application.pannel_current_session_mode);
applicationNode->SetAttribute("stream_low_bandwidth", application.stream_low_bandwidth);
pRoot->InsertEndChild(applicationNode);
// Widgets
XMLElement *widgetsNode = xmlDoc.NewElement( "Widgets" );
widgetsNode->SetAttribute("preview", application.widget.preview);
widgetsNode->SetAttribute("history", application.widget.history);
widgetsNode->SetAttribute("preview_view", application.widget.preview_view);
widgetsNode->SetAttribute("timer", application.widget.timer);
widgetsNode->SetAttribute("timer_view", application.widget.timer_view);
widgetsNode->SetAttribute("media_player", application.widget.media_player);
widgetsNode->SetAttribute("media_player_view", application.widget.media_player_view);
widgetsNode->SetAttribute("timeline_editmode", application.widget.timeline_editmode);
widgetsNode->SetAttribute("shader_editor", application.widget.shader_editor);
widgetsNode->SetAttribute("stats", application.widget.stats);
widgetsNode->SetAttribute("stats_mode", application.widget.stats_mode);
@@ -99,6 +145,11 @@ void Settings::Save()
RecordNode->SetAttribute("path", application.record.path.c_str());
RecordNode->SetAttribute("profile", application.record.profile);
RecordNode->SetAttribute("timeout", application.record.timeout);
RecordNode->SetAttribute("delay", application.record.delay);
RecordNode->SetAttribute("resolution_mode", application.record.resolution_mode);
RecordNode->SetAttribute("framerate_mode", application.record.framerate_mode);
RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode);
RecordNode->SetAttribute("priority_mode", application.record.priority_mode);
pRoot->InsertEndChild(RecordNode);
// Transition
@@ -170,44 +221,51 @@ void Settings::Save()
{
XMLElement *recent = xmlDoc.NewElement( "Recent" );
XMLElement *recentsession = xmlDoc.NewElement( "Session" );
recentsession->SetAttribute("path", application.recentSessions.path.c_str());
recentsession->SetAttribute("autoload", application.recentSessions.load_at_start);
recentsession->SetAttribute("autosave", application.recentSessions.save_on_exit);
recentsession->SetAttribute("valid", application.recentSessions.front_is_valid);
for(auto it = application.recentSessions.filenames.begin();
it != application.recentSessions.filenames.end(); ++it) {
XMLElement *fileNode = xmlDoc.NewElement("path");
XMLText *text = xmlDoc.NewText( (*it).c_str() );
fileNode->InsertEndChild( text );
recentsession->InsertFirstChild(fileNode);
};
recent->InsertEndChild(recentsession);
// recent session filenames
recent->InsertEndChild( save_history(application.recentSessions, "Session", xmlDoc));
XMLElement *recentfolder = xmlDoc.NewElement( "Folder" );
for(auto it = application.recentFolders.filenames.begin();
it != application.recentFolders.filenames.end(); ++it) {
XMLElement *fileNode = xmlDoc.NewElement("path");
XMLText *text = xmlDoc.NewText( (*it).c_str() );
fileNode->InsertEndChild( text );
recentfolder->InsertFirstChild(fileNode);
};
recent->InsertEndChild(recentfolder);
// recent session folders
recent->InsertEndChild( save_history(application.recentFolders, "Folder", xmlDoc));
XMLElement *recentmedia = xmlDoc.NewElement( "Import" );
recentmedia->SetAttribute("path", application.recentImport.path.c_str());
for(auto it = application.recentImport.filenames.begin();
it != application.recentImport.filenames.end(); ++it) {
XMLElement *fileNode = xmlDoc.NewElement("path");
XMLText *text = xmlDoc.NewText( (*it).c_str() );
fileNode->InsertEndChild( text );
recentmedia->InsertFirstChild(fileNode);
// recent import media uri
recent->InsertEndChild( save_history(application.recentImport, "Import", xmlDoc));
// recent import folders
recent->InsertEndChild( save_history(application.recentImportFolders, "ImportFolder", xmlDoc));
// recent recordings
recent->InsertEndChild( save_history(application.recentRecordings, "Record", xmlDoc));
// recent dialog path
XMLElement *recentdialogpath = xmlDoc.NewElement( "Dialog" );
for(auto it = application.dialogRecentFolder.cbegin();
it != application.dialogRecentFolder.cend(); ++it) {
XMLElement *pathNode = xmlDoc.NewElement("path");
pathNode->SetAttribute("label", (*it).first.c_str() );
XMLText *text = xmlDoc.NewText( (*it).second.c_str() );
pathNode->InsertEndChild( text );
recentdialogpath->InsertFirstChild(pathNode);
}
recent->InsertEndChild(recentmedia);
recent->InsertEndChild(recentdialogpath);
pRoot->InsertEndChild(recent);
}
// Metronome
XMLElement *timerConfNode = xmlDoc.NewElement( "Timer" );
timerConfNode->SetAttribute("mode", application.timer.mode);
timerConfNode->SetAttribute("link_enabled", application.timer.link_enabled);
timerConfNode->SetAttribute("link_tempo", application.timer.link_tempo);
timerConfNode->SetAttribute("link_quantum", application.timer.link_quantum);
timerConfNode->SetAttribute("link_start_stop_sync", application.timer.link_start_stop_sync);
timerConfNode->SetAttribute("stopwatch_duration", application.timer.stopwatch_duration);
pRoot->InsertEndChild(timerConfNode);
// Controller
XMLElement *controlConfNode = xmlDoc.NewElement( "Control" );
controlConfNode->SetAttribute("osc_port_receive", application.control.osc_port_receive);
controlConfNode->SetAttribute("osc_port_send", application.control.osc_port_send);
// First save : create filename
if (settingsFilename.empty())
@@ -218,14 +276,45 @@ void Settings::Save()
}
void load_history(Settings::History &h, const char *nodename, XMLElement *root)
{
XMLElement * pElement = root->FirstChildElement(nodename);
if (pElement)
{
// list of path
h.filenames.clear();
XMLElement* path = pElement->FirstChildElement("path");
for( ; path ; path = path->NextSiblingElement())
{
const char *p = path->GetText();
if (p)
h.push( std::string (p) );
}
// path attribute
const char *path_ = pElement->Attribute("path");
if (path_)
h.path = std::string(path_);
else
h.path = SystemToolkit::home_path();
// other attritutes
pElement->QueryBoolAttribute("autoload", &h.load_at_start);
pElement->QueryBoolAttribute("autosave", &h.save_on_exit);
pElement->QueryBoolAttribute("valid", &h.front_is_valid);
}
}
void Settings::Load()
{
// impose C locale for all app
setlocale(LC_ALL, "C");
// set filenames from settings path
application.control.osc_filename = SystemToolkit::full_filename(SystemToolkit::settings_path(), OSC_CONFIG_FILE);
settingsFilename = SystemToolkit::full_filename(SystemToolkit::settings_path(), APP_SETTINGS);
// try to load settings file
XMLDocument xmlDoc;
if (settingsFilename.empty())
settingsFilename = SystemToolkit::full_filename(SystemToolkit::settings_path(), APP_SETTINGS);
XMLError eResult = xmlDoc.LoadFile(settingsFilename.c_str());
// do not warn if non existing file
@@ -235,8 +324,10 @@ void Settings::Load()
else if (XMLResultError(eResult))
return;
// first element should be called by the application name
XMLElement *pRoot = xmlDoc.FirstChildElement(application.name.c_str());
if (pRoot == nullptr) return;
if (pRoot == nullptr)
return;
// cancel on different root name
if (application.name.compare( string( pRoot->Value() ) ) != 0 )
@@ -250,25 +341,33 @@ void Settings::Load()
if (version_major != VIMIX_VERSION_MAJOR || version_minor != VIMIX_VERSION_MINOR)
return;
#endif
// runtime
pRoot->QueryUnsigned64Attribute("runtime", &application.total_runtime);
XMLElement * applicationNode = pRoot->FirstChildElement("Application");
if (applicationNode != nullptr) {
applicationNode->QueryFloatAttribute("scale", &application.scale);
applicationNode->QueryIntAttribute("accent_color", &application.accent_color);
applicationNode->QueryBoolAttribute("smooth_transition", &application.smooth_transition);
applicationNode->QueryBoolAttribute("smooth_snapshot", &application.smooth_snapshot);
applicationNode->QueryBoolAttribute("save_snapshot", &application.save_version_snapshot);
applicationNode->QueryBoolAttribute("smooth_cursor", &application.smooth_cursor);
applicationNode->QueryBoolAttribute("action_history_follow_view", &application.action_history_follow_view);
applicationNode->QueryBoolAttribute("show_tooptips", &application.show_tooptips);
applicationNode->QueryBoolAttribute("accept_connections", &application.accept_connections);
applicationNode->QueryIntAttribute("pannel_history_mode", &application.pannel_history_mode);
applicationNode->QueryIntAttribute("pannel_history_mode", &application.pannel_current_session_mode);
applicationNode->QueryBoolAttribute("stream_low_bandwidth", &application.stream_low_bandwidth);
}
// Widgets
XMLElement * widgetsNode = pRoot->FirstChildElement("Widgets");
if (widgetsNode != nullptr) {
widgetsNode->QueryBoolAttribute("preview", &application.widget.preview);
widgetsNode->QueryBoolAttribute("history", &application.widget.history);
widgetsNode->QueryIntAttribute("preview_view", &application.widget.preview_view);
widgetsNode->QueryBoolAttribute("timer", &application.widget.timer);
widgetsNode->QueryIntAttribute("timer_view", &application.widget.timer_view);
widgetsNode->QueryBoolAttribute("media_player", &application.widget.media_player);
widgetsNode->QueryIntAttribute("media_player_view", &application.widget.media_player_view);
widgetsNode->QueryBoolAttribute("timeline_editmode", &application.widget.timeline_editmode);
widgetsNode->QueryBoolAttribute("shader_editor", &application.widget.shader_editor);
widgetsNode->QueryBoolAttribute("stats", &application.widget.stats);
widgetsNode->QueryIntAttribute("stats_mode", &application.widget.stats_mode);
@@ -292,7 +391,12 @@ void Settings::Load()
XMLElement * recordnode = pRoot->FirstChildElement("Record");
if (recordnode != nullptr) {
recordnode->QueryIntAttribute("profile", &application.record.profile);
recordnode->QueryFloatAttribute("timeout", &application.record.timeout);
recordnode->QueryUnsignedAttribute("timeout", &application.record.timeout);
recordnode->QueryIntAttribute("delay", &application.record.delay);
recordnode->QueryIntAttribute("resolution_mode", &application.record.resolution_mode);
recordnode->QueryIntAttribute("framerate_mode", &application.record.framerate_mode);
recordnode->QueryIntAttribute("buffering_mode", &application.record.buffering_mode);
recordnode->QueryIntAttribute("priority_mode", &application.record.priority_mode);
const char *path_ = recordnode->Attribute("path");
if (path_)
@@ -327,16 +431,18 @@ void Settings::Load()
for( ; windowNode ; windowNode=windowNode->NextSiblingElement())
{
Settings::WindowConfig w;
w.name = std::string(windowNode->Attribute("name"));
windowNode->QueryIntAttribute("x", &w.x); // If this fails, original value is left as-is
windowNode->QueryIntAttribute("y", &w.y);
windowNode->QueryIntAttribute("w", &w.w);
windowNode->QueryIntAttribute("h", &w.h);
windowNode->QueryBoolAttribute("f", &w.fullscreen);
w.monitor = std::string(windowNode->Attribute("m"));
const char *text = windowNode->Attribute("m");
if (text)
w.monitor = std::string(text);
int i = 0;
windowNode->QueryIntAttribute("id", &i);
w.name = application.windows[i].name; // keep only original name
application.windows[i] = w;
}
}
@@ -399,60 +505,56 @@ void Settings::Load()
if (pElement)
{
// recent session filenames
XMLElement * pSession = pElement->FirstChildElement("Session");
if (pSession)
{
const char *path_ = pSession->Attribute("path");
if (path_)
application.recentSessions.path = std::string(path_);
else
application.recentSessions.path = SystemToolkit::home_path();
application.recentSessions.filenames.clear();
XMLElement* path = pSession->FirstChildElement("path");
for( ; path ; path = path->NextSiblingElement())
{
const char *p = path->GetText();
if (p)
application.recentSessions.push( std::string (p) );
}
pSession->QueryBoolAttribute("autoload", &application.recentSessions.load_at_start);
pSession->QueryBoolAttribute("autosave", &application.recentSessions.save_on_exit);
pSession->QueryBoolAttribute("valid", &application.recentSessions.front_is_valid);
}
load_history(application.recentSessions, "Session", pElement);
// recent session folders
XMLElement * pFolder = pElement->FirstChildElement("Folder");
if (pFolder)
{
application.recentFolders.filenames.clear();
XMLElement* path = pFolder->FirstChildElement("path");
for( ; path ; path = path->NextSiblingElement())
{
const char *p = path->GetText();
if (p)
application.recentFolders.push( std::string (p) );
}
}
load_history(application.recentFolders, "Folder", pElement);
// recent media uri
XMLElement * pImport = pElement->FirstChildElement("Import");
if (pImport)
load_history(application.recentImport, "Import", pElement);
// recent import folders
load_history(application.recentImportFolders, "ImportFolder", pElement);
// recent recordings
load_history(application.recentRecordings, "Record", pElement);
// recent dialog path
XMLElement * pDialog = pElement->FirstChildElement("Dialog");
if (pDialog)
{
const char *path_ = pImport->Attribute("path");
if (path_)
application.recentImport.path = std::string(path_);
else
application.recentImport.path = SystemToolkit::home_path();
application.recentImport.filenames.clear();
XMLElement* path = pImport->FirstChildElement("path");
application.dialogRecentFolder.clear();
XMLElement* path = pDialog->FirstChildElement("path");
for( ; path ; path = path->NextSiblingElement())
{
const char *l = path->Attribute("label");
const char *p = path->GetText();
if (p)
application.recentImport.push( std::string (p) );
if (l && p)
application.dialogRecentFolder[ std::string(l)] = std::string (p);
}
}
}
}
// bloc metronome
XMLElement * timerconfnode = pRoot->FirstChildElement("Timer");
if (timerconfnode != nullptr) {
timerconfnode->QueryUnsigned64Attribute("mode", &application.timer.mode);
timerconfnode->QueryBoolAttribute("link_enabled", &application.timer.link_enabled);
timerconfnode->QueryDoubleAttribute("link_tempo", &application.timer.link_tempo);
timerconfnode->QueryDoubleAttribute("link_quantum", &application.timer.link_quantum);
timerconfnode->QueryBoolAttribute("link_start_stop_sync", &application.timer.link_start_stop_sync);
timerconfnode->QueryUnsigned64Attribute("stopwatch_duration", &application.timer.stopwatch_duration);
}
// bloc Controller
XMLElement *controlconfnode = pRoot->FirstChildElement("Control");
if (controlconfnode != nullptr) {
controlconfnode->QueryIntAttribute("osc_port_receive", &application.control.osc_port_receive);
controlconfnode->QueryIntAttribute("osc_port_send", &application.control.osc_port_send);
}
}
void Settings::History::push(const string &filename)
@@ -528,13 +630,11 @@ void Settings::Unlock()
void Settings::Check()
{
Settings::Save();
XMLDocument xmlDoc;
XMLError eResult = xmlDoc.LoadFile(settingsFilename.c_str());
if (XMLResultError(eResult))
if (XMLResultError(eResult)) {
return;
}
xmlDoc.Print();
}

View File

@@ -1,7 +1,9 @@
#ifndef __SETTINGS_H_
#define __SETTINGS_H_
#include "defines.h"
#ifdef __APPLE__
#include <sys/types.h>
#endif
#include <string>
#include <map>
@@ -9,6 +11,8 @@
#include <list>
#include <glm/glm.hpp>
#include "defines.h"
namespace Settings {
struct WidgetsConfig
@@ -18,11 +22,15 @@ struct WidgetsConfig
int stats_mode;
bool logs;
bool preview;
bool history;
int preview_view;
bool media_player;
bool media_player_view;
int media_player_view;
bool timer;
int timer_view;
bool timeline_editmode;
bool shader_editor;
bool toolbox;
bool help;
WidgetsConfig() {
stats = false;
@@ -30,11 +38,15 @@ struct WidgetsConfig
stats_corner = 1;
logs = false;
preview = false;
history = false;
preview_view = -1;
media_player = false;
media_player_view = true;
media_player_view = -1;
timeline_editmode = false;
shader_editor = false;
toolbox = false;
help = false;
timer = false;
timer_view = -1;
}
};
@@ -62,17 +74,26 @@ struct ViewConfig
};
#define RECORD_MAX_TIMEOUT 1800.f
struct RecordConfig
{
std::string path;
int profile;
float timeout;
uint timeout;
int delay;
int resolution_mode;
int framerate_mode;
int buffering_mode;
int priority_mode;
RecordConfig() : path("") {
profile = 0;
timeout = RECORD_MAX_TIMEOUT;
delay = 0;
resolution_mode = 1;
framerate_mode = 1;
buffering_mode = 2;
priority_mode = 1;
}
};
@@ -115,6 +136,7 @@ struct TransitionConfig
struct RenderConfig
{
bool disabled;
bool blit;
int vsync;
int multisampling;
@@ -124,6 +146,7 @@ struct RenderConfig
bool gpu_decoding;
RenderConfig() {
disabled = false;
blit = false;
vsync = 1;
multisampling = 2;
@@ -147,6 +170,36 @@ struct SourceConfig
}
};
struct TimerConfig
{
uint64_t mode;
bool link_enabled;
double link_tempo;
double link_quantum;
bool link_start_stop_sync;
uint64_t stopwatch_duration;
TimerConfig() {
mode = 0;
link_enabled = true;
link_tempo = 120.;
link_quantum = 4.;
link_start_stop_sync = true;
stopwatch_duration = 60;
}
};
struct ControllerConfig
{
int osc_port_receive;
int osc_port_send;
std::string osc_filename;
ControllerConfig() {
osc_port_receive = OSC_PORT_RECV_DEFAULT;
osc_port_send = OSC_PORT_SEND_DEFAULT;
}
};
struct Application
{
@@ -157,16 +210,18 @@ struct Application
// Verification
std::string name;
std::string executable;
uint64_t total_runtime;
// Global settings Application interface
float scale;
int accent_color;
bool smooth_snapshot;
bool save_version_snapshot;
bool smooth_transition;
bool smooth_cursor;
bool action_history_follow_view;
bool show_tooptips;
int pannel_history_mode;
int pannel_current_session_mode;
// connection settings
bool accept_connections;
@@ -187,6 +242,7 @@ struct Application
// settings exporters
RecordConfig record;
bool stream_low_bandwidth;
// settings new source
SourceConfig source;
@@ -194,6 +250,9 @@ struct Application
// settings transition
TransitionConfig transition;
// settings controller
ControllerConfig control;
// multiple windows handling
std::vector<WindowConfig> windows;
@@ -201,24 +260,32 @@ struct Application
History recentSessions;
History recentFolders;
History recentImport;
History recentImportFolders;
History recentRecordings;
std::map< std::string, std::string > dialogRecentFolder;
// Metronome & stopwatch
TimerConfig timer;
Application() : fresh_start(false), instance_id(0), name(APP_NAME), executable(APP_NAME) {
scale = 1.f;
accent_color = 0;
smooth_transition = false;
smooth_snapshot = false;
save_version_snapshot = false;
smooth_cursor = false;
action_history_follow_view = false;
show_tooptips = true;
accept_connections = false;
pannel_history_mode = 0;
pannel_current_session_mode = 0;
current_view = 1;
current_workspace= 1;
brush = glm::vec3(0.5f, 0.1f, 0.f);
stream_low_bandwidth = false;
windows = std::vector<WindowConfig>(3);
windows[0].name = APP_NAME APP_TITLE;
windows[0].name = APP_TITLE;
windows[0].w = 1600;
windows[0].h = 900;
windows[1].name = APP_NAME " -- Output";
windows[1].name = "Output " APP_TITLE;
}
};
@@ -229,7 +296,7 @@ struct Application
extern Application application;
// Save and Load store settings in XML file
void Save();
void Save(uint64_t runtime = 0);
void Load();
void Lock();
void Unlock();

View File

@@ -1,9 +1,21 @@
#include "Shader.h"
#include "Resource.h"
#include "FrameBuffer.h"
#include "Log.h"
#include "Visitor.h"
#include "RenderingManager.h"
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <fstream>
#include <sstream>
@@ -14,11 +26,19 @@
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "BaseToolkit.h"
#include <glm/gtc/type_ptr.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
#include "Resource.h"
#include "FrameBuffer.h"
#include "Log.h"
#include "Visitor.h"
#include "BaseToolkit.h"
#include "RenderingManager.h"
#include "Shader.h"
// Globals
ShadingProgram *ShadingProgram::currentProgram_ = nullptr;
ShadingProgram simpleShadingProgram("shaders/simple.vs", "shaders/simple.fs");
@@ -206,7 +226,7 @@ Shader::Shader() : blending(BLEND_OPACITY)
id_ = BaseToolkit::uniqueId();
program_ = &simpleShadingProgram;
reset();
Shader::reset();
}

View File

@@ -1,3 +1,22 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <algorithm>
#include <locale>
#include <tinyxml2.h>
@@ -97,7 +116,7 @@ SourceCore& SourceCore::operator= (SourceCore const& other)
Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(nullptr),
active_(true), locked_(false), need_update_(true), dt_(0), workspace_(STAGE)
active_(true), locked_(false), need_update_(true), dt_(16.f), workspace_(STAGE)
{
// create unique id
if (id_ == 0)
@@ -124,6 +143,16 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
frames_[View::MIXING]->attach(frame);
groups_[View::MIXING]->attach(frames_[View::MIXING]);
// Glyphs show letters from the intials, with Font index 4 (LARGE)
initial_0_ = new Glyph(4);
initial_0_->translation_ = glm::vec3(0.2f, 0.8f, 0.1f);
initial_0_->scale_.y = 0.2f;
groups_[View::MIXING]->attach(initial_0_);
initial_1_ = new Glyph(4);
initial_1_->translation_ = glm::vec3(0.4f, 0.8f, 0.1f);
initial_1_->scale_.y = 0.2f;
groups_[View::MIXING]->attach(initial_1_);
overlays_[View::MIXING] = new Group;
overlays_[View::MIXING]->translation_.z = 0.1;
overlays_[View::MIXING]->visible_ = false;
@@ -207,6 +236,9 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
frames_[View::LAYER]->attach(frame);
groups_[View::LAYER]->attach(frames_[View::LAYER]);
groups_[View::LAYER]->attach(initial_0_);
groups_[View::LAYER]->attach(initial_1_);
overlays_[View::LAYER] = new Group;
overlays_[View::LAYER]->translation_.z = 0.15;
overlays_[View::LAYER]->visible_ = false;
@@ -256,8 +288,10 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
// locker switch button : locked / unlocked icons
locker_ = new Switch;
lock_ = new Handles(Handles::LOCKED);
lock_->color.a = 0.6;
locker_->attach(lock_);
unlock_ = new Handles(Handles::UNLOCKED);
unlock_->color.a = 0.6;
locker_->attach(unlock_);
// simple image shader (with texturing) for blending
@@ -268,7 +302,7 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
// for drawing in mixing view
mixingshader_ = new ImageShader;
mixingshader_->stipple = 1.0;
mixingshader_->stipple = 1.0f;
mixinggroup_ = nullptr;
// create media surface:
@@ -281,6 +315,7 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
renderbuffer_ = nullptr;
rendersurface_ = nullptr;
mixingsurface_ = nullptr;
activesurface_ = nullptr;
maskbuffer_ = nullptr;
maskimage_ = nullptr;
mask_need_update_ = false;
@@ -313,15 +348,25 @@ Source::~Source()
overlays_.clear();
frames_.clear();
handles_.clear();
// clear and delete callbacks
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) {
SourceCallback *callback = *iter;
iter = update_callbacks_.erase(iter);
delete callback;
}
}
void Source::setName (const std::string &name)
{
if (!name.empty())
name_ = BaseToolkit::transliterate(name);
name_ = BaseToolkit::unspace( BaseToolkit::transliterate(name) );
initials_[0] = std::toupper( name_.front(), std::locale("C") );
initials_[1] = std::toupper( name_.back(), std::locale("C") );
initial_0_->setChar(initials_[0]);
initial_1_->setChar(initials_[1]);
}
void Source::accept(Visitor& v)
@@ -352,6 +397,10 @@ void Source::setMode(Source::Mode m)
for (auto o = overlays_.begin(); o != overlays_.end(); ++o)
(*o).second->visible_ = (current && !locked_);
// the opacity of the initials changes if current
initial_0_->color.w = current ? 1.0 : 0.7;
initial_1_->color.w = current ? 1.0 : 0.7;
// the lock icon
locker_->setActive( locked_ ? 0 : 1);
@@ -425,7 +474,6 @@ void Source::render()
}
}
void Source::attach(FrameBuffer *renderbuffer)
{
// invalid argument
@@ -437,12 +485,6 @@ void Source::attach(FrameBuffer *renderbuffer)
delete renderbuffer_;
renderbuffer_ = renderbuffer;
// if a symbol is available, add it to overlay
if (symbol_) {
overlays_[View::MIXING]->attach( symbol_ );
overlays_[View::LAYER]->attach( symbol_ );
}
// create the surfaces to draw the frame buffer in the views
rendersurface_ = new FrameBufferSurface(renderbuffer_, blendingshader_);
groups_[View::RENDERING]->attach(rendersurface_);
@@ -455,18 +497,27 @@ void Source::attach(FrameBuffer *renderbuffer)
groups_[View::LAYER]->attach(mixingsurface_);
// for views showing a scaled mixing surface, a dedicated transparent surface allows grabbing
Surface *surfacetmp = new Surface();
surfacetmp->setTextureIndex(Resource::getTextureTransparent());
groups_[View::TEXTURE]->attach(surfacetmp);
groups_[View::MIXING]->attach(surfacetmp);
groups_[View::LAYER]->attach(surfacetmp);
activesurface_ = new Surface();
activesurface_->setTextureIndex(Resource::getTextureTransparent());
groups_[View::TEXTURE]->attach(activesurface_);
groups_[View::MIXING]->attach(activesurface_);
groups_[View::LAYER]->attach(activesurface_);
// Transition group node is optionnal
if (groups_[View::TRANSITION]->numChildren() > 0)
groups_[View::TRANSITION]->attach(mixingsurface_);
// hack to place the symbols in the corner independently of aspect ratio
symbol_->translation_.x += 0.1f * (renderbuffer_->aspectRatio()-1.f);
// if a symbol is available, add it to overlay
if (symbol_) {
overlays_[View::MIXING]->attach( symbol_ );
overlays_[View::LAYER]->attach( symbol_ );
// hack to place the symbols in the corner independently of aspect ratio
symbol_->translation_.x += 0.1f * (renderbuffer_->aspectRatio()-1.f);
}
// hack to place the initials in the corner independently of aspect ratio
initial_0_->translation_.x -= renderbuffer_->aspectRatio();
initial_1_->translation_.x -= renderbuffer_->aspectRatio();
// add lock icon to views (displayed on front)
groups_[View::LAYER]->attach( locker_ );
@@ -499,6 +550,10 @@ void Source::attach(FrameBuffer *renderbuffer)
void Source::setActive (bool on)
{
// request update
need_update_ |= active_ != on;
// activate
active_ = on;
// do not disactivate if a clone depends on it
@@ -513,6 +568,11 @@ void Source::setActive (bool on)
groups_[View::LAYER]->visible_ = active_;
}
void Source::setActive (float threshold)
{
setActive( glm::length( glm::vec2(groups_[View::MIXING]->translation_) ) < threshold );
}
void Source::setLocked (bool on)
{
locked_ = on;
@@ -522,21 +582,27 @@ void Source::setLocked (bool on)
// Transfer functions from coordinates to alpha (1 - transparency)
// linear distance
float linear_(float x, float y) {
return 1.f - CLAMP( sqrt( ( x * x ) + ( y * y ) ), 0.f, 1.f );
}
//// linear distance
//float linear_(float x, float y) {
// return 1.f - CLAMP( sqrt( ( x * x ) + ( y * y ) ), 0.f, 1.f );
//}
// quadratic distance
float quad_(float x, float y) {
return 1.f - CLAMP( ( x * x ) + ( y * y ), 0.f, 1.f );
}
//// quadratic distance
//float quad_(float x, float y) {
// return 1.f - CLAMP( ( x * x ) + ( y * y ), 0.f, 1.f );
//}
// best alpha transfer function: quadratic sinusoidal shape
float sin_quad_(float x, float y) {
//// best alpha transfer function: quadratic sinusoidal shape
//float sin_quad_(float x, float y) {
// float D = sqrt( ( x * x ) + ( y * y ) );
// return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
//// return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ), 0.f, 1.f ) );
//}
float SourceCore::alphaFromCordinates(float x, float y)
{
float D = sqrt( ( x * x ) + ( y * y ) );
return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
// return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ), 0.f, 1.f ) );
}
@@ -545,38 +611,41 @@ float Source::depth() const
return group(View::RENDERING)->translation_.z;
}
void Source::setDepth(float d)
{
groups_[View::LAYER]->translation_.z = CLAMP(d, MIN_DEPTH, MAX_DEPTH);
touch();
}
float Source::alpha() const
{
return blendingShader()->color.a;
}
void Source::setAlpha(float a)
void Source::call(SourceCallback *callback, bool override)
{
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
glm::vec2 step = glm::normalize(glm::vec2(1.f, 1.f));// step in diagonal by default
if (callback != nullptr) {
// step in direction of source translation if possible
if ( glm::length(dist) > DELTA_ALPHA)
step = glm::normalize(dist);
// lock access to callbacks list
access_callbacks_.lock();
// converge to reduce the difference of alpha
// using dichotomic algorithm
float delta = sin_quad_(dist.x, dist.y) - CLAMP(a, 0.f, 1.f);
while ( glm::abs(delta) > DELTA_ALPHA ){
dist += step * (delta / 2.f);
delta = sin_quad_(dist.x, dist.y) - CLAMP(a, 0.f, 1.f);
// remove similar callbacks if override
if (override) {
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
{
// remove and delete all callbacks of same type
SourceCallback *c = *iter;
if (callback->type() == c->type() ) {
iter = update_callbacks_.erase(iter);
delete c;
}
// iterate
else
++iter;
}
}
// add callback to callbacks list
update_callbacks_.push_back(callback);
// release access to callbacks list
access_callbacks_.unlock();
}
// apply new mixing coordinates
groups_[View::MIXING]->translation_.x = dist.x;
groups_[View::MIXING]->translation_.y = dist.y;
touch();
}
void Source::update(float dt)
@@ -584,126 +653,153 @@ void Source::update(float dt)
// keep delta-t
dt_ = dt;
// update nodes if needed
if (renderbuffer_ && mixingsurface_ && maskbuffer_ && need_update_)
// if update is possible
if (renderbuffer_ && mixingsurface_ && maskbuffer_)
{
// ADJUST alpha based on MIXING node
// read position of the mixing node and interpret this as transparency of render output
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
// use the sinusoidal transfer function
blendingshader_->color = glm::vec4(1.f, 1.f, 1.f, sin_quad_( dist.x, dist.y ));
mixingshader_->color = blendingshader_->color;
// CHANGE update status based on limbo
bool a = glm::length(dist) < MIXING_LIMBO_SCALE;
setActive( a );
// adjust scale of mixing icon : smaller if not active
groups_[View::MIXING]->scale_ = glm::vec3(MIXING_ICON_SCALE) - ( a ? glm::vec3(0.f, 0.f, 0.f) : glm::vec3(0.03f, 0.03f, 0.f) );
// MODIFY geometry based on GEOMETRY node
groups_[View::RENDERING]->translation_ = groups_[View::GEOMETRY]->translation_;
groups_[View::RENDERING]->rotation_ = groups_[View::GEOMETRY]->rotation_;
glm::vec3 s = groups_[View::GEOMETRY]->scale_;
// avoid any null scale
s.x = CLAMP_SCALE(s.x);
s.y = CLAMP_SCALE(s.y);
s.z = 1.f;
groups_[View::GEOMETRY]->scale_ = s;
groups_[View::RENDERING]->scale_ = s;
// MODIFY CROP projection based on GEOMETRY crop
renderbuffer_->setProjectionArea( glm::vec2(groups_[View::GEOMETRY]->crop_) );
// Mixing and layer icons scaled based on GEOMETRY crop
mixingsurface_->scale_ = groups_[View::GEOMETRY]->crop_;
mixingsurface_->scale_.x *= renderbuffer_->aspectRatio();
mixingsurface_->update(dt_);
// Layers icons are displayed in Perspective (diagonal)
groups_[View::LAYER]->translation_.x = -groups_[View::LAYER]->translation_.z;
groups_[View::LAYER]->translation_.y = groups_[View::LAYER]->translation_.x / LAYER_PERSPECTIVE;
// Update workspace based on depth, and
// adjust vertical position of icon depending on workspace
if (groups_[View::LAYER]->translation_.x < -LAYER_FOREGROUND) {
groups_[View::LAYER]->translation_.y -= 0.3f;
workspace_ = Source::FOREGROUND;
}
else if (groups_[View::LAYER]->translation_.x < -LAYER_BACKGROUND) {
groups_[View::LAYER]->translation_.y -= 0.15f;
workspace_ = Source::STAGE;
}
else
workspace_ = Source::BACKGROUND;
// MODIFY depth based on LAYER node
groups_[View::MIXING]->translation_.z = groups_[View::LAYER]->translation_.z;
groups_[View::GEOMETRY]->translation_.z = groups_[View::LAYER]->translation_.z;
groups_[View::RENDERING]->translation_.z = groups_[View::LAYER]->translation_.z;
// MODIFY texture projection based on APPEARANCE node
// UV to node coordinates
static glm::mat4 UVtoScene = GlmToolkit::transform(glm::vec3(1.f, -1.f, 0.f),
glm::vec3(0.f, 0.f, 0.f),
glm::vec3(-2.f, 2.f, 1.f));
// Aspect Ratio correction transform : coordinates of Appearance Frame are scaled by render buffer width
glm::mat4 Ar = glm::scale(glm::identity<glm::mat4>(), glm::vec3(renderbuffer_->aspectRatio(), 1.f, 1.f) );
// Translation : same as Appearance Frame (modified by Ar)
glm::mat4 Tra = glm::translate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->translation_);
// Scaling : inverse scaling (larger UV when smaller Appearance Frame)
glm::vec2 scale = glm::vec2(groups_[View::TEXTURE]->scale_.x,groups_[View::TEXTURE]->scale_.y);
scale = glm::sign(scale) * glm::max( glm::vec2(glm::epsilon<float>()), glm::abs(scale));
glm::mat4 Sca = glm::scale(glm::identity<glm::mat4>(), glm::vec3(scale, 1.f));
// Rotation : same angle than Appearance Frame, inverted axis
glm::mat4 Rot = glm::rotate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->rotation_.z, glm::vec3(0.f, 0.f, -1.f) );
// Combine transformations (non transitive) in this order:
// 1. switch to Scene coordinate system
// 2. Apply the aspect ratio correction
// 3. Apply the translation
// 4. Apply the rotation (centered after translation)
// 5. Revert aspect ration correction
// 6. Apply the Scaling (independent of aspect ratio)
// 7. switch back to UV coordinate system
texturesurface_->shader()->iTransform = glm::inverse(UVtoScene) * glm::inverse(Sca) * glm::inverse(Ar) * Rot * Tra * Ar * UVtoScene;
// if a mask image was given to be updated
if (mask_need_update_) {
// fill the mask buffer (once)
if (maskbuffer_->fill(maskimage_) )
mask_need_update_ = false;
}
// otherwise, render the mask buffer
else
// lock access to callbacks list
access_callbacks_.lock();
// call all callbacks
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
{
// draw mask in mask frame buffer
maskbuffer_->begin(false);
// loopback maskbuffer texture for painting
masksurface_->setTextureIndex(maskbuffer_->texture());
// fill surface with mask texture
masksurface_->draw(glm::identity<glm::mat4>(), maskbuffer_->projection());
maskbuffer_->end();
}
SourceCallback *callback = *iter;
// set the rendered mask as mask for blending
blendingshader_->mask_texture = maskbuffer_->texture();
// call update for active callbacks
if (callback->active()) {
callback->update(this, dt);
need_update_ = true;
}
// inform mixing group
if (mixinggroup_)
mixinggroup_->setAction(MixingGroup::ACTION_UPDATE);
// do not update next frame
need_update_ = false;
}
if (processingshader_link_.connected() && imageProcessingEnabled()) {
Source *ref_source = processingshader_link_.source();
if (ref_source!=nullptr) {
if (ref_source->imageProcessingEnabled())
processingshader_->copy( *ref_source->processingShader() );
// remove and delete finished callbacks
if (callback->finished()) {
iter = update_callbacks_.erase(iter);
delete callback;
}
// iterate
else
processingshader_link_.disconnect();
++iter;
}
// release access to callbacks list
access_callbacks_.unlock();
// update nodes if needed
if (need_update_)
{
// ADJUST alpha based on MIXING node
// read position of the mixing node and interpret this as transparency of render output
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
// use the sinusoidal transfer function
blendingshader_->color = glm::vec4(1.f, 1.f, 1.f, SourceCore::alphaFromCordinates( dist.x, dist.y ));
mixingshader_->color = blendingshader_->color;
// adjust scale of mixing icon : smaller if not active
groups_[View::MIXING]->scale_ = glm::vec3(MIXING_ICON_SCALE) - ( active_ ? glm::vec3(0.f, 0.f, 0.f) : glm::vec3(0.03f, 0.03f, 0.f) );
// MODIFY geometry based on GEOMETRY node
groups_[View::RENDERING]->translation_ = groups_[View::GEOMETRY]->translation_;
groups_[View::RENDERING]->rotation_ = groups_[View::GEOMETRY]->rotation_;
glm::vec3 s = groups_[View::GEOMETRY]->scale_;
// avoid any null scale
s.x = CLAMP_SCALE(s.x);
s.y = CLAMP_SCALE(s.y);
s.z = 1.f;
groups_[View::GEOMETRY]->scale_ = s;
groups_[View::RENDERING]->scale_ = s;
// MODIFY CROP projection based on GEOMETRY crop
renderbuffer_->setProjectionArea( glm::vec2(groups_[View::GEOMETRY]->crop_) );
// Mixing and layer icons scaled based on GEOMETRY crop
mixingsurface_->scale_ = groups_[View::GEOMETRY]->crop_;
mixingsurface_->scale_.x *= renderbuffer_->aspectRatio();
mixingsurface_->update(dt_);
// Layers icons are displayed in Perspective (diagonal)
groups_[View::LAYER]->translation_.x = -groups_[View::LAYER]->translation_.z;
groups_[View::LAYER]->translation_.y = groups_[View::LAYER]->translation_.x / LAYER_PERSPECTIVE;
// Update workspace based on depth, and
// adjust vertical position of icon depending on workspace
if (groups_[View::LAYER]->translation_.x < -LAYER_FOREGROUND) {
groups_[View::LAYER]->translation_.y -= 0.3f;
workspace_ = Source::FOREGROUND;
}
else if (groups_[View::LAYER]->translation_.x < -LAYER_BACKGROUND) {
groups_[View::LAYER]->translation_.y -= 0.15f;
workspace_ = Source::STAGE;
}
else
workspace_ = Source::BACKGROUND;
// MODIFY depth based on LAYER node
groups_[View::MIXING]->translation_.z = groups_[View::LAYER]->translation_.z;
groups_[View::GEOMETRY]->translation_.z = groups_[View::LAYER]->translation_.z;
groups_[View::RENDERING]->translation_.z = groups_[View::LAYER]->translation_.z;
// MODIFY texture projection based on APPEARANCE node
// UV to node coordinates
static glm::mat4 UVtoScene = GlmToolkit::transform(glm::vec3(1.f, -1.f, 0.f),
glm::vec3(0.f, 0.f, 0.f),
glm::vec3(-2.f, 2.f, 1.f));
// Aspect Ratio correction transform : coordinates of Appearance Frame are scaled by render buffer width
glm::mat4 Ar = glm::scale(glm::identity<glm::mat4>(), glm::vec3(renderbuffer_->aspectRatio(), 1.f, 1.f) );
// Translation : same as Appearance Frame (modified by Ar)
glm::mat4 Tra = glm::translate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->translation_);
// Scaling : inverse scaling (larger UV when smaller Appearance Frame)
glm::vec2 scale = glm::vec2(groups_[View::TEXTURE]->scale_.x,groups_[View::TEXTURE]->scale_.y);
scale = glm::sign(scale) * glm::max( glm::vec2(glm::epsilon<float>()), glm::abs(scale));
glm::mat4 Sca = glm::scale(glm::identity<glm::mat4>(), glm::vec3(scale, 1.f));
// Rotation : same angle than Appearance Frame, inverted axis
glm::mat4 Rot = glm::rotate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->rotation_.z, glm::vec3(0.f, 0.f, -1.f) );
// Combine transformations (non transitive) in this order:
// 1. switch to Scene coordinate system
// 2. Apply the aspect ratio correction
// 3. Apply the translation
// 4. Apply the rotation (centered after translation)
// 5. Revert aspect ration correction
// 6. Apply the Scaling (independent of aspect ratio)
// 7. switch back to UV coordinate system
texturesurface_->shader()->iTransform = glm::inverse(UVtoScene) * glm::inverse(Sca) * glm::inverse(Ar) * Rot * Tra * Ar * UVtoScene;
// if a mask image was given to be updated
if (mask_need_update_) {
// fill the mask buffer (once)
if (maskbuffer_->fill(maskimage_) )
mask_need_update_ = false;
}
// otherwise, render the mask buffer
else
{
// draw mask in mask frame buffer
maskbuffer_->begin(false);
// loopback maskbuffer texture for painting
masksurface_->setTextureIndex(maskbuffer_->texture());
// fill surface with mask texture
masksurface_->draw(glm::identity<glm::mat4>(), maskbuffer_->projection());
maskbuffer_->end();
}
// set the rendered mask as mask for blending
blendingshader_->mask_texture = maskbuffer_->texture();
// inform mixing group
if (mixinggroup_)
mixinggroup_->setAction(MixingGroup::ACTION_UPDATE);
// do not update next frame
need_update_ = false;
}
if (processingshader_link_.connected() && imageProcessingEnabled()) {
Source *ref_source = processingshader_link_.source();
if (ref_source!=nullptr) {
if (ref_source->imageProcessingEnabled())
processingshader_->copy( *ref_source->processingShader() );
else
processingshader_link_.disconnect();
}
}
}
}
FrameBuffer *Source::frame() const
@@ -713,7 +809,7 @@ FrameBuffer *Source::frame() const
return renderbuffer_;
}
else {
static FrameBuffer *black = new FrameBuffer(640,480);
static FrameBuffer *black = new FrameBuffer(320,180);
return black;
}
}
@@ -867,10 +963,19 @@ void CloneSource::setActive (bool on)
groups_[View::GEOMETRY]->visible_ = active_;
groups_[View::LAYER]->visible_ = active_;
if ( mode_ > Source::UNINITIALIZED && origin_ != nullptr)
origin_->touch();
}
if (origin_) {
if ( mode_ > Source::UNINITIALIZED)
origin_->touch();
// change visibility of active surface (show preview of origin when inactive)
if (activesurface_) {
if (active_)
activesurface_->setTextureIndex(Resource::getTextureTransparent());
else
activesurface_->setTextureIndex(origin_->texture());
}
}
}
uint CloneSource::texture() const
{
@@ -887,3 +992,13 @@ void CloneSource::accept(Visitor& v)
v.visit(*this);
}
glm::ivec2 CloneSource::icon() const
{
return glm::ivec2(ICON_SOURCE_CLONE);
}
std::string CloneSource::info() const
{
return std::string("clone of '") + origin_->name() + "'";
}

View File

@@ -4,12 +4,29 @@
#include <string>
#include <map>
#include <atomic>
#include <mutex>
#include <list>
#include "View.h"
#include "SourceCallback.h"
#define DEFAULT_MIXING_TRANSLATION -1.f, 1.f
#define ICON_SOURCE_VIDEO 18, 13
#define ICON_SOURCE_IMAGE 4, 9
#define ICON_SOURCE_DEVICE_SCREEN 19, 1
#define ICON_SOURCE_DEVICE 2, 14
#define ICON_SOURCE_SEQUENCE 3, 9
#define ICON_SOURCE_NETWORK 18, 11
#define ICON_SOURCE_PATTERN 11, 5
#define ICON_SOURCE_SESSION 19, 6
#define ICON_SOURCE_GROUP 10, 6
#define ICON_SOURCE_RENDER 0, 2
#define ICON_SOURCE_CLONE 9, 2
#define ICON_SOURCE_GSTREAMER 16, 16
#define ICON_SOURCE 13, 11
class SourceCallback;
class ImageShader;
class MaskShader;
class ImageProcessingShader;
@@ -18,6 +35,7 @@ class FrameBufferSurface;
class Frame;
class Handles;
class Symbol;
class Glyph;
class CloneSource;
class MixingGroup;
@@ -43,6 +61,9 @@ public:
void copy(SourceCore const& other);
// alpha transfer function
static float alphaFromCordinates(float x, float y);
protected:
// nodes
std::map<View::Mode, Group*> groups_;
@@ -123,9 +144,13 @@ public:
// a Source shall be updated before displayed (Mixing, Geometry and Layer)
virtual void update (float dt);
// add callback to each update
void call(SourceCallback *callback, bool override = false);
// update mode
inline bool active () const { return active_; }
virtual void setActive (bool on);
void setActive (float threshold);
// lock mode
inline bool locked () const { return locked_; }
@@ -139,6 +164,13 @@ public:
} Workspace;
inline Workspace workspace () const { return workspace_; }
// a Source shall define a way to play
virtual bool playable () const = 0;
virtual bool playing () const = 0;
virtual void play (bool on) = 0;
virtual void replay () {}
virtual guint64 playtime () const { return 0; }
// a Source shall informs if the source failed (i.e. shall be deleted)
virtual bool failed () const = 0;
@@ -158,13 +190,9 @@ public:
void setMask (FrameBufferImage *img);
void storeMask (FrameBufferImage *img = nullptr);
// operations on depth
// get properties
float depth () const;
void setDepth (float d);
// operations on alpha
float alpha () const;
void setAlpha (float a);
// groups for mixing
MixingGroup *mixingGroup() const { return mixinggroup_; }
@@ -221,7 +249,10 @@ public:
}
// class-dependent icon
virtual glm::ivec2 icon () const { return glm::ivec2(12, 11); }
virtual glm::ivec2 icon () const { return glm::ivec2(ICON_SOURCE); }
// class-dependent notification
virtual std::string info () const { return "Undefined"; }
SourceLink processingshader_link_;
@@ -243,7 +274,11 @@ protected:
// the rendersurface draws the renderbuffer in the scene
// It is associated to the rendershader for mixing effects
FrameBufferSurface *rendersurface_;
// for the mixer, we have a surface with stippling to show
// the rendering, and a preview of the original texture
FrameBufferSurface *mixingsurface_;
Surface *activesurface_;
// blendingshader provides mixing controls
ImageShader *blendingshader_;
@@ -269,6 +304,7 @@ protected:
Handles *lock_, *unlock_;
Switch *locker_;
Symbol *symbol_;
Glyph *initial_0_, *initial_1_;
// update
bool active_;
@@ -276,6 +312,8 @@ protected:
bool need_update_;
float dt_;
Workspace workspace_;
std::list<SourceCallback *> update_callbacks_;
std::mutex access_callbacks_;
// clones
CloneList clones_;
@@ -299,6 +337,10 @@ public:
// implementation of source API
void setActive (bool on) override;
bool playing () const override { return true; }
void play (bool) override {}
bool playable () const override { return false; }
void replay () override {}
uint texture() const override;
bool failed() const override { return origin_ == nullptr; }
void accept (Visitor& v) override;
@@ -307,7 +349,8 @@ public:
inline void detach() { origin_ = nullptr; }
inline Source *origin() const { return origin_; }
glm::ivec2 icon() const override { return glm::ivec2(9, 2); }
glm::ivec2 icon() const override;
std::string info() const override;
protected:
// only Source class can create new CloneSource via clone();

299
SourceCallback.cpp Normal file
View File

@@ -0,0 +1,299 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "defines.h"
#include "Source.h"
#include "Log.h"
#include "SourceCallback.h"
SourceCallback::SourceCallback(): active_(true), finished_(false), initialized_(false)
{
}
void ResetGeometry::update(Source *s, float)
{
s->group(View::GEOMETRY)->scale_ = glm::vec3(1.f);
s->group(View::GEOMETRY)->rotation_.z = 0;
s->group(View::GEOMETRY)->crop_ = glm::vec3(1.f);
s->group(View::GEOMETRY)->translation_ = glm::vec3(0.f);
s->touch();
finished_ = true;
}
SetAlpha::SetAlpha(float alpha) : SourceCallback(), alpha_(CLAMP(alpha, 0.f, 1.f))
{
pos_ = glm::vec2();
step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default
}
void SetAlpha::update(Source *s, float)
{
if (s && !s->locked()) {
// set start position on first run or upon call of reset()
if (!initialized_){
// initial position
pos_ = glm::vec2(s->group(View::MIXING)->translation_);
// step in direction of source translation if possible
if ( glm::length(pos_) > DELTA_ALPHA)
step_ = glm::normalize(pos_);
initialized_ = true;
}
// perform operation
float delta = SourceCore::alphaFromCordinates(pos_.x, pos_.y) - alpha_;
// converge to reduce the difference of alpha using dichotomic algorithm
if ( glm::abs(delta) > DELTA_ALPHA ){
pos_ += step_ * (delta / 2.f);
s->group(View::MIXING)->translation_ = glm::vec3(pos_, s->group(View::MIXING)->translation_.z);
}
// done
else
finished_ = true;
}
else
finished_ = true;
}
SetLock::SetLock(bool on) : SourceCallback(), lock_(on)
{
}
void SetLock::update(Source *s, float)
{
if (s)
s->setLocked(lock_);
finished_ = true;
}
Loom::Loom(float da, float duration) : SourceCallback(), speed_(da),
duration_(duration), progress_(0.f)
{
pos_ = glm::vec2();
step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default
}
void Loom::update(Source *s, float dt)
{
if (s && !s->locked()) {
// reset on first run or upon call of reset()
if (!initialized_){
// start animation
progress_ = 0.f;
// initial position
pos_ = glm::vec2(s->group(View::MIXING)->translation_);
// step in direction of source translation if possible
if ( glm::length(pos_) > DELTA_ALPHA)
step_ = glm::normalize(pos_);
initialized_ = true;
}
// time passed
progress_ += dt;
// move target by speed vector (in the direction of step_, amplitude of speed * time (in second))
pos_ += step_ * ( speed_ * dt * 0.001f);
// apply alpha if valid in range [0 1]
float alpha = SourceCore::alphaFromCordinates(pos_.x, pos_.y);
if ( alpha > DELTA_ALPHA && alpha < 1.0 - DELTA_ALPHA )
s->group(View::MIXING)->translation_ = glm::vec3(pos_, s->group(View::MIXING)->translation_.z);
// time-out
if ( progress_ > duration_ ) {
// done
finished_ = true;
}
}
else
finished_ = true;
}
SetDepth::SetDepth(float target, float duration) : SourceCallback(),
duration_(duration), progress_(0.f), start_(0.f), target_(CLAMP(target, MIN_DEPTH, MAX_DEPTH))
{
}
void SetDepth::update(Source *s, float dt)
{
if (s && !s->locked()) {
// set start position on first run or upon call of reset()
if (!initialized_){
start_ = s->group(View::LAYER)->translation_.z;
progress_ = 0.f;
initialized_ = true;
}
// time passed
progress_ += dt;
// perform movement
if ( ABS(duration_) > 0.f)
s->group(View::LAYER)->translation_.z = start_ + (progress_/duration_) * (target_ - start_);
// time-out
if ( progress_ > duration_ ) {
// apply depth to target
s->group(View::LAYER)->translation_.z = target_;
// ensure reordering of view
++View::need_deep_update_;
// done
finished_ = true;
}
}
else
finished_ = true;
}
SetPlay::SetPlay(bool on) : SourceCallback(), play_(on)
{
}
void SetPlay::update(Source *s, float)
{
if (s && s->playing() != play_) {
// call play function
s->play(play_);
}
finished_ = true;
}
RePlay::RePlay() : SourceCallback()
{
}
void RePlay::update(Source *s, float)
{
if (s) {
// call replay function
s->replay();
}
finished_ = true;
}
Grab::Grab(float dx, float dy, float duration) : SourceCallback(), speed_(glm::vec2(dx,dy)),
duration_(duration), progress_(0.f)
{
}
void Grab::update(Source *s, float dt)
{
if (s && !s->locked()) {
// reset on first run or upon call of reset()
if (!initialized_){
// start animation
progress_ = 0.f;
// initial position
start_ = glm::vec2(s->group(View::GEOMETRY)->translation_);
initialized_ = true;
}
// time passed
progress_ += dt;
// move target by speed vector * time (in second)
glm::vec2 pos = start_ + speed_ * ( dt * 0.001f);
s->group(View::GEOMETRY)->translation_ = glm::vec3(pos, s->group(View::GEOMETRY)->translation_.z);
// time-out
if ( progress_ > duration_ ) {
// done
finished_ = true;
}
}
else
finished_ = true;
}
Resize::Resize(float dx, float dy, float duration) : SourceCallback(), speed_(glm::vec2(dx,dy)),
duration_(duration), progress_(0.f)
{
}
void Resize::update(Source *s, float dt)
{
if (s && !s->locked()) {
// reset on first run or upon call of reset()
if (!initialized_){
// start animation
progress_ = 0.f;
// initial position
start_ = glm::vec2(s->group(View::GEOMETRY)->scale_);
initialized_ = true;
}
// time passed
progress_ += dt;
// move target by speed vector * time (in second)
glm::vec2 scale = start_ + speed_ * ( dt * 0.001f);
s->group(View::GEOMETRY)->scale_ = glm::vec3(scale, s->group(View::GEOMETRY)->scale_.z);
// time-out
if ( progress_ > duration_ ) {
// done
finished_ = true;
}
}
else
finished_ = true;
}
Turn::Turn(float da, float duration) : SourceCallback(), speed_(da),
duration_(duration), progress_(0.f)
{
}
void Turn::update(Source *s, float dt)
{
if (s && !s->locked()) {
// reset on first run or upon call of reset()
if (!initialized_){
// start animation
progress_ = 0.f;
// initial position
start_ = s->group(View::GEOMETRY)->rotation_.z;
initialized_ = true;
}
// calculate amplitude of movement
progress_ += dt;
// perform movement
s->group(View::GEOMETRY)->rotation_.z = start_ + speed_ * ( dt * -0.001f / M_PI);
// timeout
if ( progress_ > duration_ ) {
// done
finished_ = true;
}
}
else
finished_ = true;
}

Some files were not shown because too many files have changed in this diff Show More