UNPKG

7.96 kBJavaScriptView Raw
1import '@polymer/iron-flex-layout/iron-flex-layout.js';
2import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
3
4class KwcMusicPlayer extends PolymerElement {
5 static get template() {
6 return html`
7 <style>
8 :host {
9 background: #58d6fa;
10 @apply --layout-vertical;
11 @apply --layout-center-justified;
12 color: white;
13 padding: 16px;
14 }
15 .player {
16 @apply --layout-horizontal;
17 @apply --layout-center;
18 }
19 .player .spectrum {
20 flex: 1 0 auto;
21 margin-left: 16px;
22 }
23 .playback-time {
24 margin-left: 16px;
25 min-width: 30px;
26 }
27 .playback-button {
28 border-radius: 50%;
29 border: 4px solid white;
30 width: 32px;
31 height: 32px;
32 background: transparent;
33 cursor: pointer;
34 padding: 4px;
35 }
36 .playback-button:focus {
37 outline: none;
38 @apply --shadow-elevation-4dp;
39 }
40 .playback-button svg {
41 width: 100%;
42 height: 100%;
43 }
44 .playback-button svg path {
45 fill : white;
46 transition: all linear 100ms;
47 }
48 </style>
49 <div class="player">
50 <button type="button" class="playback-button" on-click="_playbackButtonTapped" hidden$="[[cannotRenderSample]]">
51 <svg viewBox="0 0 17 19">
52 <path id="button-icon-path" d="M 4,18 10.5,14 10.5,6 4,2 z M 10.5,14 17,10 17,10 10.5,6 z">
53 </path>
54 </svg>
55 </button>
56 <div id="spectrum-container" class="spectrum">
57 <canvas id="canvas"></canvas>
58 </div>
59 <span class="playback-time">[[_formatTime(playbackTime)]]</span>
60 </div>
61 `;
62 }
63 static get properties() {
64 return {
65 share: {
66 type: Object,
67 observer: '_shareChanged',
68 },
69 cannotRenderSample: {
70 type: Boolean,
71 value: false,
72 },
73 playbackTime: {
74 type: Number,
75 },
76 };
77 }
78 static get observers() {
79 return [
80 '_playbackStatusChanged(_playbackStatus)',
81 ];
82 }
83 connectedCallback() {
84 super.connectedCallback();
85 this.stoppedPath = 'M 4,18 10.5,14 10.5,6 4,2 z M 10.5,14 17,10 17,10 10.5,6 z';
86 this.playingPath = 'M 2,18 6,18 6,2 2,2 z M 11,18 15,18 15,2 11,2 z';
87 try {
88 window.AudioContext = window.AudioContext || window.webkitAudioContext;
89 window.OfflineAudioContext = window.OfflineAudioContext
90 || window.webkitOfflineAudioContext;
91 this.context = new AudioContext();
92 } catch (e) {
93 this.cannotRenderSample = true;
94 }
95 this._onPlaybackEnded = this._onPlaybackEnded.bind(this);
96 }
97 _playbackStatusChanged(status) {
98 if (status === 'playing') {
99 this.$['button-icon-path'].setAttribute('d', this.playingPath);
100 } else {
101 this.$['button-icon-path'].setAttribute('d', this.stoppedPath);
102 }
103 }
104 _formatTime(duration) {
105 const min = Math.floor(duration / 60);
106
107
108 const sec = Math.floor(duration % 60);
109 return `${min}:${sec < 10 ? `0${sec}` : sec}`;
110 }
111 _fitCanvas() {
112 const rect = this.$['spectrum-container'].getBoundingClientRect();
113 this.$.canvas.width = rect.width;
114 this.$.canvas.height = rect.height;
115 this._canvasFitted = true;
116 }
117 _play() {
118 if (this.cannotRenderSample || !this.buffer) {
119 return;
120 }
121 this.source = this.context.createBufferSource();
122 this.source.buffer = this.buffer;
123 this.source.connect(this.context.destination);
124
125 this.source.addEventListener('ended', this._onPlaybackEnded);
126
127 if (this._playbackPosition) {
128 this.source.start(0, this._playbackPosition);
129 this._startedAt = this.context.currentTime - this._playbackPosition;
130 } else {
131 this.source.start(0);
132 this._startedAt = this.context.currentTime;
133 }
134 this._playbackStatus = 'playing';
135 this._render();
136 }
137 _pause() {
138 this._playbackPosition = this.context.currentTime - this._startedAt;
139 this.source.removeEventListener('ended', this._onPlaybackEnded);
140 this.source.stop();
141 this._playbackStatus = 'stopped';
142 this._stopRendering();
143 }
144 _onPlaybackEnded() {
145 this._playbackPosition = 0;
146 this._playbackStatus = 'stopped';
147 this._stopRendering();
148 }
149 _playbackButtonTapped() {
150 if (this._playbackStatus === 'playing') {
151 this._pause();
152 } else {
153 this._play();
154 }
155 }
156 _updatePlaybackTime() {
157 const position = this._startedAt ? this.context.currentTime - this._startedAt : 0;
158 this.playbackTime = this.buffer.duration - position;
159 }
160 _shareChanged(share) {
161 if (!share) {
162 return;
163 }
164
165 const sampleUrl = share.sample_url;
166
167 if (!sampleUrl) {
168 this.cannotRenderSample = true;
169 }
170 // That might have been set by the audio context checking
171 if (this.cannotRenderSample) {
172 return;
173 }
174 fetch(sampleUrl)
175 .then(r => r.arrayBuffer())
176 .then((ab) => {
177 this.context.decodeAudioData(ab, (buffer) => {
178 this.buffer = buffer;
179 this._playbackPosition = 0;
180 this._updatePlaybackTime();
181 this._render(true);
182 });
183 });
184 }
185 _render(once) {
186 const ctx = this.$.canvas.getContext('2d');
187 if (!this._spectrumCache) {
188 this._drawSpectrum();
189 }
190 if (!this._canvasFitted) {
191 this._fitCanvas();
192 }
193 if (!this._playbackTimeInterval) {
194 this._playbackTimeInterval = setInterval(this._updatePlaybackTime.bind(this), 1000);
195 }
196 ctx.save();
197 ctx.fillStyle = 'white';
198 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
199 const playbackTime = this.context.currentTime - this._startedAt;
200 const playbackPosition = playbackTime / this.buffer.duration;
201 ctx.fillRect(0, 0, playbackPosition * ctx.canvas.width, ctx.canvas.height);
202 ctx.globalCompositeOperation = 'destination-atop';
203 ctx.drawImage(this._spectrumCache, 0, 0, this.$.canvas.width, this.$.canvas.height);
204 ctx.restore();
205 if (!once) {
206 this._renderId = requestAnimationFrame(this._render.bind(this, false));
207 }
208 }
209 _stopRendering() {
210 cancelAnimationFrame(this._renderId);
211 clearInterval(this._playbackTimeInterval);
212 this._playbackTimeInterval = null;
213 }
214 _drawSpectrum() {
215 const data = this.buffer.getChannelData(0);
216
217
218 const height = 140;
219
220
221 const width = 600;
222
223
224 let value;
225
226 this._spectrumCache = document.createElement('canvas');
227
228 this._spectrumCache.width = width;
229 this._spectrumCache.height = height;
230
231 const ctx = this._spectrumCache.getContext('2d');
232
233 ctx.fillStyle = '#58afd4';
234
235 ctx.fillRect(0, height / 2, width, 1);
236
237 for (let i = 0; i < width; i += 1) {
238 value = (height / 2) * data[Math.floor((data.length / width) * i)];
239 ctx.fillRect(i, (height / 2) - (value / 2), 1, value);
240 }
241 }
242}
243
244customElements.define('kwc-music-player', KwcMusicPlayer);