diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b324af..b438ad3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -584,6 +584,7 @@ set(VMIX_RSC_FILES ./rsc/mesh/icon_cube.ply ./rsc/mesh/icon_sequence.ply ./rsc/mesh/icon_receive.ply + ./rsc/mesh/icon_text.ply ./rsc/mesh/h_line.ply ./rsc/mesh/h_mark.ply ./rsc/shaders/filters/default.glsl diff --git a/rsc/mesh/icon_text.ply b/rsc/mesh/icon_text.ply new file mode 100644 index 0000000..aba6e7a --- /dev/null +++ b/rsc/mesh/icon_text.ply @@ -0,0 +1,269 @@ +ply +format ascii 1.0 +comment Created by Blender 3.6.2 - www.blender.org +element vertex 128 +property float x +property float y +property float z +property float nx +property float ny +property float nz +property float s +property float t +element face 126 +property list uchar uint vertex_indices +end_header +-0.097202 0.041461 0.000000 0.000000 0.000000 1.000000 0.015748 0.000000 +-0.065577 0.065123 0.000000 0.000000 0.000000 1.000000 0.062992 0.000000 +-0.081445 0.088699 0.000000 0.000000 0.000000 1.000000 0.007874 0.000000 +-0.060589 0.068724 0.000000 0.000000 0.000000 1.000000 0.070866 0.000000 +-0.055407 0.071740 0.000000 0.000000 0.000000 1.000000 0.078740 0.000000 +-0.050016 0.074183 0.000000 0.000000 0.000000 1.000000 0.086614 0.000000 +-0.044397 0.076063 0.000000 0.000000 0.000000 1.000000 0.094488 0.000000 +-0.038536 0.077392 0.000000 0.000000 0.000000 1.000000 0.102362 0.000000 +-0.032414 0.078181 0.000000 0.000000 0.000000 1.000000 0.110236 0.000000 +0.082027 0.088699 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 +-0.026016 0.078442 0.000000 0.000000 0.000000 1.000000 0.118110 0.000000 +0.025754 0.078442 0.000000 0.000000 0.000000 1.000000 0.811024 0.000000 +0.031937 0.078156 0.000000 0.000000 0.000000 1.000000 0.818898 0.000000 +0.037960 0.077298 0.000000 0.000000 0.000000 1.000000 0.826772 0.000000 +0.043814 0.075873 0.000000 0.000000 0.000000 1.000000 0.834646 0.000000 +0.049493 0.073883 0.000000 0.000000 0.000000 1.000000 0.842520 0.000000 +0.054988 0.071330 0.000000 0.000000 0.000000 1.000000 0.850394 0.000000 +0.060292 0.068218 0.000000 0.000000 0.000000 1.000000 0.858268 0.000000 +0.097784 0.041461 0.000000 0.000000 0.000000 1.000000 1.000000 0.000000 +-0.023822 0.078384 0.000000 0.000000 0.000000 1.000000 0.125984 0.000000 +0.023642 0.078389 0.000000 0.000000 0.000000 1.000000 0.803150 0.000000 +0.021795 0.078218 0.000000 0.000000 0.000000 1.000000 0.795276 0.000000 +-0.021875 0.078204 0.000000 0.000000 0.000000 1.000000 0.133858 0.000000 +0.020198 0.077915 0.000000 0.000000 0.000000 1.000000 0.787402 0.000000 +-0.020165 0.077894 0.000000 0.000000 0.000000 1.000000 0.141732 0.000000 +0.018835 0.077462 0.000000 0.000000 0.000000 1.000000 0.779528 0.000000 +-0.018680 0.077442 0.000000 0.000000 0.000000 1.000000 0.149606 0.000000 +0.017691 0.076845 0.000000 0.000000 0.000000 1.000000 0.771654 0.000000 +-0.017410 0.076841 0.000000 0.000000 0.000000 1.000000 0.157480 0.000000 +0.016751 0.076046 0.000000 0.000000 0.000000 1.000000 0.763780 0.000000 +-0.016345 0.076080 0.000000 0.000000 0.000000 1.000000 0.165354 0.000000 +-0.015472 0.075150 0.000000 0.000000 0.000000 1.000000 0.173228 0.000000 +0.015998 0.075051 0.000000 0.000000 0.000000 1.000000 0.755906 0.000000 +-0.014783 0.074043 0.000000 0.000000 0.000000 1.000000 0.181102 0.000000 +0.015417 0.073843 0.000000 0.000000 0.000000 1.000000 0.748031 0.000000 +-0.014265 0.072748 0.000000 0.000000 0.000000 1.000000 0.188976 0.000000 +0.014992 0.072406 0.000000 0.000000 0.000000 1.000000 0.740157 0.000000 +-0.013909 0.071256 0.000000 0.000000 0.000000 1.000000 0.196850 0.000000 +0.014708 0.070725 0.000000 0.000000 0.000000 1.000000 0.732283 0.000000 +-0.013703 0.069558 0.000000 0.000000 0.000000 1.000000 0.204724 0.000000 +0.014549 0.068783 0.000000 0.000000 0.000000 1.000000 0.724409 0.000000 +-0.013636 0.067644 0.000000 0.000000 0.000000 1.000000 0.212598 0.000000 +0.014500 0.066565 0.000000 0.000000 0.000000 1.000000 0.716535 0.000000 +0.065396 0.064549 0.000000 0.000000 0.000000 1.000000 0.866142 0.000000 +-0.013636 0.065207 0.000000 0.000000 0.000000 1.000000 0.220472 0.000000 +0.014500 0.064148 0.000000 0.000000 0.000000 1.000000 0.708661 0.000000 +-0.013636 0.058467 0.000000 0.000000 0.000000 1.000000 0.228346 0.000000 +-0.070389 0.060926 0.000000 0.000000 0.000000 1.000000 0.055118 0.000000 +0.070293 0.060326 0.000000 0.000000 0.000000 1.000000 0.874016 0.000000 +0.014500 0.057467 0.000000 0.000000 0.000000 1.000000 0.700787 0.000000 +-0.075040 0.056122 0.000000 0.000000 0.000000 1.000000 0.047244 0.000000 +0.074976 0.055552 0.000000 0.000000 0.000000 1.000000 0.881890 0.000000 +-0.013636 0.048285 0.000000 0.000000 0.000000 1.000000 0.236220 0.000000 +0.014500 0.047374 0.000000 0.000000 0.000000 1.000000 0.692913 0.000000 +-0.079547 0.050699 0.000000 0.000000 0.000000 1.000000 0.039370 0.000000 +0.079435 0.050230 0.000000 0.000000 0.000000 1.000000 0.889764 0.000000 +-0.083928 0.044646 0.000000 0.000000 0.000000 1.000000 0.031496 0.000000 +0.083664 0.044362 0.000000 0.000000 0.000000 1.000000 0.897638 0.000000 +-0.013636 0.035522 0.000000 0.000000 0.000000 1.000000 0.244094 0.000000 +0.014500 0.034722 0.000000 0.000000 0.000000 1.000000 0.685039 0.000000 +-0.088198 0.037952 0.000000 0.000000 0.000000 1.000000 0.023622 0.000000 +0.087654 0.037952 0.000000 0.000000 0.000000 1.000000 0.905512 0.000000 +0.089237 0.038500 0.000000 0.000000 0.000000 -1.000000 0.929134 0.000000 +0.090281 0.038861 0.000000 0.000000 0.000000 1.000000 0.937008 0.000000 +0.091465 0.039272 0.000000 0.000000 0.000000 -1.000000 0.944882 0.000000 +0.092719 0.039706 0.000000 0.000000 0.000000 1.000000 0.952756 0.000000 +0.096201 0.040912 0.000000 0.000000 0.000000 1.000000 0.976378 0.000000 +0.097033 0.041201 0.000000 0.000000 0.000000 1.000000 0.984252 0.000000 +0.097584 0.041392 0.000000 0.000000 0.000000 1.000000 0.992126 0.000000 +0.095158 0.040551 0.000000 0.000000 0.000000 -1.000000 0.968504 0.000000 +0.093973 0.040141 0.000000 0.000000 0.000000 1.000000 0.960630 0.000000 +0.087854 0.038021 0.000000 0.000000 0.000000 -1.000000 0.913386 0.000000 +0.088405 0.038212 0.000000 0.000000 0.000000 1.000000 0.921260 0.000000 +-0.013636 0.021038 0.000000 0.000000 0.000000 1.000000 0.251969 0.000000 +0.014500 0.020365 0.000000 0.000000 0.000000 1.000000 0.677165 0.000000 +-0.013636 0.005694 0.000000 0.000000 0.000000 1.000000 0.259843 0.000000 +0.014500 0.005154 0.000000 0.000000 0.000000 1.000000 0.669291 0.000000 +-0.013636 -0.009650 0.000000 0.000000 0.000000 1.000000 0.267717 0.000000 +0.014500 -0.010056 0.000000 0.000000 0.000000 1.000000 0.661417 0.000000 +-0.013636 -0.024133 0.000000 0.000000 0.000000 1.000000 0.275591 0.000000 +0.014500 -0.024413 0.000000 0.000000 0.000000 1.000000 0.653543 0.000000 +-0.013636 -0.036896 0.000000 0.000000 0.000000 1.000000 0.283465 0.000000 +0.014500 -0.037065 0.000000 0.000000 0.000000 1.000000 0.645669 0.000000 +-0.013636 -0.047078 0.000000 0.000000 0.000000 1.000000 0.291339 0.000000 +0.014500 -0.047158 0.000000 0.000000 0.000000 1.000000 0.637795 0.000000 +-0.013636 -0.053818 0.000000 0.000000 0.000000 1.000000 0.299213 0.000000 +0.014500 -0.053839 0.000000 0.000000 0.000000 1.000000 0.629921 0.000000 +-0.013636 -0.056256 0.000000 0.000000 0.000000 1.000000 0.307087 0.000000 +0.014500 -0.056256 0.000000 0.000000 0.000000 1.000000 0.622047 0.000000 +-0.013782 -0.061521 0.000000 0.000000 0.000000 1.000000 0.314961 0.000000 +0.014580 -0.061516 0.000000 0.000000 0.000000 1.000000 0.614173 0.000000 +0.014839 -0.066122 0.000000 0.000000 0.000000 1.000000 0.606299 0.000000 +-0.014094 -0.066141 0.000000 0.000000 0.000000 1.000000 0.322835 0.000000 +0.015300 -0.070119 0.000000 0.000000 0.000000 1.000000 0.598425 0.000000 +-0.014599 -0.070157 0.000000 0.000000 0.000000 1.000000 0.330709 0.000000 +0.015990 -0.073552 0.000000 0.000000 0.000000 1.000000 0.590551 0.000000 +-0.015325 -0.073612 0.000000 0.000000 0.000000 1.000000 0.338583 0.000000 +0.016934 -0.076463 0.000000 0.000000 0.000000 1.000000 0.582677 0.000000 +-0.016296 -0.076545 0.000000 0.000000 0.000000 1.000000 0.346457 0.000000 +0.018158 -0.078897 0.000000 0.000000 0.000000 1.000000 0.574803 0.000000 +-0.017540 -0.078998 0.000000 0.000000 0.000000 1.000000 0.354331 0.000000 +0.019686 -0.080897 0.000000 0.000000 0.000000 1.000000 0.566929 0.000000 +-0.019083 -0.081012 0.000000 0.000000 0.000000 1.000000 0.362205 0.000000 +0.021544 -0.082509 0.000000 0.000000 0.000000 1.000000 0.559055 0.000000 +-0.020952 -0.082629 0.000000 0.000000 0.000000 1.000000 0.370079 0.000000 +0.023759 -0.083776 0.000000 0.000000 0.000000 1.000000 0.551181 0.000000 +-0.023172 -0.083890 0.000000 0.000000 0.000000 1.000000 0.377953 0.000000 +0.026354 -0.084743 0.000000 0.000000 0.000000 1.000000 0.543307 0.000000 +-0.025770 -0.084836 0.000000 0.000000 0.000000 1.000000 0.385827 0.000000 +0.029355 -0.085452 0.000000 0.000000 0.000000 1.000000 0.535433 0.000000 +-0.028773 -0.085509 0.000000 0.000000 0.000000 1.000000 0.393701 0.000000 +0.032789 -0.085949 0.000000 0.000000 0.000000 1.000000 0.527559 0.000000 +-0.032206 -0.085949 0.000000 0.000000 0.000000 1.000000 0.401575 0.000000 +-0.032417 -0.085965 0.000000 0.000000 0.000000 1.000000 0.409449 0.000000 +0.043762 -0.086758 0.000000 0.000000 0.000000 1.000000 0.519685 0.000000 +-0.032998 -0.086009 0.000000 0.000000 0.000000 1.000000 0.417323 0.000000 +-0.033877 -0.086075 0.000000 0.000000 0.000000 1.000000 0.425197 0.000000 +-0.034978 -0.086159 0.000000 0.000000 0.000000 1.000000 0.433071 0.000000 +-0.036228 -0.086253 0.000000 0.000000 0.000000 1.000000 0.440945 0.000000 +-0.037552 -0.086354 0.000000 0.000000 0.000000 1.000000 0.448819 0.000000 +-0.038876 -0.086454 0.000000 0.000000 0.000000 1.000000 0.456693 0.000000 +-0.040126 -0.086548 0.000000 0.000000 0.000000 1.000000 0.464567 0.000000 +-0.041228 -0.086632 0.000000 0.000000 0.000000 1.000000 0.472441 0.000000 +-0.042106 -0.086698 0.000000 0.000000 0.000000 1.000000 0.480315 0.000000 +-0.042688 -0.086743 0.000000 0.000000 0.000000 1.000000 0.488189 0.000000 +-0.042898 -0.086758 0.000000 0.000000 0.000000 1.000000 0.496063 0.000000 +-0.042898 -0.095126 0.000000 0.000000 0.000000 1.000000 0.503937 0.000000 +0.043762 -0.095126 0.000000 0.000000 0.000000 1.000000 0.511811 0.000000 +3 0 1 2 +3 1 3 2 +3 3 4 2 +3 4 5 2 +3 5 6 2 +3 6 7 2 +3 7 8 2 +3 8 9 2 +3 8 10 9 +3 10 11 9 +3 11 12 9 +3 12 13 9 +3 13 14 9 +3 14 15 9 +3 15 16 9 +3 16 17 9 +3 17 18 9 +3 19 11 10 +3 19 20 11 +3 19 21 20 +3 22 21 19 +3 22 23 21 +3 24 23 22 +3 24 25 23 +3 26 25 24 +3 26 27 25 +3 28 27 26 +3 28 29 27 +3 30 29 28 +3 31 29 30 +3 31 32 29 +3 33 32 31 +3 33 34 32 +3 35 34 33 +3 35 36 34 +3 37 36 35 +3 37 38 36 +3 39 38 37 +3 39 40 38 +3 41 40 39 +3 41 42 40 +3 43 18 17 +3 44 42 41 +3 44 45 42 +3 46 45 44 +3 0 47 1 +3 48 18 43 +3 46 49 45 +3 0 50 47 +3 51 18 48 +3 52 49 46 +3 52 53 49 +3 0 54 50 +3 55 18 51 +3 0 56 54 +3 57 18 55 +3 58 53 52 +3 58 59 53 +3 0 60 56 +3 61 18 57 +3 61 62 18 +3 62 63 18 +3 63 64 18 +3 64 65 18 +3 65 66 18 +3 66 67 18 +3 67 68 18 +3 65 69 66 +3 65 70 69 +3 61 71 62 +3 71 72 62 +3 73 59 58 +3 73 74 59 +3 75 74 73 +3 75 76 74 +3 77 76 75 +3 77 78 76 +3 79 78 77 +3 79 80 78 +3 81 80 79 +3 81 82 80 +3 83 82 81 +3 83 84 82 +3 85 84 83 +3 85 86 84 +3 87 86 85 +3 87 88 86 +3 89 88 87 +3 89 90 88 +3 89 91 90 +3 92 91 89 +3 92 93 91 +3 94 93 92 +3 94 95 93 +3 96 95 94 +3 96 97 95 +3 98 97 96 +3 98 99 97 +3 100 99 98 +3 100 101 99 +3 102 101 100 +3 102 103 101 +3 104 103 102 +3 104 105 103 +3 106 105 104 +3 106 107 105 +3 108 107 106 +3 108 109 107 +3 110 109 108 +3 110 111 109 +3 112 111 110 +3 113 111 112 +3 113 114 111 +3 115 114 113 +3 116 114 115 +3 117 114 116 +3 118 114 117 +3 119 114 118 +3 120 114 119 +3 121 114 120 +3 122 114 121 +3 123 114 122 +3 124 114 123 +3 125 114 124 +3 126 114 125 +3 126 127 114 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2451808..b6e234c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,7 @@ set(VMIX_SRCS Grid.cpp Playlist.cpp Audio.cpp + TextSource.cpp ) ##### diff --git a/src/ControlManager.cpp b/src/ControlManager.cpp index d079132..d05ebfd 100644 --- a/src/ControlManager.cpp +++ b/src/ControlManager.cpp @@ -32,6 +32,7 @@ #include "BaseToolkit.h" #include "Mixer.h" #include "Source.h" +#include "TextSource.h" #include "SourceCallback.h" #include "ImageProcessingShader.h" #include "ActionManager.h" @@ -831,10 +832,20 @@ bool Control::receiveSourceAttribute(Source *target, const std::string &attribut target->call( new Seek( t ), true ); } /// e.g. '/vimix/current/speed f 0.25' - else if ( attribute.compare(OSC_SOURCE_SPEED) == 0) { + else if (attribute.compare(OSC_SOURCE_SPEED) == 0) { float t = 0.f; arguments >> t >> osc::EndMessage; - target->call( new PlaySpeed( t ), true ); + target->call(new PlaySpeed(t), true); + } + /// e.g. '/vimix/current/contents s text' + else if (attribute.compare(OSC_SOURCE_CONTENTS) == 0) { + // try by client name if given: remove all streams with that name + const char *label = nullptr; + arguments >> label >> osc::EndMessage; + TextSource *textsrc = dynamic_cast(target); + if (textsrc && label) { + textsrc->contents()->setText(label); + } } /// e.g. '/vimix/name/sync' else if ( attribute.compare(OSC_SYNC) == 0) { diff --git a/src/ControlManager.h b/src/ControlManager.h index 5b3ae78..a24de02 100644 --- a/src/ControlManager.h +++ b/src/ControlManager.h @@ -51,6 +51,7 @@ #define OSC_SOURCE_ANGLE "/angle" #define OSC_SOURCE_SEEK "/seek" #define OSC_SOURCE_SPEED "/speed" +#define OSC_SOURCE_CONTENTS "/contents" #define OSC_SOURCE_BRIGHTNESS "/brightness" #define OSC_SOURCE_CONTRAST "/contrast" #define OSC_SOURCE_SATURATION "/saturation" diff --git a/src/Decorations.cpp b/src/Decorations.cpp index 4f44e14..c288f8c 100644 --- a/src/Decorations.cpp +++ b/src/Decorations.cpp @@ -460,6 +460,8 @@ Symbol::Symbol(Type t, glm::vec3 pos) : Node(), type_(t) shadows[SHARE] = shadow; icons[RECEIVE] = new Mesh("mesh/icon_receive.ply"); shadows[RECEIVE]= shadow; + icons[TEXT] = new Mesh("mesh/icon_text.ply"); + shadows[TEXT] = shadow; icons[DOTS] = new Mesh("mesh/icon_dots.ply"); shadows[DOTS] = nullptr; icons[BUSY] = new Mesh("mesh/icon_circles.ply"); diff --git a/src/Decorations.h b/src/Decorations.h index e770bbf..5ca9944 100644 --- a/src/Decorations.h +++ b/src/Decorations.h @@ -60,7 +60,7 @@ protected: class Symbol : public Node { public: - typedef enum { CIRCLE_POINT = 0, SQUARE_POINT, IMAGE, SEQUENCE, VIDEO, SESSION, CLONE, RENDER, GROUP, PATTERN, GEAR, CAMERA, SCREEN, SHARE, RECEIVE, + typedef enum { CIRCLE_POINT = 0, SQUARE_POINT, IMAGE, SEQUENCE, VIDEO, SESSION, CLONE, RENDER, GROUP, PATTERN, GEAR, CAMERA, SCREEN, SHARE, RECEIVE, TEXT, DOTS, BUSY, LOCK, UNLOCK, EYE, EYESLASH, TELEVISION, ARROWS, ROTATION, CROP, CIRCLE, SQUARE, CLOCK, CLOCK_H, GRID, CROSS, EMPTY } Type; Symbol(Type t, glm::vec3 pos = glm::vec3(0.f)); ~Symbol(); diff --git a/src/DialogToolkit.cpp b/src/DialogToolkit.cpp index 5e592a8..fff0613 100644 --- a/src/DialogToolkit.cpp +++ b/src/DialogToolkit.cpp @@ -188,6 +188,16 @@ void DialogToolkit::OpenPlaylistDialog::open() } } +std::string openSubtitleFileDialog(const std::string &label, const std::string &path); +void DialogToolkit::OpenSubtitleDialog::open() +{ + if ( !busy_ && promises_.empty() ) { + promises_.emplace_back( std::async(std::launch::async, openSubtitleFileDialog, id_, + Settings::application.dialogRecentFolder[id_]) ); + busy_ = true; + } +} + std::string openMediaFileDialog(const std::string &label, const std::string &path); void DialogToolkit::OpenMediaDialog::open() { @@ -598,6 +608,63 @@ std::string savePlaylistFileDialog(const std::string &label, const std::string & } +std::string openSubtitleFileDialog(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[2] = { SUBTITLE_FILES_PATTERN }; + +#if USE_TINYFILEDIALOG + char const * open_file_name; + open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 2, open_pattern, "Subtitle (SRT, SUB)", 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, 2, open_pattern, "Subtitle (SRT, SUB)"); + 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 openMediaFileDialog(const std::string &label, const std::string &path) { diff --git a/src/DialogToolkit.h b/src/DialogToolkit.h index 9432939..c514e3b 100644 --- a/src/DialogToolkit.h +++ b/src/DialogToolkit.h @@ -78,6 +78,13 @@ public: void open(); }; +class OpenSubtitleDialog : public FileDialog +{ +public: + OpenSubtitleDialog(const std::string &name) : FileDialog(name) {} + void open(); +}; + class OpenFolderDialog : public FileDialog { public: diff --git a/src/ImGuiToolkit.cpp b/src/ImGuiToolkit.cpp index a425a03..8f50624 100644 --- a/src/ImGuiToolkit.cpp +++ b/src/ImGuiToolkit.cpp @@ -1763,7 +1763,7 @@ void ImGuiToolkit::SetAccentColor(accent_color color) colors[ImGuiCol_TextDisabled] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); colors[ImGuiCol_WindowBg] = ImVec4(0.13f, 0.13f, 0.13f, 0.94f); colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.10f, 0.10f, 0.10f, 0.90f); + colors[ImGuiCol_PopupBg] = ImVec4(0.13f, 0.13f, 0.13f, 1.00f); colors[ImGuiCol_Border] = ImVec4(0.69f, 0.69f, 0.69f, 0.25f); colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_FrameBg] = ImVec4(0.39f, 0.39f, 0.39f, 0.55f); @@ -1928,12 +1928,16 @@ bool ImGuiToolkit::InputText(const char* label, std::string* str, ImGuiInputText return ImGui::InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, str); } -bool ImGuiToolkit::InputTextMultiline(const char* label, std::string* str, const ImVec2& size) +bool ImGuiToolkit::InputTextMultiline(const char* label, std::string* str, const ImVec2& size, int *numline) { ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize; ImGui::InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, str); + // number of lines + if (numline) + *numline = std::count(str->begin(), str->end(), '\n') + 1; + return ImGui::IsItemDeactivatedAfterEdit(); } diff --git a/src/ImGuiToolkit.h b/src/ImGuiToolkit.h index 01fb4dc..a0ff65a 100644 --- a/src/ImGuiToolkit.h +++ b/src/ImGuiToolkit.h @@ -75,7 +75,7 @@ namespace ImGuiToolkit // text input bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flag = ImGuiInputTextFlags_CharsNoBlank); - bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0)); + bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int *numline = NULL); 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); diff --git a/src/ImGuiVisitor.cpp b/src/ImGuiVisitor.cpp index 9636d9a..10298a2 100644 --- a/src/ImGuiVisitor.cpp +++ b/src/ImGuiVisitor.cpp @@ -53,6 +53,7 @@ #include "NetworkSource.h" #include "SrtReceiverSource.h" #include "MultiFileSource.h" +#include "TextSource.h" #include "SessionCreator.h" #include "SessionVisitor.h" #include "Settings.h" @@ -809,7 +810,8 @@ void ImGuiVisitor::visit (MediaSource& s) } } } - + else + ImGui::SetCursorPos(botom); } else { // failed @@ -1833,3 +1835,111 @@ void ImGuiVisitor::visit (SrtReceiverSource& s) ImGui::SetCursorPos(botom); } + + +void ImGuiVisitor::visit(TextSource &s) +{ + ImVec2 top = ImGui::GetCursorPos(); + top.x = 0.5f * ImGui::GetFrameHeight() + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN; + float w = ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN; + + // 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(); + ImGui::Spacing(); + + ImVec2 botom = ImGui::GetCursorPos(); + + TextContents *tc = s.contents(); + if (tc) { + // Prepare display 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 _contents = tc->text(); + // if the content is a subtitle file + if (tc->isSubtitle()) { + static char dummy_str[1024]; + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + snprintf(dummy_str, 1024, "%s", _contents.c_str()); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f)); + ImGui::InputText("##Filesubtitle", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(1); + botom = ImGui::GetCursorPos(); + // Action on the filename + ImGui::SetCursorPos(ImVec2(top.x, botom.y - ImGui::GetFrameHeight())); + if (ImGuiToolkit::IconButton(3, 5, "Open")) + SystemToolkit::open(_contents.c_str()); + } + // general case of free text + else { + if (ImGuiToolkit::InputTextMultiline("Text", &_contents, fieldsize, &numlines)) { + info.reset(); + tc->setText(_contents); + Action::manager().store(s.name() + ": Change text"); + } + botom = ImGui::GetCursorPos(); + + // Actions on the text + ImGui::SetCursorPos(ImVec2(top.x, botom.y - ImGui::GetFrameHeight())); + if (ImGuiToolkit::IconButton(ICON_FA_COPY, "Copy")) + ImGui::SetClipboardText(_contents.c_str()); + ImGui::SetCursorPos( + ImVec2(top.x + 0.9 * ImGui::GetFrameHeight(), botom.y - ImGui::GetFrameHeight())); + if (ImGuiToolkit::IconButton(ICON_FA_PASTE, "Paste")) { + _contents = std::string(ImGui::GetClipboardText()); + info.reset(); + tc->setText(_contents); + Action::manager().store(s.name() + ": Change text"); + } + } + // Properties + ImGui::SetCursorPos(botom); + + std::string font = tc->fontDescriptor(); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + ImGuiToolkit::InputText("Font", &font); + if (ImGui::IsItemDeactivatedAfterEdit()) { + tc->setFontDescriptor(font); + Action::manager().store( s.name() + " Change font"); + } + + int align = tc->halignment(); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if (ImGui::Combo("Horizontal", &align, "Left\0Center\0Right\0Absolute\0")) { + tc->setHalignment(align); + Action::manager().store( s.name() + " Horizontal align"); + } + + align = tc->valignment(); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if (ImGui::Combo("Vertical", &align, "Baseline\0Bottom\0top\0Absolute\0Center\0")) { + tc->setValignment(align); + Action::manager().store( s.name() + " Vertical align"); + } + + botom = ImGui::GetCursorPos(); + } + + if ( !s.failed() ) { + // icon (>) to open player + if ( s.stream()->isOpen() ) { + ImGui::SetCursorPos(top); + std::string msg = s.playing() ? "Open Player\n(source is playing)" : "Open Player\n(source is paused)"; + if (ImGuiToolkit::IconButton( s.playing() ? ICON_FA_PLAY_CIRCLE : ICON_FA_PAUSE_CIRCLE, msg.c_str())) + UserInterface::manager().showSourceEditor(&s); + } + else + info.reset(); + } + else + info.reset(); + + ImGui::SetCursorPos(botom); + +} diff --git a/src/ImGuiVisitor.h b/src/ImGuiVisitor.h index 915d1b7..f2a716b 100644 --- a/src/ImGuiVisitor.h +++ b/src/ImGuiVisitor.h @@ -35,6 +35,7 @@ public: void visit (MultiFileSource& s) override; void visit (GenericStreamSource& s) override; void visit (SrtReceiverSource& s) override; + void visit (TextSource& s) override; void visit (CloneSource& s) override; void visit (FrameBufferFilter&) override; diff --git a/src/InfoVisitor.cpp b/src/InfoVisitor.cpp index 839bea8..8b91db6 100644 --- a/src/InfoVisitor.cpp +++ b/src/InfoVisitor.cpp @@ -40,6 +40,7 @@ #include "NetworkSource.h" #include "SrtReceiverSource.h" #include "MultiFileSource.h" +#include "TextSource.h" #include "Session.h" #include "BaseToolkit.h" #include "SystemToolkit.h" @@ -499,3 +500,32 @@ void InfoVisitor::visit (SrtReceiverSource& s) information_ = oss.str(); current_id_ = s.id(); } + +void InfoVisitor::visit (TextSource& s) +{ + if (current_id_ == s.id()) + return; + + std::ostringstream oss; + + TextContents *ptn = s.contents(); + if (ptn) { + if (ptn->failed()) + oss << ptn->description() << std::endl << ptn->log(); + else { + if (brief_) { + oss << "RGBA, " << ptn->width() << " x " << ptn->height(); + } + else { + oss << (ptn->isSubtitle() ? "Subtitle " : "Free text") << std::endl; + oss << "RGBA" << std::endl; + oss << ptn->width() << " x " << ptn->height(); + } + } + } + else + oss << "Undefined"; + + information_ = oss.str(); + current_id_ = s.id(); +} diff --git a/src/InfoVisitor.h b/src/InfoVisitor.h index adb4807..7175166 100644 --- a/src/InfoVisitor.h +++ b/src/InfoVisitor.h @@ -2,6 +2,7 @@ #define INFOVISITOR_H #include "Visitor.h" +#include class InfoVisitor : public Visitor { @@ -38,6 +39,7 @@ public: void visit (MultiFileSource& s) override; void visit (GenericStreamSource& s) override; void visit (SrtReceiverSource& s) override; + void visit (TextSource& s) override; }; #endif // INFOVISITOR_H diff --git a/src/Mixer.cpp b/src/Mixer.cpp index 0fbd2c4..033bc1f 100644 --- a/src/Mixer.cpp +++ b/src/Mixer.cpp @@ -32,6 +32,7 @@ #include #include "ImageShader.h" +#include "TextSource.h" #include "defines.h" #include "Settings.h" #include "Log.h" @@ -436,6 +437,22 @@ Source * Mixer::createSourceSrt(const std::string &ip, const std::string &port) return s; } +Source *Mixer::createSourceText(const std::string &contents, glm::ivec2 res) +{ + // ready to create a source + TextSource *s = new TextSource; + s->setContents(contents, res); + + // propose a new name based on contents + std::string basestring = BaseToolkit::transliterate(contents); + basestring = BaseToolkit::splitted(basestring, '\n').front(); + if (SystemToolkit::file_exists(basestring)) + basestring = SystemToolkit::base_filename(basestring); + s->setName( basestring ); + + return s; +} + Source * Mixer::createSourceGroup() { SessionGroupSource *s = new SessionGroupSource; diff --git a/src/Mixer.h b/src/Mixer.h index 67b9ac0..c34f447 100644 --- a/src/Mixer.h +++ b/src/Mixer.h @@ -58,6 +58,7 @@ public: Source * createSourceScreen (const std::string &namedevice); Source * createSourceNetwork(const std::string &nameconnection); Source * createSourceSrt (const std::string &ip, const std::string &port); + Source * createSourceText (const std::string &contents, glm::ivec2 res); Source * createSourceGroup (); // operations on sources diff --git a/src/SessionCreator.cpp b/src/SessionCreator.cpp index 03a3350..c0c508f 100644 --- a/src/SessionCreator.cpp +++ b/src/SessionCreator.cpp @@ -37,6 +37,7 @@ #include "NetworkSource.h" #include "SrtReceiverSource.h" #include "MultiFileSource.h" +#include "TextSource.h" #include "RenderSource.h" #include "Session.h" #include "ImageShader.h" @@ -448,6 +449,9 @@ void SessionLoader::load(XMLElement *sessionNode) else if ( std::string(pType) == "SrtReceiverSource") { load_source = new SrtReceiverSource(id_xml_); } + else if ( std::string(pType) == "TextSource") { + load_source = new TextSource(id_xml_); + } else if ( std::string(pType) == "CloneSource") { cloneNodesToAdd.push_front(xmlCurrent_); } @@ -634,6 +638,9 @@ Source *SessionLoader::createSource(tinyxml2::XMLElement *sourceNode, Mode mode) else if ( std::string(pType) == "SrtReceiverSource") { load_source = new SrtReceiverSource(id__); } + else if ( std::string(pType) == "TextSource") { + load_source = new TextSource(id__); + } else if ( std::string(pType) == "CloneSource") { // clone from given origin XMLElement* originNode = xmlCurrent_->FirstChildElement("origin"); @@ -1222,10 +1229,70 @@ void SessionLoader::visit (PatternSource& s) tinyxml2::XMLElementToGLM( res->FirstChildElement("ivec2"), resolution); // change only if different pattern - if ( t != s.pattern()->type() ) + if ( s.pattern() && t != s.pattern()->type() ) s.setPattern(t, resolution); } +void SessionLoader::visit(TextSource &s) +{ + bool value_changed = false; + std::string text; + glm::ivec2 resolution(800, 600); + + XMLElement *_contents = xmlCurrent_->FirstChildElement("contents"); + if ( s.contents() && _contents) { + // content can be an array of encoded text (to support strings with Pango styles , , etc.) + unsigned int len = 0; + const XMLElement *array = _contents->FirstChildElement("array"); + if (array) { + array->QueryUnsignedAttribute("len", &len); + // ok, we got a size of data to load + if (len > 0) { + GString *data = g_string_new_len("", len); + if (XMLElementDecodeArray(array, data->str, data->len)) { + text = std::string(data->str); + } + g_string_free(data, FALSE); + } + } + // content can also just be raw text + else { + const char *data = _contents->GetText(); + if (data) + text = std::string(data); + } + + value_changed |= s.contents()->text().compare(text) != 0; + + // attributes of content + const char *font; + if (_contents->QueryStringAttribute("font-desc", &font) == XML_SUCCESS && font) { + if (s.contents()->fontDescriptor().compare(font) != 0) { + s.contents()->setFontDescriptor(font); + } + } + uint alignment = s.contents()->halignment(); + _contents->QueryUnsignedAttribute("halignment", &alignment); + if (s.contents()->halignment() != alignment) + s.contents()->setHalignment(alignment); + alignment = s.contents()->valignment(); + _contents->QueryUnsignedAttribute("valignment", &alignment); + if (s.contents()->valignment() != alignment) + s.contents()->setValignment(alignment); + } + + XMLElement* res = xmlCurrent_->FirstChildElement("resolution"); + if (res) { + tinyxml2::XMLElementToGLM( res->FirstChildElement("ivec2"), resolution); + value_changed |= resolution.x != (int) s.stream()->width() || resolution.y != (int) s.stream()->height(); + } + + // change only if different + if ( s.contents() && ( value_changed || s.contents()->text().empty() ) ) { + s.setContents( text, resolution ); + } +} + void SessionLoader::visit (DeviceSource& s) { std::string devname = std::string ( xmlCurrent_->Attribute("device") ); diff --git a/src/SessionCreator.h b/src/SessionCreator.h index d68bdec..8ec724c 100644 --- a/src/SessionCreator.h +++ b/src/SessionCreator.h @@ -65,6 +65,7 @@ public: void visit (MultiFileSource& s) override; void visit (GenericStreamSource& s) override; void visit (SrtReceiverSource& s) override; + void visit (TextSource& s) override; void visit (CloneSource& s) override; void visit (FrameBufferFilter&) override; diff --git a/src/SessionVisitor.cpp b/src/SessionVisitor.cpp index e3d768c..ac0492d 100644 --- a/src/SessionVisitor.cpp +++ b/src/SessionVisitor.cpp @@ -42,6 +42,7 @@ using namespace tinyxml2; #include "NetworkSource.h" #include "SrtReceiverSource.h" #include "MultiFileSource.h" +#include "TextSource.h" #include "ImageShader.h" #include "ImageProcessingShader.h" #include "ImageFilter.h" @@ -845,6 +846,37 @@ void SessionVisitor::visit (PatternSource& s) } } +void SessionVisitor::visit (TextSource& s) +{ + xmlCurrent_->SetAttribute("type", "TextSource"); + + if (s.contents()) { + XMLElement *contents = xmlDoc_->NewElement("contents"); + // simple text node to store filename of subtitle + if (s.contents()->isSubtitle()) { + XMLText *text = xmlDoc_->NewText(s.contents()->text().c_str()); + contents->InsertEndChild(text); + } + // encoded text node to store string with Pango style + else { + GString *data = g_string_new(s.contents()->text().c_str()); + XMLElement *text = XMLElementEncodeArray(xmlDoc_, data->str, data->len); + contents->InsertEndChild(text); + g_string_free(data, FALSE); + } + + contents->SetAttribute("font-desc", s.contents()->fontDescriptor().c_str() ); + contents->SetAttribute("halignment", s.contents()->halignment() ); + contents->SetAttribute("valignment", s.contents()->valignment() ); + + xmlCurrent_->InsertEndChild(contents); + } + + XMLElement *resolution = xmlDoc_->NewElement("resolution"); + resolution->InsertEndChild( XMLElementFromGLM(xmlDoc_, glm::ivec2(s.stream()->width(), s.stream()->height())) ); + xmlCurrent_->InsertEndChild(resolution); +} + void SessionVisitor::visit (DeviceSource& s) { xmlCurrent_->SetAttribute("type", "DeviceSource"); diff --git a/src/SessionVisitor.h b/src/SessionVisitor.h index a1e0811..0d5ecb3 100644 --- a/src/SessionVisitor.h +++ b/src/SessionVisitor.h @@ -71,6 +71,7 @@ public: void visit (MultiFileSource& s) override; void visit (GenericStreamSource& s) override; void visit (SrtReceiverSource& s) override; + void visit (TextSource& s) override; void visit (CloneSource& s) override; void visit (FrameBufferFilter&) override; diff --git a/src/Source.h b/src/Source.h index 781df4e..23d2033 100644 --- a/src/Source.h +++ b/src/Source.h @@ -26,6 +26,7 @@ #define ICON_SOURCE_CLONE 9, 2 #define ICON_SOURCE_GSTREAMER 16, 16 #define ICON_SOURCE_SRT 14, 5 +#define ICON_SOURCE_TEXT 0, 13 #define ICON_SOURCE 13, 11 #define ICON_WORKSPACE_BACKGROUND 10, 16 #define ICON_WORKSPACE_CENTRAL 11, 16 diff --git a/src/TextSource.cpp b/src/TextSource.cpp new file mode 100644 index 0000000..1d9e17d --- /dev/null +++ b/src/TextSource.cpp @@ -0,0 +1,315 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2023 Bruno Herbelin + * + * 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 . +**/ +#include +#include +#include +#include + +#include +#include +#include + +#include "SystemToolkit.h" +#include "Decorations.h" +#include "Visitor.h" +#include "Log.h" + +#include "TextSource.h" + +/// filesrc location=/home/bh/vimix/test.srt ! subparse ! txt. +/// videotestsrc pattern=black background-color=0x00000000 ! video/x-raw,width=1280,height=768,framerate=24/1 ! +/// textoverlay name=txt halignment=center valignment=center font-desc="sans,72" ! + +bool TextContents::SubtitleDiscoverer(const std::string &path) +{ + bool ret = false; + + // if path is valid + if (SystemToolkit::file_exists(path.c_str())) { + // set uri to open + gchar *uritmp = gst_filename_to_uri(path.c_str(), NULL); + // if uri is valid + if (uritmp != NULL) { + // create discoverer + GError *err = NULL; + GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, &err); + if (discoverer != nullptr) { + // get discoverer info for the URI + GstDiscovererInfo *info = NULL; + info = gst_discoverer_discover_uri(discoverer, uritmp, &err); + GstDiscovererResult result = gst_discoverer_info_get_result(info); + // discoverer success + if (result == GST_DISCOVERER_OK) { + // get subtitle streams + GList *streams = gst_discoverer_info_get_subtitle_streams(info); + // return true if at least one subtitle stream is discovered + ret = (g_list_length(streams) > 0); + gst_discoverer_stream_info_list_free(streams); + } + if (info) + gst_discoverer_info_unref(info); + g_object_unref(discoverer); + } + g_clear_error(&err); + g_free(uritmp); + } + } + + return ret; +} + + +TextContents::TextContents() + : Stream(), src_(nullptr), txt_(nullptr), + halignment_(1), valignment_(4) +{ + fontdesc_ = "arial, 24"; +} + +void TextContents::execute_open() +{ + // reset + opened_ = false; + textureinitialized_ = false; + + // Add custom app sink to the gstreamer pipeline + std::string description = description_; + description += " ! appsink name=sink"; + + // parse pipeline descriptor + GError *error = NULL; + pipeline_ = gst_parse_launch(description.c_str(), &error); + if (error != NULL) { + fail(std::string("TextContents: Could not construct pipeline: ") + error->message + "\n" + + description); + g_clear_error(&error); + return; + } + 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); + std::string capstring = "video/x-raw,format=RGBA,width=" + std::to_string(width_) + + ",height=" + std::to_string(height_); + GstCaps *caps = gst_caps_from_string(capstring.c_str()); + if (!caps || !gst_video_info_from_caps(&v_frame_video_info_, caps)) { + fail("TextContents: Could not configure video frame info"); + return; + } + + // setup appsink + GstElement *sink = gst_bin_get_by_name(GST_BIN(pipeline_), "sink"); + if (!sink) { + fail("TextContents: Could not configure pipeline sink."); + return; + } + + // instruct sink to use the required caps + gst_app_sink_set_caps(GST_APP_SINK(sink), caps); + + // Instruct appsink to drop old buffers when the maximum amount of queued buffers is reached. + gst_app_sink_set_max_buffers(GST_APP_SINK(sink), 30); + gst_app_sink_set_drop(GST_APP_SINK(sink), true); + + // set the callbacks + GstAppSinkCallbacks callbacks; +#if GST_VERSION_MINOR > 18 && GST_VERSION_MAJOR > 0 + callbacks.new_event = NULL; +#endif + callbacks.new_preroll = callback_new_preroll; + callbacks.eos = callback_end_of_stream; + callbacks.new_sample = callback_new_sample; + gst_app_sink_set_callbacks(GST_APP_SINK(sink), &callbacks, this, NULL); + gst_app_sink_set_emit_signals(GST_APP_SINK(sink), false); + + // + // TextContents + // + // keep source and text elements for dynamic properties change + src_ = gst_bin_get_by_name(GST_BIN(pipeline_), "src"); + txt_ = gst_bin_get_by_name(GST_BIN(pipeline_), "txt"); + + if (txt_) { + if (src_) { + // set the location of the file + g_object_set(G_OBJECT(src_), "location", text_.c_str(), NULL); + } + else { + // set the content of the text overlay + g_object_set(G_OBJECT(txt_), "text", text_.c_str(), NULL); + } + + g_object_set(G_OBJECT(txt_), "font-desc", fontdesc_.c_str(), NULL); + g_object_set(G_OBJECT(txt_), "halignment", halignment_, NULL); + g_object_set(G_OBJECT(txt_), "valignment", valignment_, NULL); + } + + + // set to desired state (PLAY or PAUSE) + live_ = false; + GstStateChangeReturn ret = gst_element_set_state(pipeline_, desired_state_); + if (ret == GST_STATE_CHANGE_FAILURE) { + fail(std::string("TextContents: Could not open ") + description_); + return; + } else if (ret == GST_STATE_CHANGE_NO_PREROLL) { + Log::Info("TextContents: %s is a live stream", std::to_string(id_).c_str()); + live_ = true; + } + + // instruct the sink to send samples synched in time if not live source + gst_base_sink_set_sync(GST_BASE_SINK(sink), !live_); + + // done with refs + gst_object_unref(sink); + gst_caps_unref(caps); + + // all good + Log::Info("TextContents: %s Opened '%s' (%d x %d)", + std::to_string(id_).c_str(), + description.c_str(), + width_, + height_); + opened_ = true; + + // launch a timeout to check on open status + std::thread(timeout_initialize, this).detach(); +} + +void TextContents::open(const std::string &text, glm::ivec2 res) +{ + std::string gstreamer_pattern + = "videotestsrc pattern=black background-color=0x00000000 ! " + "textoverlay text=\"! no text !\" halignment=center valignment=center "; + + if (!text.empty() && text_.compare(text) != 0) { + // set text + text_ = text; + // test if text is the filename of a subtitle + if (TextContents::SubtitleDiscoverer(text_)) { + // setup a pipeline that reads the file and parses subtitle + // Log::Info("Using %s as subtitle file", text.c_str()); + gstreamer_pattern = "filesrc name=src ! subparse ! queue ! txt. "; + } else { + // else, setup a pipeline with custom appsrc + // Log::Info("Using '%s' as raw text content", text.c_str()); + gstreamer_pattern = ""; + } + gstreamer_pattern += "videotestsrc name=bg pattern=black background-color=0x00000000 ! " + "textoverlay name=txt "; + } + + // (private) open stream + Stream::open(gstreamer_pattern, res.x, res.y); +} + +void TextContents::setText(const std::string &t) +{ + if ( src_ == nullptr && !t.empty() && text_.compare(t) != 0) { + // set text + text_ = t; + // apply if ready + if (txt_) + g_object_set(G_OBJECT(txt_),"text", text_.c_str(), NULL); + } +} + +void TextContents::setFontDescriptor(const std::string &fd) +{ + if (!fd.empty() && fontdesc_.compare(fd) != 0) { + // set text + fontdesc_ = fd; + // apply if ready + if (txt_) + g_object_set(G_OBJECT(txt_),"font-desc", fontdesc_.c_str(), NULL); + } +} + +void TextContents::setHalignment(uint h) +{ + if ( halignment_ != h ) { + // set value + halignment_ = h; + // apply if ready + if (txt_) + g_object_set(G_OBJECT(txt_),"halignment", halignment_, NULL); + } +} + +void TextContents::setValignment(uint v) +{ + if ( valignment_ != v ) { + // set value + valignment_ = v; + // apply if ready + if (txt_) + g_object_set(G_OBJECT(txt_),"valignment", valignment_, NULL); + } +} + +TextSource::TextSource(uint64_t id) : StreamSource(id) +{ + // create stream + stream_ = static_cast( new TextContents ); + + // set symbol + symbol_ = new Symbol(Symbol::TEXT, glm::vec3(0.75f, 0.75f, 0.01f)); + symbol_->scale_.y = 1.5f; +} + + +void TextSource::setContents(const std::string &c, glm::ivec2 resolution) +{ + // set contents + contents()->open( c, resolution ); + + // play gstreamer + stream_->play(true); + + // delete and reset render buffer to enforce re-init of StreamSource + if (renderbuffer_) + delete renderbuffer_; + renderbuffer_ = nullptr; + + // will be ready after init and one frame rendered + ready_ = false; +} + +TextContents *TextSource::contents() const +{ + return dynamic_cast(stream_); +} + +void TextSource::accept(Visitor& v) +{ + StreamSource::accept(v); + v.visit(*this); +} + +glm::ivec2 TextSource::icon() const +{ + return glm::ivec2(ICON_SOURCE_TEXT); +} + +std::string TextSource::info() const +{ + if ( contents()->isSubtitle() ) + return "Subtitle text"; + else + return "Free text"; +} diff --git a/src/TextSource.h b/src/TextSource.h new file mode 100644 index 0000000..bdf5166 --- /dev/null +++ b/src/TextSource.h @@ -0,0 +1,74 @@ +#ifndef TEXTSOURCE_H +#define TEXTSOURCE_H + +#include "StreamSource.h" +#include + +class TextContents : public Stream +{ +public: + TextContents(); + void open(const std::string &contents, glm::ivec2 res); + + void setText(const std::string &t); + inline std::string text() const { return text_; } + + inline bool isSubtitle() const { return src_ != nullptr; } + static bool SubtitleDiscoverer(const std::string &path); + + void setFontDescriptor(const std::string &fd); + inline std::string fontDescriptor() const { return fontdesc_; } + /* (0): left - left + * (1): center - center + * (2): right - right + * (4): Absolute position clamped to canvas - position + * (5): Absolute position - absolute + * */ + void setHalignment(uint h); + inline uint halignment() const { return halignment_; } + /* (0): baseline - baseline + * (1): bottom - bottom + * (2): top - top + * (3): Absolute position clamped to canvas - position + * (4): center - center + * (5): Absolute position - absolute + * */ + void setValignment(uint v); + inline uint valignment() const { return valignment_; } + +private: + GstElement *src_; + GstElement *txt_; + void execute_open() override; + void push_data(); + + std::string text_; + std::string fontdesc_; + uint halignment_; + uint valignment_; +}; + + +class TextSource : public StreamSource +{ + +public: + TextSource(uint64_t id = 0); + + // Source interface + void accept (Visitor& v) override; + glm::ivec2 icon() const override; + std::string info() const override; + + // StreamSource interface + Stream *stream() const override { return stream_; } + + // Text specific interface + void setContents(const std::string &p, glm::ivec2 resolution); + TextContents *contents() const; + +}; + + + +#endif // TEXTSOURCE_H diff --git a/src/UserInterfaceManager.cpp b/src/UserInterfaceManager.cpp index 60d7d90..a658652 100644 --- a/src/UserInterfaceManager.cpp +++ b/src/UserInterfaceManager.cpp @@ -2730,7 +2730,7 @@ void Navigator::clearNewPannel() { new_source_preview_.setSource(); pattern_type = -1; - custom_pipeline = false; + generated_type = -1; custom_connected = false; custom_screencapture = false; sourceSequenceFiles.clear(); @@ -3748,6 +3748,7 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) // Generated patterns Source creator else if (Settings::application.source.new_type == SOURCE_GENERATED){ + static DialogToolkit::OpenSubtitleDialog subtitleopenialog("Open Subtitle"); bool update_new_source = false; ImGui::Text("Patterns & generated graphics"); @@ -3757,7 +3758,12 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) { if ( ImGui::Selectable("Custom gstreamer " ICON_FA_CODE) ) { update_new_source = true; - custom_pipeline = true; + generated_type = 0; + pattern_type = -1; + } + if ( ImGui::Selectable("Text " ICON_FA_CODE) ) { + update_new_source = true; + generated_type = 1; pattern_type = -1; } for (int p = 0; p < (int) Pattern::count(); ++p){ @@ -3765,20 +3771,25 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) std::string label = pattern.label + (pattern.animated ? " " ICON_FA_PLAY_CIRCLE : " "); if (pattern.available && ImGui::Selectable( label.c_str(), p == pattern_type )) { update_new_source = true; - custom_pipeline = false; + generated_type = 2; pattern_type = p; } } ImGui::EndCombo(); } + static ImVec2 fieldsize(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 100); + static int numlines = 0; + const ImGuiContext& g = *GImGui; + fieldsize.y = MAX(3, numlines) * g.FontSize + g.Style.ItemSpacing.y + g.Style.FramePadding.y; + // Indication ImGui::SameLine(); ImGuiToolkit::HelpToolTip("Create a source with patterns or graphics generated algorithmically. " - "Entering a custom gstreamer pipeline is also possible."); + "Entering text or a custom gstreamer pipeline is also possible."); ImGui::Spacing(); - if (custom_pipeline) { + if (generated_type == 0) { static std::vector< std::pair< std::string, std::string> > _examples = { {"Videotest", "videotestsrc horizontal-speed=1 ! video/x-raw, width=640, height=480 " }, {"Checker", "videotestsrc pattern=checkers-8 ! video/x-raw, width=64, height=64 "}, {"Color", "videotestsrc pattern=gradient foreground-color= 0xff55f54f background-color= 0x000000 "}, @@ -3787,10 +3798,6 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) {"SRT listener", "srtsrc uri=\"srt://:5000?mode=listener\" ! decodebin "} }; static std::string _description = _examples[0].second; - static ImVec2 fieldsize(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 100); - static int numlines = 0; - const ImGuiContext& g = *GImGui; - fieldsize.y = MAX(3, numlines) * g.FontSize + g.Style.ItemSpacing.y + g.Style.FramePadding.y; // Editor if ( ImGuiToolkit::InputCodeMultiline("Pipeline", &_description, fieldsize, &numlines) ) @@ -3801,7 +3808,6 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) ImGui::SetCursorPos( pos_bot + ImVec2(fieldsize.x + IMGUI_SAME_LINE, -ImGui::GetFrameHeightWithSpacing())); if (ImGui::BeginCombo("##Examples", "Examples", ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLarge)) { ImGui::TextDisabled("Examples"); - ImGui::Separator(); for (auto it = _examples.begin(); it != _examples.end(); ++it) { if (ImGui::Selectable( it->first.c_str() ) ) { _description = it->second; @@ -3822,10 +3828,108 @@ void Navigator::RenderNewPannel(const ImVec2 &iconsize) new_source_preview_.setSource( Mixer::manager().createSourceStream(_description), "Custom"); } + // if text source selected + else if (generated_type == 1) { + static std::vector > _examples + = {{"Hello", "Hello world!"}, + {"Italics", "Text in italics"}, + {"Multiline", "One\nTwo\nThree\nFour\nFive"}, + {"Code", "Monospace"}}; + static std::string _contents = _examples[0].second; + + // Editor + if (SystemToolkit::file_exists(_contents)) { + static char dummy_str[1024]; + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + snprintf(dummy_str, 1024, "%s", _contents.c_str()); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f)); + ImGui::InputText("##Filesubtitle", + dummy_str, + IM_ARRAYSIZE(dummy_str), + ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(1); + } else if (ImGuiToolkit::InputTextMultiline("Text", &_contents, fieldsize, &numlines)) + update_new_source = true; + + // Local menu for list of examples + ImVec2 pos_bot = ImGui::GetCursorPos(); + ImGui::SetCursorPos( + pos_bot + + ImVec2(fieldsize.x + IMGUI_SAME_LINE, -ImGui::GetFrameHeightWithSpacing())); + if (ImGui::BeginCombo("##Examples", + "Examples", + ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLarge)) { + if (ImGui::Selectable(ICON_FA_FOLDER_OPEN " Open subtitle")) + subtitleopenialog.open(); + ImGui::Separator(); + ImGui::TextDisabled("Examples"); + for (auto it = _examples.begin(); it != _examples.end(); ++it) { + if (ImGui::Selectable(it->first.c_str())) { + _contents = it->second; + update_new_source = true; + } + } + ImGui::Separator(); + ImGui::TextDisabled("Explore online"); + if (ImGui::Selectable(ICON_FA_EXTERNAL_LINK_ALT " Pango syntax")) + SystemToolkit::open("https://docs.gtk.org/Pango/pango_markup.html"); + if (ImGui::Selectable(ICON_FA_EXTERNAL_LINK_ALT " Gstreamer")) + SystemToolkit::open( + "https://gstreamer.freedesktop.org/documentation/pango/" + "textoverlay.html?gi-language=c#textoverlay-page"); + if (ImGui::Selectable(ICON_FA_EXTERNAL_LINK_ALT " SubRip files")) + SystemToolkit::open("https://en.wikipedia.org/wiki/SubRip"); + ImGui::EndCombo(); + } + ImGui::SameLine(0, IMGUI_SAME_LINE); + ImGuiToolkit::Indication("More text layout options are available after source creation.", ICON_FA_INFO_CIRCLE); + ImGui::SetCursorPos(pos_bot); + + // resolution + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if (ImGui::Combo("Ratio", + &Settings::application.source.ratio, + GlmToolkit::aspect_ratio_names, + IM_ARRAYSIZE(GlmToolkit::aspect_ratio_names))) + update_new_source = true; + + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if (ImGui::Combo("Height", + &Settings::application.source.res, + GlmToolkit::height_names, + IM_ARRAYSIZE(GlmToolkit::height_names))) + update_new_source = true; + + // get subtitle file if dialog finished + if (subtitleopenialog.closed()) { + // get the filename from this file dialog + std::string importpath = subtitleopenialog.path(); + // open file + if (!importpath.empty()) { + _contents = importpath; + update_new_source = true; + } + } + + // take action + if (update_new_source) { + glm::ivec2 res = GlmToolkit::resolutionFromDescription(Settings::application.source.ratio, Settings::application.source.res); + new_source_preview_.setSource(Mixer::manager().createSourceText(_contents, res), "Text"); + } + } // if pattern selected else { // resolution if (pattern_type >= 0) { + + static char dummy_str[1024]; + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + pattern_descriptor pattern = Pattern::get(pattern_type); + snprintf(dummy_str, 1024, "%s", pattern.label.c_str()); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f)); + ImGui::InputText("Pattern", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(1); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); if (ImGui::Combo("Ratio", &Settings::application.source.ratio, GlmToolkit::aspect_ratio_names, IM_ARRAYSIZE(GlmToolkit::aspect_ratio_names) ) ) diff --git a/src/UserInterfaceManager.h b/src/UserInterfaceManager.h index eafafed..0a088f1 100644 --- a/src/UserInterfaceManager.h +++ b/src/UserInterfaceManager.h @@ -56,7 +56,7 @@ class Navigator bool selected_button[NAV_COUNT]; int selected_index; int pattern_type; - bool custom_pipeline; + int generated_type; bool custom_connected; bool custom_screencapture; void clearButtonSelection(); diff --git a/src/Visitor.h b/src/Visitor.h index 2937afd..278ff59 100644 --- a/src/Visitor.h +++ b/src/Visitor.h @@ -43,6 +43,7 @@ class CloneSource; class NetworkSource; class MixingGroup; class MultiFileSource; +class TextSource; class FrameBufferFilter; class PassthroughFilter; @@ -115,6 +116,7 @@ public: virtual void visit (RenderSource&) {} virtual void visit (CloneSource&) {} virtual void visit (MultiFileSource&) {} + virtual void visit (TextSource&) {} virtual void visit (FrameBufferFilter&) {} virtual void visit (PassthroughFilter&) {} diff --git a/src/defines.h b/src/defines.h index 1e92827..b2637e0 100644 --- a/src/defines.h +++ b/src/defines.h @@ -22,6 +22,7 @@ "*.flv", "*.hevc", "*.asf", "*.jpg", "*.png", "*.gif",\ "*.tif", "*.tiff", "*.webp", "*.bmp", "*.ppm", "*.svg," #define IMAGES_FILES_PATTERN "*.jpg", "*.png", "*.bmp", "*.ppm", "*.gif" +#define SUBTITLE_FILES_PATTERN "*.srt", "*.sub" #define MINI(a, b) (((a) < (b)) ? (a) : (b)) #define MAXI(a, b) (((a) > (b)) ? (a) : (b))