Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

### Features

- Add a "mirror mode" to the speaker view, which mirrors the entire screen you
are sharing with the audience (#188)

### Fix

## [v0.7.0] The Slipshow of Dorian Gray (Wednesday 26th November, 2025)

## Compiler
Expand Down
71 changes: 71 additions & 0 deletions src/engine/scheduler/date.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
let date = Jv.get Jv.global "Date"
let now () = Jv.call date "now" [||] |> Jv.to_int

let soi i =
if i = 0 then "00"
else if i < 10 then "0" ^ string_of_int i
else string_of_int i

let string_of_t ms =
let t = ms / 1000 in
let s = t mod 60 in
let m = t / 60 in
let h = m / 60 in
let m = m mod 60 in
soi h ^ ":" ^ soi m ^ ":" ^ soi s

let setup_timer el =
let timer_mode = ref (`Since (now ())) in
let timer = Brr.El.span [] in
let restart =
Brr.El.input
~at:[ Brr.At.type' (Jstr.v "button"); Brr.At.value (Jstr.v "Restart") ]
()
in
let pause =
Brr.El.input
~at:
[
Brr.At.type' (Jstr.v "button");
Brr.At.value (Jstr.v "Play/Pause");
Brr.At.style (Jstr.v "margin-left: 20px");
]
()
in
let current_time = ref "" in
let _ =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
match !timer_mode with
| `Since _ -> timer_mode := `Since (now ())
| `Paused_at _ -> timer_mode := `Paused_at 0)
(Brr.El.as_target restart)
in
let _ =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
match !timer_mode with
| `Since n -> timer_mode := `Paused_at (now () - n)
| `Paused_at n -> timer_mode := `Since (now () - n))
(Brr.El.as_target pause)
in
Brr.El.set_children el [ timer; pause; restart ];
Brr.G.set_interval ~ms:100 (fun () ->
let v =
match !timer_mode with `Since n -> now () - n | `Paused_at n -> n
in
let new_current_time = "⏱️ " ^ string_of_t v in
if not (String.equal !current_time new_current_time) then (
Brr.El.set_children timer [ Brr.El.txt' new_current_time ];
current_time := new_current_time))

let clock el =
let write_date () =
let now = Jv.new' date [||] in
let hours = Jv.call now "getHours" [||] |> Jv.to_int in
let minutes = Jv.call now "getMinutes" [||] |> Jv.to_int in
Brr.El.set_children el
[ Brr.El.txt' ("⏰ " ^ soi hours ^ ":" ^ soi minutes) ]
in
write_date ();
Brr.G.set_interval ~ms:20000 write_date
157 changes: 73 additions & 84 deletions src/engine/scheduler/scheduler.ml
Original file line number Diff line number Diff line change
@@ -1,77 +1,3 @@
module Date = struct
let date = Jv.get Jv.global "Date"
let now () = Jv.call date "now" [||] |> Jv.to_int

let soi i =
if i = 0 then "00"
else if i < 10 then "0" ^ string_of_int i
else string_of_int i

let string_of_t ms =
let t = ms / 1000 in
let s = t mod 60 in
let m = t / 60 in
let h = m / 60 in
let m = m mod 60 in
soi h ^ ":" ^ soi m ^ ":" ^ soi s

let setup_timer el =
let timer_mode = ref (`Since (now ())) in
let timer = Brr.El.span [] in
let restart =
Brr.El.input
~at:[ Brr.At.type' (Jstr.v "button"); Brr.At.value (Jstr.v "Restart") ]
()
in
let pause =
Brr.El.input
~at:
[
Brr.At.type' (Jstr.v "button");
Brr.At.value (Jstr.v "Play/Pause");
Brr.At.style (Jstr.v "margin-left: 20px");
]
()
in
let current_time = ref "" in
let _ =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
match !timer_mode with
| `Since _ -> timer_mode := `Since (now ())
| `Paused_at _ -> timer_mode := `Paused_at 0)
(Brr.El.as_target restart)
in
let _ =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
match !timer_mode with
| `Since n -> timer_mode := `Paused_at (now () - n)
| `Paused_at n -> timer_mode := `Since (now () - n))
(Brr.El.as_target pause)
in
Brr.El.set_children el [ timer; pause; restart ];
Brr.G.set_interval ~ms:100 (fun () ->
let v =
match !timer_mode with `Since n -> now () - n | `Paused_at n -> n
in
let new_current_time = "⏱️ " ^ string_of_t v in
if not (String.equal !current_time new_current_time) then (
Brr.El.set_children timer [ Brr.El.txt' new_current_time ];
current_time := new_current_time))

let clock el =
let write_date () =
let now = Jv.new' date [||] in
let hours = Jv.call now "getHours" [||] |> Jv.to_int in
let minutes = Jv.call now "getMinutes" [||] |> Jv.to_int in
Brr.El.set_children el
[ Brr.El.txt' ("⏰ " ^ soi hours ^ ":" ^ soi minutes) ]
in
write_date ();
Brr.G.set_interval ~ms:20000 write_date
end

module Msg = struct
type msg = Communication.t

Expand All @@ -90,16 +16,59 @@ let html =
{|
<!doctype html>
<html>
<body>
<body class="clone-mode">
<div id=slipshow__mirror-view><video autoplay></video></div>
<iframe name="slipshow_speaker_view" id="speaker-view"></iframe>
<div id="speaker-notes"><div id="slswrapper"><div id="timer"></div><div id="clock"></div><h2>Notes</h2><div id="notes_div"></div></div></div>
<script>
document.getElementById('speaker-view').addEventListener('load', function () {
// Ensure iframe gets focus
this.contentWindow.focus();
});
</script>
<div id="speaker-notes">
<div id="slswrapper">
<div id="timer"></div>
<div id="clock"></div>
<p id=mirror-button-div>
<button id="slipshow__mirror-view-button">Use Mirror view</button>
<span>Mirror an other screen. Select the screen your audience sees, and move the mouse there. Useful if you need to interact with the presentation, or another window, but still want to see your notes.</span>
</p>
<p id=clone-button-div>
<button id="slipshow__cloned-view-button">Stop Mirror view</button>
<span>Stop mirroring another screen, and go back to a synchronized clone.</span>
</p>
<h2>Notes</h2>
<div id="notes_div"></div>
</div>
</div>
<script>
document.getElementById('speaker-view').addEventListener('load', function () {
// Ensure iframe gets focus
this.contentWindow.focus();
});
</script>
<style>
#slipshow__mirror-view {
background: black;
}
button, input[type=button] {
font-size: 20px;
}
.mirror-mode #slipshow__mirror-view {
display: flex;
width: 100%;
}
.mirror-mode #slipshow__mirror-view video {
width: 100%;
}
.clone-mode #slipshow__mirror-view {
display: none;
}
.mirror-mode #speaker-view {
display: none;
}
.clone-mode #speaker-view {
}
.mirror-mode #mirror-button-div {
display: none;
}
.clone-mode #clone-button-div {
display: none;
}
#timer {
font-size: 2em;
}
Expand All @@ -119,11 +88,12 @@ document.getElementById('speaker-view').addEventListener('load', function () {
display: flex;
}
#speaker-view {
width:60%;
width:100%;
}
#speaker-notes {
width:40%;
width:35%;
overflow: scroll;
flex-shrink:0;
}
</style>
</body>
Expand Down Expand Up @@ -195,6 +165,25 @@ let open_window handle_msg =
in
speaker_view_ref := Some (child, child_iframe);
Brr.El.set_at (Jstr.v "srcdoc") (Some src) child_iframe;
let mirror_button =
Brr.El.find_first_by_selector ~root:el
(Jstr.v "#slipshow__mirror-view-button")
|> Option.get
in
let clone_button =
Brr.El.find_first_by_selector ~root:el
(Jstr.v "#slipshow__cloned-view-button")
|> Option.get
in
let mirror_video =
Brr.El.find_first_by_selector ~root:el
(Jstr.v "#slipshow__mirror-view video")
|> Option.get |> Brr_io.Media.El.of_el
in
let () =
View_mode.setup ~speaker_view:child ~video_el:mirror_video
~mirror_button ~clone_button ~child_iframe ~src
in
let timer =
Brr.El.find_first_by_selector ~root:el (Jstr.v "#timer")
|> Option.get
Expand Down
48 changes: 48 additions & 0 deletions src/engine/scheduler/view_mode.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
let setup ~speaker_view:child ~video_el:mirror_video ~mirror_button
~clone_button ~child_iframe ~src =
let _unlisten =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
let open Fut.Syntax in
let _ : unit Fut.t =
let ( !! ) = Jstr.v in
Brr.El.set_class !!"clone-mode" false
(Brr.Document.body (Brr.Window.document child));
Brr.El.set_class !!"mirror-mode" true
(Brr.Document.body (Brr.Window.document child));
let child_navigator =
Jv.get (Brr.Window.to_jv child) "navigator" |> Brr.Navigator.of_jv
in
let devices = Brr_io.Media.Devices.of_navigator child_navigator in
let constraints = Brr_io.Media.Stream.Constraints.av () in
let+ media =
Brr_io.Media.Devices.get_display_media devices constraints
in
match media with
| Ok stream ->
let provider = Brr_io.Media.El.Provider.of_media_stream stream in
Brr_io.Media.El.set_src_object mirror_video (Some provider);
Brr.El.set_at (Jstr.v "srcdoc") None child_iframe
| Error e -> Brr.Console.(error [ e ])
in
())
(Brr.El.as_target mirror_button)
in
let _unlisten =
Brr.Ev.listen Brr.Ev.click
(fun _ ->
let ( !! ) = Jstr.v in
Brr.El.set_at (Jstr.v "srcdoc") (Some src) child_iframe;
Brr.El.set_class !!"clone-mode" true
(Brr.Document.body (Brr.Window.document child));
Brr.El.set_class !!"mirror-mode" false
(Brr.Document.body (Brr.Window.document child));
let tracks =
let stream = Brr_io.Media.El.capture_stream mirror_video in
Brr_io.Media.Stream.get_tracks stream
in
List.iter (fun t -> Brr_io.Media.Track.stop t) tracks;
Brr_io.Media.El.set_src_object mirror_video None)
(Brr.El.as_target clone_button)
in
()
8 changes: 8 additions & 0 deletions src/engine/scheduler/view_mode.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
val setup :
speaker_view:Brr.El.window ->
video_el:Brr_io.Media.El.t ->
mirror_button:Brr.El.t ->
clone_button:Brr.El.t ->
child_iframe:Brr.El.t ->
src:Jstr.t ->
unit
2 changes: 1 addition & 1 deletion test/engine/campus_du_libre.t/run.t
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@


$ slipshow compile cdl.md
$ cp cdl.html /tmp/
$ cp cdl.html /tmp/

Loading