diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js --- a/src/resources/js/meet/app.js +++ b/src/resources/js/meet/app.js @@ -1009,6 +1009,7 @@ + svgIcon('user', 'fas', 'watermark') + '
' + '' + + '' + '' + '' + '' @@ -1034,12 +1035,52 @@ if (params.isSelf) { wrapper.find('.link-setup').removeClass('hidden').click(() => sessionData.onMediaSetup()) } else { - // Enable audio mute button - wrapper.find('.link-audio').removeClass('hidden') + let volumeInput = wrapper.find('.volume input') + let audioButton = wrapper.find('.link-audio') + let inVolume = false + let hideVolumeTimeout + let hideVolume = () => { + if (inVolume) { + hideVolumeTimeout = setTimeout(hideVolume, 1000) + } else { + volumeInput.parent().addClass('hidden') + } + } + + // Enable and set up the audio mute button + audioButton.removeClass('hidden') .on('click', e => { let video = wrapper.find('video')[0] + video.muted = !video.muted - wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger') + video.volume = video.muted ? 0 : 1 + + audioButton[video.muted ? 'addClass' : 'removeClass']('text-danger') + volumeInput.val(video.volume) + }) + // Show the volume slider when mouse is over the audio mute/unmute button + .on('mouseenter', () => { + let video = wrapper.find('video')[0] + + clearTimeout(hideVolumeTimeout) + volumeInput.parent().removeClass('hidden') + volumeInput.val(video.volume) + }) + .on('mouseleave', () => { + hideVolumeTimeout = setTimeout(hideVolume, 1000) + }) + + // Set up the audio volume control + volumeInput + .on('mouseenter', () => { inVolume = true }) + .on('mouseleave', () => { inVolume = false }) + .on('change input', () => { + let video = wrapper.find('video')[0] + let volume = volumeInput.val() + + video.volume = volume + video.muted = volume == 0 + audioButton[video.muted ? 'addClass' : 'removeClass']('text-danger') }) } diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss --- a/src/resources/themes/meet.scss +++ b/src/resources/themes/meet.scss @@ -1,3 +1,19 @@ +@mixin range-track() { + background-color: $link-color !important; + height: 0.2em; + border: 0; + border-radius: 0.1em; +} + +@mixin range-thumb() { + appearance: none; + background-color: $link-color; + width: 0.75em; + height: 0.75em; + border: none; + border-radius: 0.5em; +} + .meet-nickname { padding: 0; line-height: 2em; @@ -142,6 +158,39 @@ display: none; } } + + .volume { + position: absolute; + bottom: 2.15em; + right: 2em; + border-radius: 1em; + background: rgba(#000, 0.7); + width: 2em; + height: 4em; + overflow: hidden; // for Edge + + input { + appearance: none; + cursor: pointer; + width: 3em; + height: 2em; + margin-top: 3.5em; + transform-origin: 0 0; + transform: rotate(-90deg); + background: transparent; + outline: 0; + + &::-ms-track { @include range-track; } + &::-moz-range-track { @include range-track; } + &::-webkit-slider-runnable-track { @include range-track; } + + &::-ms-thumb { @include range-thumb; } + &::-moz-range-thumb { @include range-thumb; } + &::-webkit-slider-thumb { @include range-thumb; margin-top: -0.25em; } + + &::-ms-tooltip { display: none; } + } + } } #meet-component { diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php --- a/src/tests/Browser/Meet/RoomControlsTest.php +++ b/src/tests/Browser/Meet/RoomControlsTest.php @@ -74,7 +74,7 @@ } /** - * Test nickname and muting audio/video + * Test nickname and audio/video muting/volume controls * * @group openvidu */ @@ -221,6 +221,40 @@ ->assertAudioMuted('video', false) ->assertVisible('.controls button.link-audio:not(.text-danger)'); }); + + // Test volume control + $guest->mouseover('@menu') + ->with('div.meet-video:not(.self)', function (Browser $browser) { + $browser->waitUntilMissing('.volume') + ->mouseover('.controls button.link-audio') + ->waitFor('.volume') + ->assertValue('.volume input', '1') + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->keys('.volume input', ['{arrow_down}']) + ->assertValue('.volume input', '0') + ->assertAudioMuted('video', true) + ->assertVisible('.controls button.link-audio.text-danger') + ->click('.controls button.link-audio') + ->assertAudioMuted('video', false) + ->assertValue('.volume input', '1') + ->click('.controls button.link-audio') + ->assertAudioMuted('video', true) + ->assertValue('.volume input', '0') + ->keys('.volume input', ['{arrow_up}']) + ->assertValue('.volume input', '0.1') + ->assertAudioMuted('video', false) + ->assertVisible('.controls button.link-audio:not(.text-danger)') + ->mouseover('.meet-nickname') + ->waitUntilMissing('.volume'); + }); }); }