Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # ==============================================================================
- # DEL 1: CONFIGURATION.YAML (Eller packages/helpers)
- # ==============================================================================
- # 1. Input Select - Holder styr på hvilken kanal der er valgt
- # (Kan også oprettes via GUI under Indstillinger -> Enheder -> Hjælpere)
- input_select:
- tv_stream_source:
- name: TV Stream Source
- icon: mdi:television
- options:
- - DR 1
- - DR 2
- - TVA
- - Ramasjang
- - Storebælt
- # 2. Template Sensor - "Hjernen" der holder URL'er og logik
- template:
- - trigger:
- - trigger: state
- entity_id: input_select.tv_stream_source
- - trigger: homeassistant
- event: start
- - trigger: event
- event_type: event_template_reloaded
- sensor:
- - name: "TV Stream Context"
- unique_id: tv_stream_context
- state: "{{ states('input_select.tv_stream_source') }}"
- attributes:
- # Databasen over alle kanaler (DRY: Defineres kun ét sted)
- sources: >
- {% set base_url = "https://prod95-static.dr-massive.com/api/shain/v1/dataservice/ResizeImage/$value?Format=%27png%27&Quality=90&Width=320&Height=320&ImageId=" %}
- {% set map = {
- "DR 1": {
- "image": base_url ~ 2344192,
- "url": "https://drlivedr1hls.akamaized.net/hls/live/2113625/drlivedr1/master.m3u8"
- },
- "DR 2": {
- "image": base_url ~ 46461911,
- "url": "https://drlivedr2hls.akamaized.net/hls/live/2113623/drlivedr2/master.m3u8"
- },
- "TVA": {
- "image": base_url ~ 16100144,
- "url": "https://drlivedrtvahls.akamaized.net/hls/live/2113613/drlivedrtva/master.m3u8"
- },
- "Ramasjang": {
- "image": base_url ~ 2399380,
- "url": "https://drlivedrrhls.akamaized.net/hls/live/2113621/drlivedrr/master.m3u8"
- },
- "Storebælt": {
- "image": "/local/images/icons/storebælt_tv.jpg",
- "url": "https://stream.sob.m-dn.net/live/sb1/index.m3u8"
- }
- } %}
- {{ map }}
- # Den URL playeren skal afspille LIGE NU
- stream_url: >-
- {{ this.attributes.sources.get(states('input_select.tv_stream_source'), {'url': ''}).url }}
- # ==============================================================================
- # DEL 2: HTML PLAYER
- # Opret en fil i din config mappe: /www/tv_player.html
- # ==============================================================================
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
- <title>HA TV Player</title>
- <style>
- body { margin: 0; background: #000; overflow: hidden; font-family: sans-serif; }
- video { width: 100vw; height: 100vh; object-fit: contain; }
- /* --- UI Overlay Container --- */
- #ui-layer {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 80px;
- background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
- display: flex;
- align-items: center;
- padding: 0 20px;
- box-sizing: border-box;
- opacity: 0;
- transition: opacity 0.5s ease;
- }
- #ui-layer.visible { opacity: 1; }
- /* --- Volume Slider --- */
- input[type=range] {
- -webkit-appearance: none;
- width: 200px;
- height: 6px;
- background: rgba(255,255,255,0.3);
- border-radius: 3px;
- outline: none;
- margin-left: 15px;
- }
- input[type=range]::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- background: #a83232;
- cursor: pointer;
- box-shadow: 0 0 10px rgba(0,0,0,0.5);
- }
- /* --- Knapper (Mute + Fullscreen) --- */
- .icon-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
- fill: white;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- #fs-btn { margin-left: auto; }
- </style>
- <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
- </head>
- <body>
- <video id="video" autoplay playsinline ondblclick="toggleFullscreen()"></video>
- <div id="ui-layer">
- <button id="mute-btn" class="icon-btn" onclick="toggleMute()">
- <svg viewBox="0 0 24 24" width="28" height="28"><path id="mute-icon-path" d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.48,8.71 14,7.97V16.02C15.48,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" /></svg>
- </button>
- <input type="range" id="vol-control" min="0" max="1" step="0.05" oninput="setVolume(this.value)" onchange="setVolume(this.value)">
- <button id="fs-btn" class="icon-btn" onclick="toggleFullscreen()">
- <svg viewBox="0 0 24 24" width="28" height="28"><path id="fs-icon-path" d="M7,14H5V19H10V17H7V14M5,10H7V7H10V5H5V10M19,14H17V17H14V19H19V14M14,5V7H17V10H19V5H14Z" /></svg>
- </button>
- </div>
- <script>
- const urlParams = new URLSearchParams(window.location.search);
- const HA_URL = window.location.origin;
- const PLAYER_ENTITY = urlParams.get('entity') || "sensor.tv_stream_context";
- function getAuthToken() {
- if (urlParams.get('token')) return urlParams.get('token');
- try {
- const rawTokens = window.parent.localStorage.getItem('hassTokens');
- if (rawTokens) {
- const tokens = JSON.parse(rawTokens);
- return tokens.access_token;
- }
- } catch (e) {
- console.warn("Kunne ikke hente token fra parent vindue. Er du på samme domæne?", e);
- }
- return null;
- }
- const HA_TOKEN = getAuthToken();
- if (!HA_TOKEN) {
- const msg = "Fejl: Kunne ikke finde Auth Token.<br>Er du logget ind i HA?";
- document.body.innerHTML = `<h1 style='color:white;padding:20px;font-family:sans-serif'>${msg}</h1>`;
- throw new Error("No Token Found");
- }
- const video = document.getElementById('video');
- const uiLayer = document.getElementById('ui-layer');
- const volSlider = document.getElementById('vol-control');
- const muteIconPath = document.getElementById('mute-icon-path');
- let hls = null;
- let currentUrl = "";
- let uiTimeout;
- // Gendan volumen fra sidst
- const savedVol = localStorage.getItem('tv_volume');
- if (savedVol) {
- video.volume = parseFloat(savedVol);
- volSlider.value = savedVol;
- }
- updateMuteIcon();
- async function syncLoop() {
- try {
- const response = await fetch(`${HA_URL}/api/states/${PLAYER_ENTITY}`, {
- headers: {
- "Authorization": `Bearer ${HA_TOKEN}`,
- "Content-Type": "application/json"
- }
- });
- if (!response.ok) return;
- const data = await response.json();
- const attrs = data.attributes;
- const stream_url = attrs.stream_url;
- if (stream_url && stream_url !== currentUrl) {
- playStream(stream_url);
- }
- } catch (e) { console.error("Sync Error:", e); }
- }
- function playStream(url) {
- stopStream();
- currentUrl = url;
- if (Hls.isSupported()) {
- hls = new Hls({ enableWorker: true, lowLatencyMode: true, backBufferLength: 90 });
- hls.loadSource(url);
- hls.attachMedia(video);
- } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
- video.src = url;
- }
- }
- function stopStream() {
- if (hls) { hls.destroy(); hls = null; }
- video.pause();
- video.removeAttribute('src');
- video.load();
- currentUrl = "";
- }
- function setVolume(val) {
- video.volume = val; video.muted = false;
- updateMuteIcon(); localStorage.setItem('tv_volume', val);
- }
- function toggleMute() { video.muted = !video.muted; updateMuteIcon(); }
- function updateMuteIcon() {
- if (video.muted || video.volume === 0) {
- muteIconPath.setAttribute('d', 'M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,10.23 15.48,8.71 14,7.97V10.18L16.45,12.63C16.5,12.43 16.5,12.21 16.5,12Z');
- } else {
- muteIconPath.setAttribute('d', 'M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.48,8.71 14,7.97V16.02C15.48,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z');
- }
- }
- function toggleFullscreen() {
- if (!document.fullscreenElement) {
- document.documentElement.requestFullscreen().catch(e => console.log(e));
- } else { if (document.exitFullscreen) document.exitFullscreen(); }
- }
- document.addEventListener("fullscreenchange", () => {
- const path = document.getElementById('fs-icon-path');
- path.setAttribute('d', document.fullscreenElement ? 'M5,16H8V19H10V14H5V16M8,8H5V10H10V5H8V8M14,19H16V16H19V14H14V19M16,8V5H14V10H19V8H16Z' : 'M7,14H5V19H10V17H7V14M5,10H7V7H10V5H5V10M19,14H17V17H14V19H19V14M14,5V7H17V10H19V5H14Z');
- });
- function showUI() {
- uiLayer.classList.add('visible');
- clearTimeout(uiTimeout);
- uiTimeout = setTimeout(() => { uiLayer.classList.remove('visible'); }, 3000);
- }
- ['mousemove','click','touchstart'].forEach(e => document.addEventListener(e, showUI));
- showUI();
- let observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- video.play().catch(e => console.log("Auto-resume blocked", e));
- } else { video.pause(); }
- });
- }, { threshold: 0.1 });
- observer.observe(document.body);
- setInterval(syncLoop, 1500);
- syncLoop();
- </script>
- </body>
- </html>
- # ==============================================================================
- # DEL 3: LOVELACE DASHBOARD YAML
- # Kræver: custom:layout-card, custom:auto-entities, card-mod
- # ==============================================================================
- type: panel
- path: tv
- title: TV
- icon: mdi:television
- cards:
- - type: custom:layout-card
- layout_type: custom:grid-layout
- layout:
- grid-template-columns: 200px 1fr
- grid-template-rows: 1fr auto
- grid-gap: 0px
- margin: 0
- padding: 0
- grid-template-areas: |
- "sidebar video"
- cards:
- # --- SIDEBAR MED KANALER ---
- - type: custom:auto-entities
- view_layout:
- grid_area: sidebar
- card:
- type: grid
- columns: 1
- square: true
- card_param: cards
- filter:
- template: >-
- {% set sources = state_attr('sensor.tv_stream_context', 'sources') %}
- {% set options_entity = "input_select.tv_stream_source" %}
- {% set ns = namespace(cards = []) %}
- {% for option in state_attr(options_entity, 'options') %}
- {% if sources is not none and sources[option] is defined %}
- {# --- STYLING: Glow effekt på valgt kanal --- #}
- {% if is_state(options_entity, option) %}
- {% set style = "ha-card {
- border: 3px solid white !important;
- box-shadow: 0 0 15px rgba(255, 255, 255, 0.4);
- opacity: 1;
- transform: scale(1.02);
- transition: all 0.3s ease-in-out;
- z-index: 2;
- }" %}
- {% else %}
- {% set style = "ha-card {
- border: 3px solid transparent;
- opacity: 0.5;
- transform: scale(0.95);
- filter: grayscale(20%);
- transition: all 0.3s ease-in-out;
- }" %}
- {% endif %}
- {% set card = {
- "type": "picture",
- "image": sources[option].image,
- "card_mod": {
- "style": style
- },
- "tap_action": {
- "action": "perform-action",
- "perform_action": "input_select.select_option",
- "target": {
- "entity_id": options_entity
- },
- "data": {
- "option": option
- }
- }
- } %}
- {% set ns.cards = ns.cards + [card] %}
- {% endif %}
- {% endfor %}
- {{ ns.cards }}
- # --- VIDEO PLAYER ---
- - type: iframe
- url: /local/tv_player.html?v=11
- aspect_ratio: 16:9
- hide_background: true
- view_layout:
- grid_area: video
- card_mod:
- style: |
- ha-card {
- height: 100vh !important;
- padding: 0 !important;
- margin: 0 !important;
- background: black;
- }
Advertisement
Add Comment
Please, Sign In to add comment