class Application < Lively::Application
- Inherits from
Lively::Application
Represents the main Presently application middleware.
Routes the presentation, recording, playback, export, and live interfaces.
Creates a shared class Presently::PresentationController that keeps connected clients in sync.
Definitions
def initialize(delegate, slides_root: "slides", templates_roots: [], recordings_root: nil, playback_recordings_root: nil)
Initialize a new Presently application.
Signature
-
parameter
delegateProtocol::HTTP::Middleware The next middleware in the chain.
-
parameter
slides_rootString The directory containing slide files.
-
parameter
templates_rootsArray(String) Additional directories to search for templates.
-
parameter
recordings_rootString | Nil Directory where source narration is stored. Defaults to
audiobeside the slides directory.-
parameter
playback_recordings_rootString | Nil Directory containing normalized narration. Defaults to
audio-normalizedbeside the slides directory.
Implementation
def initialize(delegate, slides_root: "slides", templates_roots: [], recordings_root: nil, playback_recordings_root: nil)
@slides_root = slides_root
@templates_roots = templates_roots
@recordings = Recordings.new(recordings_root || File.expand_path("../audio", slides_root))
@playback_recordings = Recordings.new(playback_recordings_root || File.expand_path("../audio-normalized", slides_root))
slide_assets = SlideAssets.new(delegate, root: @slides_root, stylesheets: ->{controller.presentation.stylesheets})
super(slide_assets)
end
def allowed_views
The view classes that this application allows.
Signature
-
returns
Array(Class) The allowed view classes.
Implementation
def allowed_views
[HomeView, DisplayView, PresenterView, RecorderView, RecordingControlsView]
end
def state
The shared state passed to all views via the resolver.
Signature
-
returns
Hash The controller as keyword state.
Implementation
def state
{controller: controller}
end
def controller
The shared presentation controller.
Signature
-
returns
PresentationController The controller instance.
Implementation
def controller
@controller ||= begin
templates = Templates.for(@templates_roots)
presentation = Presentation.load(@slides_root, templates)
PresentationController.new(presentation, state: State.new, recordings: @recordings)
end
end
def title
The application title shown in the browser.
Signature
-
returns
String The page title.
Implementation
def title
"Presently"
end
def make_page(view, interface: nil)
Create a Presently page with the presentation-specific stylesheets.
Signature
-
parameter
viewLive::View The root view for the page.
-
parameter
interfaceSymbol | Nil The interface-specific stylesheet to load.
-
returns
Page The presentation page.
Implementation
def make_page(view, interface: nil)
stylesheets = controller.presentation.stylesheets.map(&:url)
Page.new(title: title, body: view, interface: interface, stylesheets: stylesheets)
end
def configure_routes(router)
Add Presently's application routes.
Signature
-
parameter
routerLively::Router::Builder The router to configure.
Implementation
def configure_routes(router)
router.get("/") do
body = resolver.root(HomeView)
Page.new(title: title, body: body, interface: :home).call
end
router.get("/display"){make_page(resolver.root(DisplayView), interface: :display).call}
router.get("/presenter"){make_page(resolver.root(PresenterView), interface: :presenter).call}
router.get("/recorder"){make_page(resolver.root(RecorderView), interface: :recorder).call}
router.route("/recordings", methods: ["GET", "HEAD", "PUT", "PATCH"]) do |request|
handle_recording(request, request_parameters(request))
end
router.route("/playback/recordings", methods: ["GET", "HEAD"]) do |request|
handle_playback_recording(request, request_parameters(request))
end
router.get("/playback") do |request|
render_playback(request_parameters(request))
end
router.get("/export") do |request|
render_export(request_parameters(request))
end
end
def request_parameters(request)
Parse query parameters from the incoming request target.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
returns
Hash The decoded query parameters.
Implementation
def request_parameters(request)
Protocol::URL::Reference[request.path].parse_query!
end
def render_playback(parameters)
Render the narrated playback interface.
Implementation
def render_playback(parameters)
presentation = Presentation.load(@slides_root, controller.templates)
recording_urls = presentation.slides.each_index.map do |index|
slide = presentation.slides[index]
if @playback_recordings.exist?(slide) || @recordings.exist?(slide)
"/playback/recordings?index=#{index}"
end
end
playback = Playback.new(
presentation: presentation,
recording_urls: recording_urls,
**Playback.options_from_parameters(parameters),
)
Protocol::HTTP::Response[200, [["content-type", "text/html"]], [playback.call]]
end
def render_export(parameters)
Render the printable export interface.
Implementation
def render_export(parameters)
presentation = Presentation.load(@slides_root, controller.templates)
export = Export.new(presentation: presentation, **Export.options_from_parameters(parameters))
Protocol::HTTP::Response[200, [["content-type", "text/html"]], [export.call]]
end
def handle_recording(request, parameters)
Handle reading and writing a slide recording.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
parameter
parametersHash The decoded query parameters.
-
returns
Protocol::HTTP::Response
Implementation
def handle_recording(request, parameters)
index = recording_index(parameters)
return Protocol::HTTP::Response[400, [], ["A valid slide index is required."]] unless index
slide = controller.slides[index]
return Protocol::HTTP::Response[404, [], ["Slide not found."]] unless slide
case request.method
when "GET", "HEAD"
serve_recording(request, slide, @recordings)
when "PUT"
duration = recording_duration(parameters)
if parameters.key?("duration") && !duration
return invalid_recording_duration
end
store_recording(request, slide, duration: duration)
when "PATCH"
duration = recording_duration(parameters)
return invalid_recording_duration unless duration
return Protocol::HTTP::Response[404, [], ["Recording not found."]] unless @recordings.exist?(slide)
slide.update_duration!(duration)
Protocol::HTTP::Response[200, [["content-type", "application/json"]], ["{\"updated\":true,\"duration\":#{duration}}"]]
end
end
def recording_duration(parameters)
Parse an optional positive recording duration.
Signature
-
parameter
parametersHash The decoded query parameters.
-
returns
Integer | Nil The duration in seconds, or
nilwhen absent or invalid.
Implementation
def recording_duration(parameters)
if value = parameters["duration"]
duration = Integer(value, exception: false)
return duration if duration&.positive?
end
end
def invalid_recording_duration
Build the response for an invalid recording duration.
Signature
-
returns
Protocol::HTTP::Response A bad request response.
Implementation
def invalid_recording_duration
Protocol::HTTP::Response[400, [], ["Duration must be a positive number of seconds."]]
end
def handle_playback_recording(request, parameters)
Serve normalized narration for playback, falling back to the source take.
Implementation
def handle_playback_recording(request, parameters)
index = recording_index(parameters)
return Protocol::HTTP::Response[400, [], ["A valid slide index is required."]] unless index
slide = controller.slides[index]
return Protocol::HTTP::Response[404, [], ["Slide not found."]] unless slide
recordings = @playback_recordings.exist?(slide) ? @playback_recordings : @recordings
serve_recording(request, slide, recordings)
end
def recording_index(parameters)
Extract the slide index from decoded query parameters.
Signature
-
parameter
parametersHash The decoded query parameters.
-
returns
Integer | Nil
Implementation
def recording_index(parameters)
Integer(parameters.fetch("index"))
rescue ArgumentError, KeyError
nil
end
def serve_recording(request, slide, recordings)
Serve an existing recording.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
parameter
slideSlide The requested slide.
-
returns
Protocol::HTTP::Response
Implementation
def serve_recording(request, slide, recordings)
unless recordings.exist?(slide)
return Protocol::HTTP::Response[404, [], ["Recording not found."]]
end
headers = [
["content-type", Recordings::CONTENT_TYPE],
["cache-control", "no-store"],
]
body = recordings.read(slide) unless request.method == "HEAD"
Protocol::HTTP::Response[200, headers, body]
end
def store_recording(request, slide, duration: nil)
Store an uploaded recording.
Signature
-
parameter
requestProtocol::HTTP::Request The incoming request.
-
parameter
slideSlide The slide being recorded.
-
returns
Protocol::HTTP::Response
Implementation
def store_recording(request, slide, duration: nil)
content_type = request.headers["content-type"]&.split(";", 2)&.first
unless content_type == Recordings::CONTENT_TYPE
return Protocol::HTTP::Response[415, [], ["Expected #{Recordings::CONTENT_TYPE}."]]
end
unless request.body
return Protocol::HTTP::Response[400, [], ["A recording body is required."]]
end
@recordings.write(slide, request.body)
slide.update_duration!(duration) if duration
result = duration ? "{\"saved\":true,\"duration\":#{duration}}" : "{\"saved\":true}"
Protocol::HTTP::Response[201, [["content-type", "application/json"]], [result]]
rescue Recordings::TooLarge => error
Protocol::HTTP::Response[413, [], [error.message]]
end