Created
September 30, 2019 05:49
-
-
Save dasl-/a3cc39fb1bc1498ce72d3257ea9a7e81 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/app/package.json b/app/package.json | |
index 7bf73e9..2c547a9 100644 | |
--- a/app/package.json | |
+++ b/app/package.json | |
@@ -10,7 +10,8 @@ | |
"react-redux": "^7.1.0", | |
"react-scripts": "3.0.1", | |
"react-transition-group": "^4.2.2", | |
- "redux": "^4.0.4" | |
+ "redux": "^4.0.4", | |
+ "rc-slider": "8.6.1" | |
}, | |
"scripts": { | |
"start": "react-scripts start", | |
diff --git a/app/src/api.js b/app/src/api.js | |
index 1c1a80d..aa3e798 100644 | |
--- a/app/src/api.js | |
+++ b/app/src/api.js | |
@@ -38,6 +38,12 @@ class APIClient { | |
}); | |
} | |
+ setVolPct(vol_pct) { | |
+ return this.perform('post', '/vol_pct', { | |
+ vol_pct: vol_pct | |
+ }); | |
+ } | |
+ | |
clearQueue() { | |
return this.perform('post', '/clear'); | |
} | |
diff --git a/app/src/component/app/app.js b/app/src/component/app/app.js | |
index e94f11a..239976f 100644 | |
--- a/app/src/component/app/app.js | |
+++ b/app/src/component/app/app.js | |
@@ -13,6 +13,9 @@ import SearchResultVideo from 'dataobj/search_result_video'; | |
import './app.css'; | |
class App extends React.Component { | |
+ | |
+ static QUEUE_POLL_INTERVAL_MS = 1000; | |
+ | |
constructor(props) { | |
super(props); | |
@@ -35,7 +38,8 @@ class App extends React.Component { | |
playlist_videos: [], | |
color_mode: 'color', | |
last_queued_videos: [], | |
- last_queued_video_color_modes: [] | |
+ last_queued_video_color_modes: [], | |
+ vol_pct: undefined | |
}; | |
/* intro transition */ | |
@@ -53,6 +57,7 @@ class App extends React.Component { | |
this.nextVideo = this.nextVideo.bind(this); | |
this.clearQueue = this.clearQueue.bind(this); | |
this.removeVideo = this.removeVideo.bind(this); | |
+ this.setVolPct = this.setVolPct.bind(this); | |
} | |
componentDidMount() { | |
@@ -99,6 +104,8 @@ class App extends React.Component { | |
nextVideo={this.nextVideo} | |
clearQueue={this.clearQueue} | |
removeVideo={this.removeVideo} | |
+ setVolPct={this.setVolPct} | |
+ vol_pct={this.state.vol_pct} | |
/> | |
</div> | |
</CSSTransition> | |
@@ -173,7 +180,7 @@ class App extends React.Component { | |
.finally(() => { | |
// need to do this on a timeout because the server isnt so great about | |
// the currently playing video immediately after skipping | |
- setTimeout(() => {this.getPlaylistQueue()}, 1000) | |
+ setTimeout(() => {this.getPlaylistQueue()}, App.QUEUE_POLL_INTERVAL_MS) | |
}) | |
} | |
} | |
@@ -191,6 +198,9 @@ class App extends React.Component { | |
.removeVideo(video) | |
.finally(() => this.getPlaylistQueue()) | |
} | |
+ setVolPct(vol_pct) { | |
+ return this.apiClient.setVolPct(vol_pct) | |
+ } | |
cancelQueuePoll() { | |
clearTimeout(this.queue_timeout); | |
@@ -198,7 +208,7 @@ class App extends React.Component { | |
getPlaylistQueue() { | |
if (this.state.playlist_loading) { | |
this.cancelQueuePoll(); | |
- this.queue_timeout = setTimeout(this.getPlaylistQueue.bind(this), 1000); | |
+ this.queue_timeout = setTimeout(this.getPlaylistQueue.bind(this), App.QUEUE_POLL_INTERVAL_MS); | |
return; | |
} | |
@@ -209,6 +219,7 @@ class App extends React.Component { | |
.then((data) => { | |
if (data.success) { | |
var playlist_videos = PlaylistVideo.fromArray(data.queue); | |
+ var vol_pct = +(data.vol_pct.toFixed(0)); | |
var playlist_current_video = this.state.playlist_current_video; | |
var current_video = playlist_videos.find(function(video) { | |
return video.status === 'STATUS_PLAYING'; | |
@@ -234,14 +245,15 @@ class App extends React.Component { | |
this.setState({ | |
playlist_current_video: playlist_current_video, | |
- playlist_videos: playlist_videos | |
+ playlist_videos: playlist_videos, | |
+ vol_pct: vol_pct | |
}); | |
} | |
this.setState({ playlist_loading: false }); | |
}) | |
.finally((data) => { | |
- this.queue_timeout = setTimeout(this.getPlaylistQueue.bind(this), 1000); | |
+ this.queue_timeout = setTimeout(this.getPlaylistQueue.bind(this), App.QUEUE_POLL_INTERVAL_MS); | |
}); | |
} | |
diff --git a/app/src/component/app/content.js b/app/src/component/app/content.js | |
index e619168..e1dd777 100644 | |
--- a/app/src/component/app/content.js | |
+++ b/app/src/component/app/content.js | |
@@ -83,6 +83,8 @@ class Content extends React.Component { | |
nextVideo={this.props.nextVideo} | |
clearQueue={this.props.clearQueue} | |
removeVideo={this.props.removeVideo} | |
+ setVolPct={this.props.setVolPct} | |
+ vol_pct={this.props.vol_pct} | |
/> | |
</div> | |
</div> | |
@@ -108,6 +110,8 @@ class Content extends React.Component { | |
clearQueue={this.props.clearQueue} | |
removeVideo={this.props.removeVideo} | |
contractFooterPlaylist={this.contractFooterPlaylist} | |
+ setVolPct={this.props.setVolPct} | |
+ vol_pct={this.props.vol_pct} | |
/> | |
</div> | |
</div> | |
diff --git a/app/src/component/currently_playing/currently_playing.css b/app/src/component/currently_playing/currently_playing.css | |
index 35b943d..1f21b69 100644 | |
--- a/app/src/component/currently_playing/currently_playing.css | |
+++ b/app/src/component/currently_playing/currently_playing.css | |
@@ -1,6 +1,10 @@ | |
-.now_playing { | |
- min-height: 330px; | |
+input.volume { | |
+ width: 100%; | |
+} | |
+.vol-icon { | |
+ font-size:1.15rem; | |
+} | |
+.volume-container { | |
+ padding-left: 20px !important; | |
+ padding-right: 25px !important; | |
} | |
-.loading .video-thumbnail { | |
- opacity: .3; | |
-} | |
\ No newline at end of file | |
diff --git a/app/src/component/currently_playing/currently_playing_video.js b/app/src/component/currently_playing/currently_playing_video.js | |
index 0297b64..c67bcc2 100644 | |
--- a/app/src/component/currently_playing/currently_playing_video.js | |
+++ b/app/src/component/currently_playing/currently_playing_video.js | |
@@ -1,15 +1,25 @@ | |
+import './currently_playing.css'; | |
+import App from '../app/app'; | |
+ | |
+import 'rc-slider/assets/index.css'; | |
import React from 'react'; | |
+import Slider from 'rc-slider'; | |
class CurrentlyPlayingRight extends React.Component { | |
constructor(props) { | |
super(props); | |
this.handleSkip = this.handleSkip.bind(this); | |
+ this.state = { | |
+ vol_pct: this.props.vol_pct, | |
+ is_vol_locked: false, | |
+ is_vol_lock_releasable: true, | |
+ vol_lock_marked_releasable_time: 0 | |
+ }; | |
} | |
render() { | |
- var row_class = 'now-playing ' + (this.props.loading ? 'loading' : '') | |
- | |
+ var row_class = 'now-playing ' + (this.props.loading ? 'loading' : ''); | |
return ( | |
<div> | |
<div className={row_class}> | |
@@ -34,6 +44,42 @@ class CurrentlyPlayingRight extends React.Component { | |
</div> | |
</div> | |
+ <div className='row'> | |
+ <div className='col-1 p-0 text-right'><span className='glyphicon glyphicon-volume-down bg-light-text vol-icon' aria-hidden='true' /></div> | |
+ <div className='col-10 volume-container'> | |
+ <Slider | |
+ className='volume' | |
+ min={0} | |
+ max={100} | |
+ step={1} | |
+ onBeforeChange={this.grabVolMutex} | |
+ onChange={this.onVolChange} | |
+ onAfterChange={this.markVolMutexReleasable} | |
+ value={this.state.is_vol_locked ? this.state.vol_pct : this.props.vol_pct} | |
+ trackStyle={{ | |
+ border: '1px solid #686E7B', | |
+ backgroundColor: '#686E7B', | |
+ height: 10 | |
+ }} | |
+ railStyle={{ | |
+ border: '1px solid #686E7B', | |
+ backgroundColor: 'transparent', | |
+ height: 10 | |
+ }} | |
+ handleStyle={{ | |
+ border: '1px solid #5cedf9', | |
+ boxShadow: '0px 0px 3.5px #52c6f3, 0px 0px 0px #6acef5', | |
+ height: 30, | |
+ width: 30, | |
+ marginLeft: -16, | |
+ marginTop: -9, | |
+ backgroundColor: '#2e3135', | |
+ }} | |
+ /> | |
+ </div> | |
+ <div className='col-1 p-0'><span className='glyphicon glyphicon-volume-up bg-light-text vol-icon' aria-hidden='true' /></div> | |
+ </div> | |
+ | |
<div className='container pt-2 px-0 mt-2'> | |
<div className='row mr-0'> | |
<div className='col-8 px-2 pl-3 small-vertical-center'> | |
@@ -61,6 +107,45 @@ class CurrentlyPlayingRight extends React.Component { | |
this.props.setLoading(); | |
this.props.nextVideo(); | |
} | |
+ | |
+ onVolChange = (vol_pct) => { | |
+ this.props.setVolPct(vol_pct) | |
+ this.setState({vol_pct: vol_pct}); | |
+ }; | |
+ | |
+ grabVolMutex = () => { | |
+ this.setState({ | |
+ is_vol_locked: true, | |
+ is_vol_lock_releasable: false | |
+ }); | |
+ }; | |
+ markVolMutexReleasable = () => { | |
+ this.setState({ | |
+ is_vol_lock_releasable: true, | |
+ vol_lock_marked_releasable_time: (new Date()).getTime() | |
+ }); | |
+ }; | |
+ releaseVolMutex = () => { | |
+ this.setState({ | |
+ is_vol_locked: false, | |
+ is_vol_lock_releasable: true | |
+ }); | |
+ }; | |
+ | |
+ // TODO: this is deprecated | |
+ componentWillReceiveProps(nextProps) { | |
+ if (this.state.is_vol_locked && this.state.is_vol_lock_releasable) { | |
+ var millis_since_vol_locked_marked_releasable = (new Date()).getTime() - this.state.vol_lock_marked_releasable_time; | |
+ if (millis_since_vol_locked_marked_releasable > (App.QUEUE_POLL_INTERVAL_MS + 500)) { | |
+ this.releaseVolMutex(); | |
+ } | |
+ } | |
+ | |
+ if (!this.state.is_vol_locked) { | |
+ this.setState({vol_pct: nextProps.vol_pct}); | |
+ } | |
+ } | |
+ | |
} | |
export default CurrentlyPlayingRight; | |
\ No newline at end of file | |
diff --git a/app/src/component/playlist/playlist.js b/app/src/component/playlist/playlist.js | |
index 3c9ee56..015b21b 100644 | |
--- a/app/src/component/playlist/playlist.js | |
+++ b/app/src/component/playlist/playlist.js | |
@@ -51,6 +51,8 @@ class Playlist extends React.Component { | |
<LoadWithVideo video={this.props.current_video}> | |
<CurrentlyPlaying | |
nextVideo={this.props.nextVideo} | |
+ setVolPct={this.props.setVolPct} | |
+ vol_pct={this.props.vol_pct} | |
clearQueue={this.props.clearQueue} | |
/> | |
</LoadWithVideo> | |
diff --git a/app/src/component/playlist/playlist_expanded.js b/app/src/component/playlist/playlist_expanded.js | |
index fd03fa5..250bf6e 100644 | |
--- a/app/src/component/playlist/playlist_expanded.js | |
+++ b/app/src/component/playlist/playlist_expanded.js | |
@@ -25,6 +25,8 @@ class PlaylistExpanded extends React.Component { | |
<LoadWithVideo video={this.props.current_video}> | |
<CurrentlyPlaying | |
nextVideo={this.props.nextVideo} | |
+ setVolPct={this.props.setVolPct} | |
+ vol_pct={this.props.vol_pct} | |
clearQueue={this.props.clearQueue} | |
/> | |
</LoadWithVideo> | |
diff --git a/lightness/queue.py b/lightness/queue.py | |
index c822f0c..eacc1fd 100644 | |
--- a/lightness/queue.py | |
+++ b/lightness/queue.py | |
@@ -14,6 +14,7 @@ from lightness.videoplayer import VideoPlayer | |
from lightness.videoprocessor import VideoProcessor | |
from lightness.config import Config | |
from lightness.gameoflife import GameOfLife | |
+from lightness.volumecontroller import VolumeController | |
# The Queue is responsible for playing the next video in the Playlist | |
class Queue: | |
@@ -28,7 +29,10 @@ class Queue: | |
self.__config = Config() | |
self.__should_play_game_of_life = self.__config.get_queue_config('should_play_game_of_life', True) | |
self.__logger = Logger().set_namespace(self.__class__.__name__) | |
+ | |
+ # house keeping | |
self.__clear_screen() | |
+ (VolumeController()).set_vol_pct(50) | |
self.__playlist.clean_up_state() | |
def run(self): | |
diff --git a/lightness/videoprocessor.py b/lightness/videoprocessor.py | |
index 1785ae3..e73f5e9 100644 | |
--- a/lightness/videoprocessor.py | |
+++ b/lightness/videoprocessor.py | |
@@ -136,7 +136,6 @@ class VideoProcessor: | |
fps = self.__calculate_fps() | |
ffmpeg_to_python_fifo_name = self.__make_ffmpeg_to_python_fifo() | |
- self.__maybe_set_volume() | |
if self.__maybe_skip_video(): | |
return | |
@@ -368,11 +367,6 @@ class VideoProcessor: | |
self.__logger.info('Calculated video fps: ' + str(fps)) | |
return fps | |
- def __maybe_set_volume(self): | |
- if self.__video_settings.should_play_audio: | |
- self.__logger.info('Setting volume to 100%...') | |
- subprocess.check_output(('amixer', 'cset', 'numid=1', '100%')) | |
- | |
def __make_ffmpeg_to_python_fifo(self): | |
make_fifo_cmd = ( | |
'fifo_name=$(mktemp --tmpdir={} --dry-run {}) && mkfifo -m 600 "$fifo_name" && printf $fifo_name' | |
diff --git a/lightness/volumecontroller.py b/lightness/volumecontroller.py | |
new file mode 100644 | |
index 0000000..89a572e | |
--- /dev/null | |
+++ b/lightness/volumecontroller.py | |
@@ -0,0 +1,54 @@ | |
+import subprocess | |
+import re | |
+import math | |
+ | |
+class VolumeController: | |
+ | |
+ """ | |
+ sudo amixer cset numid=1 96.24% | |
+ numid=1,iface=MIXER,name='PCM Playback Volume' | |
+ ; type=INTEGER,access=rw---R--,values=1,min=-10239,max=400,step=0 | |
+ : values=0 | |
+ | dBscale-min=-102.39dB,step=0.01dB,mute=1 | |
+ | |
+ Setting 96.24% is equivalent to 0dB. Anything higher may result in clipping. | |
+ """ | |
+ __LIMITED_MAX_VOL_VAL = 0 | |
+ | |
+ # amixer output: ; type=INTEGER,access=rw---R--,values=1,min=-10239,max=400,step=0 | |
+ __GLOBAL_MIN_VOL_VAL = -10239 | |
+ __GLOBAL_MAX_VOL_VAL = 400 | |
+ | |
+ # gets a perceptual loudness % | |
+ def get_vol_pct(self): | |
+ res = subprocess.check_output(('amixer', 'cget', 'numid=1')).decode("utf-8") | |
+ m = re.search(" values=(-?\d+)", res, re.MULTILINE) | |
+ if m == None: | |
+ return 0 | |
+ | |
+ vol_val = int(m.group(1)) | |
+ if vol_val == self.__GLOBAL_MIN_VOL_VAL: | |
+ vol_pct = 0 | |
+ else: | |
+ # convert from decibel attenuation amount to perceptual loudness % | |
+ # see: http://www.sengpielaudio.com/calculator-levelchange.htm | |
+ db_level = vol_val / 100 | |
+ vol_pct = math.pow(2, (db_level / 10)) * 100 | |
+ vol_pct = max(0, vol_pct) | |
+ vol_pct = min(100, vol_pct) | |
+ return vol_pct | |
+ | |
+ # takes a perceptual loudness % | |
+ def set_vol_pct(self, vol_pct): | |
+ if (vol_pct <= 0): | |
+ db_level = self.__GLOBAL_MIN_VOL_VAL / 100 | |
+ else: | |
+ # get the decibel adjustment required for the human perceived loudness %. | |
+ # see: http://www.sengpielaudio.com/calculator-levelchange.htm | |
+ db_level = 10 * math.log(vol_pct / 100, 2) | |
+ | |
+ db_level = max(self.__GLOBAL_MIN_VOL_VAL/100, db_level) | |
+ db_level = min(self.__LIMITED_MAX_VOL_VAL, db_level) | |
+ | |
+ pct_to_set = (((db_level * 100) - self.__GLOBAL_MIN_VOL_VAL) / (self.__GLOBAL_MAX_VOL_VAL - self.__GLOBAL_MIN_VOL_VAL)) * 100 | |
+ subprocess.check_output(('amixer', 'cset', 'numid=1', '{}%'.format(pct_to_set))) | |
diff --git a/server b/server | |
index baebac1..b82215a 100755 | |
--- a/server | |
+++ b/server | |
@@ -8,14 +8,17 @@ from lightness.playlist import Playlist | |
from lightness.logger import Logger | |
from lightness.config import Config | |
from lightness.directoryutils import DirectoryUtils | |
+from lightness.volumecontroller import VolumeController | |
class LightnessAPI(): | |
__playlist = Playlist() | |
+ __vol_controller = VolumeController() | |
def get_queue(self): | |
response_details = {} | |
queue = self.__playlist.get_queue() | |
response_details['queue'] = queue | |
+ response_details['vol_pct'] = self.__vol_controller.get_vol_pct() | |
response_details['success'] = True | |
return response_details | |
@@ -49,6 +52,15 @@ class LightnessAPI(): | |
response_details['success'] = True | |
return response_details | |
+ def set_vol_pct(self, post_data): | |
+ vol_pct = int(post_data['vol_pct']) | |
+ self.__vol_controller.set_vol_pct(vol_pct) | |
+ return { | |
+ 'vol_pct': vol_pct, | |
+ 'success': True | |
+ } | |
+ | |
+ | |
class LightnessServerRequestHandler(BaseHTTPRequestHandler): | |
__root_dir = None | |
@@ -121,6 +133,8 @@ class LightnessServerRequestHandler(BaseHTTPRequestHandler): | |
response = self.__api.remove(post_data) | |
elif path == 'clear': | |
response = self.__api.clear() | |
+ elif path == 'vol_pct': | |
+ response = self.__api.set_vol_pct(post_data) | |
else: | |
self.__do_404() | |
return |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment