Skip to content

Commit

Permalink
Add Kino.Audio and Kino.Video (#311)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonatan Kłosko <[email protected]>
  • Loading branch information
fazibear and jonatanklosko authored Aug 8, 2023
1 parent 70fc3ad commit 4759cc5
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 1 deletion.
23 changes: 23 additions & 0 deletions lib/assets/audio/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function init(ctx, [{ type, opts }, content]) {
ctx.root.innerHTML = `
<div class="root">
<audio ${opts} src="${createDataUrl(content, type)}" style="height: 150px"/>
</div>
`;
}

function bufferToBase64(buffer) {
let binaryString = "";
const bytes = new Uint8Array(buffer);
const length = bytes.byteLength;

for (let i = 0; i < length; i++) {
binaryString += String.fromCharCode(bytes[i]);
}

return btoa(binaryString);
};

function createDataUrl(content, type){
return `data:${type};base64,${bufferToBase64(content)}`
};
23 changes: 23 additions & 0 deletions lib/assets/video/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function init(ctx, [{ type, opts }, content]) {
ctx.root.innerHTML = `
<div class="root">
<video ${opts} src="${createDataUrl(content, type)}" style="max-height: 500px"/>
</div>
`;
}

function bufferToBase64(buffer) {
let binaryString = "";
const bytes = new Uint8Array(buffer);
const length = bytes.byteLength;

for (let i = 0; i < length; i++) {
binaryString += String.fromCharCode(bytes[i]);
}

return btoa(binaryString);
};

function createDataUrl(content, type){
return `data:${type};base64,${bufferToBase64(content)}`
};
82 changes: 82 additions & 0 deletions lib/kino/audio.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule Kino.Audio do
@moduledoc """
A kino for rendering a binary audio.
## Examples
content = File.read!("/path/to/audio.wav")
Kino.Audio.new(content, :wav)
content = File.read!("/path/to/audio.wav")
Kino.Audio.new(content, :wav, autoplay: true, loop: true)
"""

use Kino.JS, assets_path: "lib/assets/audio"
use Kino.JS.Live

@type t :: Kino.JS.Live.t()

@type mime_type :: binary()
@type common_audio_type :: :wav | :mp3 | :mpeg | :ogg

@doc """
Creates a new kino displaying the given binary audio.
The given type be either `:wav`, `:mp3`/`:mpeg`, `:ogg`
or a string with audio MIME type.
## Options
* `:autoplay` - whether the audio should start playing as soon as
it is rendered. Defaults to `false`
* `:loop` - whether the audio should loop. Defaults to `false`
* `:muted` - whether the audio should be muted. Defaults to `false`
"""
@spec new(binary(), common_audio_type() | mime_type(), keyword()) :: t()
def new(content, type, opts \\ []) when is_binary(content) do
opts =
Keyword.validate!(opts,
autoplay: false,
loop: false,
muted: false
)

Kino.JS.Live.new(__MODULE__, %{
content: content,
type: mime_type!(type),
opts:
Enum.reduce(opts, "controls", fn {opt, val}, acc ->
if val do
"#{acc} #{opt}"
else
acc
end
end)
})
end

@impl true
def init(assigns, ctx) do
{:ok, assign(ctx, assigns)}
end

@impl true
def handle_connect(%{assigns: %{content: content, type: type, opts: opts}} = ctx) do
payload = {:binary, %{type: type, opts: opts}, content}
{:ok, payload, ctx}
end

defp mime_type!(:wav), do: "audio/wav"
defp mime_type!(:mp3), do: "audio/mpeg"
defp mime_type!(:mpeg), do: "audio/mpeg"
defp mime_type!(:ogg), do: "audio/ogg"
defp mime_type!("audio/" <> _ = mime_type), do: mime_type

defp mime_type!(other) do
raise ArgumentError,
"expected audio type to be either :wav, :mp3, :mpeg, :ogg, or an audio MIME type string, got: #{inspect(other)}"
end
end
84 changes: 84 additions & 0 deletions lib/kino/video.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
defmodule Kino.Video do
@moduledoc """
A kino for rendering a binary video.
## Examples
content = File.read!("/path/to/video.mp4")
Kino.Video.new(content, :mp4)
content = File.read!("/path/to/video.mp4")
Kino.Video.new(content, :mp4, autoplay: true, loop: true)
"""

use Kino.JS, assets_path: "lib/assets/video"
use Kino.JS.Live

@type t :: Kino.JS.Live.t()

@type mime_type :: binary()
@type common_video_type :: :mp4 | :ogg | :avi | :mwv | :mov

@doc """
Creates a new kino displaying the given binary video.
The given type be either `:mp4`, `:ogg`, `:avi`, `:wmv`, `:mov`
or a string with video MIME type.
## Options
* `:autoplay` - whether the video should start playing as soon as
it is rendered. Defaults to `false`
* `:loop` - whether the video should loop. Defaults to `false`
* `:muted` - whether the video should be muted. Defaults to `false`
"""
@spec new(binary(), common_video_type() | mime_type(), list()) :: t()
def new(content, type, opts \\ []) when is_binary(content) do
opts =
Keyword.validate!(opts,
autoplay: false,
loop: false,
muted: false
)

Kino.JS.Live.new(__MODULE__, %{
content: content,
type: mime_type!(type),
opts:
Enum.reduce(opts, "controls", fn {opt, val}, acc ->
if val do
"#{acc} #{opt}"
else
acc
end
end)
})
end

@impl true
def init(assigns, ctx) do
{:ok, assign(ctx, assigns)}
end

@impl true
def handle_connect(%{assigns: %{content: content, type: type, opts: opts}} = ctx) do
payload = {:binary, %{type: type, opts: opts}, content}
{:ok, payload, ctx}
end

defp mime_type!(:mp4), do: "video/mp4"
defp mime_type!(:ogg), do: "video/ogg"
defp mime_type!(:avi), do: "video/x-msvideo"
defp mime_type!(:wmv), do: "video/x-ms-wmv"
defp mime_type!(:mov), do: "video/quicktime"
defp mime_type!("video/" <> _ = mime_type), do: mime_type

defp mime_type!(other) do
raise ArgumentError,
"expected video type to be either :mp4, :ogg, :avi, :wmv, :mov, or an video MIME type string, got: #{inspect(other)}"
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ defmodule Kino.MixProject do
Kino.Markdown,
Kino.Mermaid,
Kino.Text,
Kino.Tree
Kino.Tree,
Kino.Audio,
Kino.Video
]
]
]
Expand Down
31 changes: 31 additions & 0 deletions test/kino/audio_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Kino.AudioTest do
use Kino.LivebookCase, async: true

