-
Notifications
You must be signed in to change notification settings - Fork 79
Description
Problem
This issue provides only another SSCCE to the #103 issue. In my opinion the explanation in the other issue applies to this SSCCE too. Like #103 this SSCCE could be added to the list in #105 as well.
Version
OS: Windows 10 1909 (Build 18363.900)
Elm: 0.19.1
Thrown exceptions
The thrown exceptions differ between Chrome and Firefox. Added so it is easier to find for other people. But the exception text may vary between the expected types.
Chrome 83.0.4103.116 (64-Bit)
Uncaught TypeError: Cannot read property 'a' of undefined
Firefox 78.0.2 (64-Bit)
TypeError: _v0 is undefined
SSCCEE
https://ellie-app.com/9pbMRZHxRkVa1
module Main exposing (main)
import Browser
import Html exposing (Html, br, button, div, source, text, video)
import Html.Attributes exposing (autoplay, id, loop, property, src, style, type_)
import Html.Events exposing (on, onClick)
import Json.Decode as JD
import Json.Encode as JE
type alias Model =
{ currentTime : Float
, duration : Float
, videoVisible : Bool
, selection : Maybe ( GroupId, ItemId )
}
initialModel : Model
initialModel =
{ currentTime = 0
, duration = 0
, videoVisible = False
, selection = Nothing
}
type MediaMsg
= MediaMsgTimeupdate { currentTime : Float, duration : Float }
type VideoPageMsg
= VideoPageMsgLeave
| VideoPageMsgMediaMsg MediaMsg
type GroupId
= GroupId String
groupIdEncoder : GroupId -> JE.Value
groupIdEncoder (GroupId value) =
JE.string value
type ItemId
= ItemId String
itemIdEncoder : ItemId -> JE.Value
itemIdEncoder (ItemId value) =
JE.string value
type ListPageMsg
= ListPageMsgSelect GroupId ItemId
type Msg
= VideoPage VideoPageMsg
| ListPage ListPageMsg
update : Msg -> Model -> Model
update msg model =
case msg of
VideoPage videoPageMsg ->
case videoPageMsg of
VideoPageMsgMediaMsg (MediaMsgTimeupdate { currentTime, duration }) ->
{ model | currentTime = currentTime, duration = duration }
VideoPageMsgLeave ->
{ model | videoVisible = False }
ListPage listPageMsg ->
case listPageMsg of
ListPageMsgSelect groupId itemId ->
let
_ =
Debug.log "Attempt to store selection with" listPageMsg
_ =
-- In my case i am storing the selection in the local storage,
-- so i can just use F5 to reload the page, without reselecting.
Debug.log "Selection json which can be sent to local storage" <|
JE.encode 0 <|
JE.object
[ ( "groupId", groupIdEncoder groupId )
, ( "itemId", itemIdEncoder itemId )
]
in
{ model | selection = Just ( groupId, itemId ), videoVisible = True }
timeupdateDecoder : JD.Decoder { currentTime : Float, duration : Float }
timeupdateDecoder =
JD.map2 (\currentTime duration -> { currentTime = currentTime, duration = duration })
(JD.at [ "target", "currentTime" ] JD.float)
(JD.at [ "target", "duration" ] JD.float)
viewList : Html ListPageMsg
viewList =
div []
[ button [ onClick <| ListPageMsgSelect (GroupId "fpie73") (ItemId "72ba27hs") ]
[ text "Open video" ]
]
viewVideo : Html VideoPageMsg
viewVideo =
div []
[ button [ onClick <| VideoPageMsgLeave ] [ text "Leave video" ]
, br [] []
, br [] []
, video
[ autoplay True
, property "muted" (JE.string "muted")
, loop True
, on "timeupdate" (timeupdateDecoder |> JD.map MediaMsgTimeupdate)
]
[ source
[ id "mp4"
, src "http://www.w3schools.com/html/movie.mp4"
, type_ "video/mp4"
]
[]
]
|> Html.map VideoPageMsgMediaMsg
]
view : Model -> Html Msg
view model =
div
[ style "transform" "scale(2, 2)"
, style "transform-origin" "left top"
]
[ if model.videoVisible then
viewVideo |> Html.map VideoPage
else
viewList |> Html.map ListPage
, div []
[ br [] []
, text <| String.fromFloat model.currentTime ++ "/" ++ String.fromFloat model.duration
]
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
Workaround
In #103 are also workarounds described. Additional Html.map identity
on the pages with same "map depth" works, but is not safe, because in future there could be another page with the same "map depth".
In my case i have wrapped the event in a custom element, which disconnects the event listener as soon as the element is removed from the DOM. The custom element controls the playstate too, so i do not need a port for pause/resume. Place this video-control
custom element inside the video
element. The custom element is defined as followed:
// This is based on the idea from baffalop on Slack
// https://elmlang.slack.com/archives/C0CJ3SBBM/p1593872335052000
// Place this custom element inside the video element.
customElements.define('video-control',
class VideoControl extends HTMLElement {
static get observedAttributes() { return ['playstate']; }
constructor() {
super();
this.boundVideoTimeupdated = this.videoTimeupdated.bind(this);
}
attributeChangedCallback(name, oldValue, newValue) {
const parentElement = this.parentElement;
if (name === "playstate") {
VideoControl.updatePlayState(parentElement, newValue);
}
}
/**
* @param {HTMLVideoElement} videoElement
* @param {boolean} value
* @return {string}
*/
static updatePlayState(videoElement, value) {
if (!videoElement)
return;
if (value === "play") {
videoElement.play();
} else if (value == "pause") {
videoElement.pause();
}
}
/**
* @param {HTMLVideoElement} videoElement
*/
videoTimeupdated() {
const videoElement = this.videoElement;
if (!videoElement || videoElement.tagName !== "VIDEO")
return;
this.dispatchEvent(new CustomEvent("timeupdate", {
detail: { currentTime: videoElement.currentTime, duration: videoElement.duration }
}));
}
/**
* @param {HTMLVideoElement} videoElement
*/
connectVideoElement(videoElement) {
this.disconnectVideoElement();
if (!videoElement || videoElement.tagName !== "VIDEO")
return;
this.videoElement = videoElement;
// Wir müssen timeupdate hier kapseln, da das timeupdate vom Video-Element
// einen Laufzeitfehler erzeugt, sollte die Seite verlassen werden, bevor
// das Video angehalten wurde.
VideoControl.updatePlayState(videoElement, this.getAttribute("playstate"));
videoElement.addEventListener('timeupdate', this.boundVideoTimeupdated, false);
}
/**
* @param {HTMLVideoElement} videoElement
*/
disconnectVideoElement() {
const videoElement = this.videoElement;
if (!videoElement || videoElement.tagName !== "VIDEO")
return;
videoElement.removeEventListener('timeupdate', this.boundVideoTimeupdated, false);
this.videoElement = null;
}
connectedCallback() {
this.connectVideoElement(this.parentElement);
}
disconnectedCallback() {
this.disconnectVideoElement();
}
});