Finally, a video player with good information that takes significantly less screen real estate:
iPhone X screenshot:
(new mini player on top, older players beneath) - Idle state comparison:
(new mini player on top, older players beneath) - Playing Netflix video comparison:
(new mini player on top, older players beneath) - Plex video using media_player.apple_tv player:
(new mini player on top, older players beneath) - Plex video using media_player.plex player:
Popup controls:
state-card-mini-video-player.html:
<!--
https://github.com/c727/home-assistant-mini-media-player
version: 20180129.2
-->
<dom-module id='state-card-mini-video-player'>
<template>
<style include='iron-flex iron-flex-alignment'></style>
<style>
state-badge {
float: left;
/* Force round badge to square movie poster size */
width: 80px !important;
height: 120px !important;
border-radius: 0% !important;
background-size: 80px !important;
background-repeat: no-repeat !important;
background-color: black !important;
background-position: center !important;
margin-right: 15px !important;
}
.info {
margin-left: 56px;
}
.state,
.playnername {
/* line-height: 40px; */
/* font-size: large; */
font-weight: bolder;
white-space: nowrap;
}
.state {
font-weight: lighter;
}
.playnername[has-info] {
}
.mediainfo {
color: var(--secondary-text-color);
font-weight: medium;
white-space: nowrap;
}
paper-dialog {
padding: 20px 8px;
border-radius: 2px;
font-weight: 500;
min-width: 500px;
}
@media screen and (max-width: 600px) {
paper-dialog {
position: absolute !important;
bottom: 0; left: 0; right: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin: 0;
}
}
paper-button-group {
display: grid;
grid-template-columns: repeat(3, 120px);
grid-gap: 4px;
margin-top: 8px;
padding: 0;
}
paper-button {
background-color: var(--primary-color);
color: #FFF;
font-weight: 500;
width: 100%;
margin: 0;
}
</style>
<template is='dom-if' if='[[!playerObj]]'>
<div class='horizontal justified layout'>
<state-info state-obj='[[stateObj]]' in-dialog='[[dialog]]'></state-info>
<div class='state'>Nothing Playing</div>
</div>
</template>
<template is='dom-if' if='[[playerObj]]'>
<div class='horizontal justified layout' on-tap='openInfoCard'>
<div class='state-info'>
<state-badge state-obj='[[playerObj]]'></state-badge>
<div class='info'>
<div class='mediainfo'>[[computeStateName(playerObj)]]</div>
<br>
<div class='playnername' hidden='[[!playerObj.hasMediaInfo]]'>[[playerObj.mediaInfo]]</div>
<div class='mediainfo' hidden='[[!playerObj.hasShowInfo]]'>[[playerObj.showInfo]]</div>
<div class='mediainfo' hidden='[[!playerObj.hasRating]]'>[[playerObj.rating]]</div>
<div class='mediainfo' hidden='[[!playerObj.hasDuration]]'>[[playerObj.duration]]</div>
<div class='mediainfo' hidden='[[!playerObj.hasSource]]'>[[playerObj.source]]</div>
</div>
</div>
<div class='horizontal layout'>
<template is='dom-if' if='[[playerObj.isNormalView]]'>
<paper-icon-button icon='mdi:power' on-tap='handlePowerTap'></paper-icon-button>
</template>
<template is='dom-if' if='[[playerObj.isOnAndCompactView]]'>
<paper-icon-button icon='mdi:skip-backward' on-tap='handlePreviousTap'></paper-icon-button>
<paper-icon-button icon='mdi:play' on-tap='handlePlayPauseTap' hidden='[[playerObj.isPlaying]]'></paper-icon-button>
<paper-icon-button icon='mdi:pause' on-tap='handlePlayPauseTap' hidden='[[!playerObj.isPlaying]]'></paper-icon-button>
<paper-icon-button icon='mdi:skip-forward' on-tap='handleNextTap'></paper-icon-button>
</template>
</div>
</template>
</div>
<paper-dialog id='infoCard'>
<template is='dom-if' if='[[playerObj]]'>
<div class='mediainfo'>[[computeStateName(playerObj)]]</div>
</template>
<more-info-media_player
hass='[[hass]]'
state-obj='[[playerObj]]'
></more-info-media_player>
</paper-dialog>
</template>
</template>
</dom-module>
<script>
class StateCardMiniVideoPlayer extends Polymer.Element {
static get is() { return 'state-card-mini-video-player'; }
static get properties() {
return {
hass: Object,
stateObj: Object,
config: {
type: Object,
computed: 'computeConfig(stateObj)',
},
playerObj: {
type: Object,
computed: 'computePlayerObj(hass, config)',
},
dialog: {
type: Boolean,
value: false,
},
};
}
computeConfig(stateObj) {
return stateObj.attributes.config;
}
computePlayerObj(hass, config) {
if (config && config.player && hass.states[this.config.player]) {
var playerObj = hass.states[this.config.player];
playerObj.isOff = playerObj.state === 'off';
playerObj.isPlaying = playerObj.state === 'playing';
playerObj.isIdle = playerObj.state === 'idle';
if (playerObj.isIdle) {
return null;
} else {
playerObj.hasMediaInfo = (playerObj.attributes.media_title) ? true : false;
if (playerObj.hasMediaInfo) {
if (playerObj.attributes.media_title) {
playerObj.mediaInfo = playerObj.attributes.media_title;
}
}
playerObj.hasDuration = (playerObj.attributes.media_duration);
if (playerObj.hasDuration) {
var duration = parseInt(playerObj.attributes.media_duration);
/* if plex */
if (playerObj.attributes.media_library_name) {
/* adjust plex time */
duration = (duration / 1000);
}
var hours = parseInt(duration / 3600);
duration = duration - hours * 3600;
var minutes = parseInt(duration / 60);
var duration_string = '';
if (hours == 1) {
duration_string = hours.toString() + ' hour ';
} else if (hours > 1) {
duration_string = hours.toString() + ' hours ';
}
if (minutes > 0){
duration_string = duration_string + minutes.toString() + ' min';
}
playerObj.duration = duration_string;
}
playerObj.hasRating = (playerObj.attributes.media_content_rating);
if (playerObj.hasRating) {
playerObj.rating = 'Rated: ' + playerObj.attributes.media_content_rating;
}
playerObj.showInfo = '';
/* if plex */
if (playerObj.attributes.media_library_name) {
if (playerObj.attributes.media_content_type == 'tvshow'){
if (playerObj.attributes.media_season && playerObj.attributes.media_episode) {
playerObj.showInfo = playerObj.attributes.media_series_title + ' - S' + playerObj.attributes.media_season + 'E' + playerObj.attributes.media_episode;
}
}
}
/* TODO: Reformat TV show titles from Apple TV media player titles
Format 1 (Plex): S4 • E5: The Signature
var re1 = ^S\d+.+E\d+:.+;
Format 2 (Netflix): S3: E3 "The Coup"
var re2 = ^S\d*:.E\d*.".*"
*/
playerObj.hasShowInfo = (playerObj.showInfo) ? true : false;
}
playerObj.hasSource = (playerObj.attributes.source && playerObj.attributes.source != playerObj.mediaInfo);
playerObj.source = playerObj.attributes.source;
playerObj.isMuted = playerObj.attributes.is_volume_muted;
playerObj.volumeSliderValue = playerObj.attributes.volume_level * 100;
playerObj.isNormalView = !this.config.compact_view;
playerObj.isOnAndNormalView = !playerObj.isOff && !this.config.compact_view;
playerObj.isOnAndCompactView = !playerObj.isOff && this.config.compact_view;
return playerObj;
}
return null;
}
computeStateName(stateObj) {
return window.hassUtil.computeStateName(stateObj);
}
handlePreviousTap(ev) {
ev.stopPropagation();
this.callService('media_previous_track');
}
handlePlayPauseTap(ev) {
ev.stopPropagation();
this.callService('media_play_pause');
}
handleNextTap(ev) {
ev.stopPropagation();
this.callService('media_next_track');
}
handlePowerTap(ev) {
ev.stopPropagation();
this.callService('toggle');
}
callService(service) {
this.hass.callService('media_player', service, { 'entity_id': this.playerObj.entity_id });
}
fireScript(ev) {
this.hass.callService('script', ev.model.item.script.split('.')[1], { 'entity_id': this.playerObj.entity_id });
}
handleVolumeTap() {
this.hass.callService('media_player', 'volume_mute', { 'entity_id': this.playerObj.entity_id, 'is_volume_muted': !this.playerObj.isMuted });
}
volumeSliderChanged(ev) {
var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
this.hass.callService('media_player', 'volume_set', { 'entity_id': this.playerObj.entity_id, 'volume_level': vol });
}
openInfoCard() {
this.root.getElementById('infoCard').open();
}
}
customElements.define(StateCardMiniVideoPlayer.is, StateCardMiniVideoPlayer);
</script>
configuration.yaml:
homeassistant:
customize_glob:
"*.*":
custom_ui_state_card: state-card-custom-ui
# give all mini video players the same look and feel
"input_text.mvp_*":
custom_ui_state_card: state-card-mini-video-player
icon: mdi:movie
friendly_name: Video
customize:
input_text.mvp_living_room_atv:
friendly_name: Apple TV Mini Player
config:
player: media_player.living_room_apple_tv
input_text.mvp_living_room_plex:
friendly_name: Plex Mini Player
config:
player: media_player.plex_living_room_apple_tv
media_player.plex_living_room_apple_tv:
friendly_name: Plex Player
media_player.living_room_apple_tv:
friendly_name: Apple TV Player
discovery:
config:
media_player:
- platform: plex
entity_namespace: 'plex'
include_non_clients: true
scan_interval: 5
show_all_controls: false
use_episode_art: false
- platform: apple_tv
name: Apple TV Player
host: !secret appletv_living_room_host
login_id: !secret appletv_login_id
start_off: true
frontend:
javascript_version: latest
extra_html_url:
- /local/custom_ui/state-card-custom-ui.html
- /local/custom_ui/state-card-mini-video-player.html
extra_html_url_es5:
- /local/custom_ui/state-card-custom-ui-es5.html
- /local/custom_ui/state-card-mini-video-player_es5.html
input_text:
mvp_living_room_plex:
name: ' '
mvp_living_room_atv:
name: ' '
group:
default_view:
name: Home
view: yes
icon: mdi:home
entities:
- group.living_room_apple_tv
- group.living_room_plex
# - media_player.living_room_apple_tv
# - media_player.plex_living_room_apple_tv
living_room_apple_tv:
name: ' ' # hide group heading
entities:
- input_text.mvp_living_room_atv
living_room_plex:
name: ' ' # hide group heading
entities:
- input_text.mvp_living_room_plex
automation:
# The HA plex player displays detailed, properly formatted media information
# Ex.
# Lawnmower Dog
# Rick and Morty - S01E02
# Rated: TV-MA
# The HA apple tv player shows minimal, improperly formatted media information
# Ex.
# S1 • E2: Lawnmower Dog
#
# Use this automation to show the plex player when plex videos are being played
# Otherwise show the apple tv player (even if nothing is being played)
- alias: Show Hide Living Room Video Players
trigger:
- platform: homeassistant
event: start
- platform: state
entity_id:
- media_player.plex_living_room_apple_tv
- media_player.living_room_apple_tv
action:
# only show plex player when its playing
- service: group.set_visibility
entity_id: group.living_room_plex
data_template:
visible: >
{% if is_state("media_player.plex_living_room_apple_tv","playing") %}
True
{% else %}
False
{% endif %}
# otherwise always show apple tv player
- service: group.set_visibility
entity_id: group.living_room_apple_tv
data_template:
visible: >
{% if is_state("media_player.plex_living_room_apple_tv","playing") %}
False
{% else %}
True
{% endif %}
To create this, I merely hacked away at the existing mini media player. Ideally @eddi89 incorporates my hacks into his player.
KNOWN ISSUE: I can only get this to work on Apple IOS by setting frontend: to javascript_version: latest. Any help you can give on fixing this issue or improving this component is appreciated.