describe "new/2" do
test "raises an error for a non-image MIME type" do
assert_raise ArgumentError,
~s{expected audio type to be either :wav, :mp3, :mpeg, :ogg, or an audio MIME type string, got: "application/json"},
fn ->
Kino.Audio.new(<<>>, "application/json")
end
end

test "raises an error for an invalid type shorthand" do
assert_raise ArgumentError,
"expected audio type to be either :wav, :mp3, :mpeg, :ogg, or an audio MIME type string, got: :invalid",
fn ->
Kino.Audio.new(<<>>, :invalid)
end
end

test "mime type shorthand and default opts" do
kino = Kino.Audio.new(<<>>, :wav)
assert {:binary, %{type: "audio/wav", opts: "controls"}, <<>>} == connect(kino)
end

test "custom mime type and custom opts" do
kino = Kino.Audio.new(<<>>, "audio/mp2", loop: true)
assert {:binary, %{type: "audio/mp2", opts: "controls loop"}, <<>>} == connect(kino)
end
end
end
31 changes: 31 additions & 0 deletions test/kino/video_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Kino.VideoTest do
use Kino.LivebookCase, async: true

describe "new/2" do
test "raises an error for a non-image MIME type" do
assert_raise ArgumentError,
~s{expected video type to be either :mp4, :ogg, :avi, :wmv, :mov, or an video MIME type string, got: "application/json"},
fn ->
Kino.Video.new(<<>>, "application/json")
end
end

test "raises an error for an invalid type shorthand" do
assert_raise ArgumentError,
"expected video type to be either :mp4, :ogg, :avi, :wmv, :mov, or an video MIME type string, got: :invalid",
fn ->
Kino.Video.new(<<>>, :invalid)
end
end

test "mime type shorthand and default opts" do
kino = Kino.Video.new(<<>>, :mp4)
assert {:binary, %{type: "video/mp4", opts: "controls"}, <<>>} == connect(kino)
end

test "custom mime type and custom opts" do
kino = Kino.Video.new(<<>>, "video/h123", loop: true)
assert {:binary, %{type: "video/h123", opts: "controls loop"}, <<>>} == connect(kino)
end
end
end

0 comments on commit 4759cc5

Please sign in to comment.