0

Add option to hide viewer count. Closes #1939

This commit is contained in:
Gabe Kangas 2022-06-26 00:46:55 -07:00
parent 97db93e0d7
commit b08393295f
No known key found for this signature in database
GPG Key ID: 9A56337728BC81EA
11 changed files with 209 additions and 125 deletions

View File

@ -711,6 +711,26 @@ func SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
controllers.WriteSimpleResponse(w, true, "chat join message status updated") controllers.WriteSimpleResponse(w, true, "chat join message status updated")
} }
// SetHideViewerCount will enable or disable hiding the viewer count.
func SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update hiding viewer count")
return
}
if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "hide viewer count setting updated")
}
func requirePOST(w http.ResponseWriter, r *http.Request) bool { func requirePOST(w http.ResponseWriter, r *http.Request) bool {
if r.Method != controllers.POST { if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported") controllers.WriteSimpleResponse(w, false, r.Method+" not supported")

View File

@ -55,6 +55,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(), ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(), SocketHostOverride: data.GetWebsocketOverrideHost(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
HideViewerCount: data.GetHideViewerCount(),
VideoSettings: videoSettings{ VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants, VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level, LatencyLevel: data.GetStreamLatencyLevel().Level,
@ -113,6 +114,7 @@ type serverConfigAdminResponse struct {
SuggestedUsernames []string `json:"suggestedUsernames"` SuggestedUsernames []string `json:"suggestedUsernames"`
SocketHostOverride string `json:"socketHostOverride,omitempty"` SocketHostOverride string `json:"socketHostOverride,omitempty"`
Notifications notificationsConfigResponse `json:"notifications"` Notifications notificationsConfigResponse `json:"notifications"`
HideViewerCount bool `json:"hideViewerCount"`
} }
type videoSettings struct { type videoSettings struct {

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/owncast/owncast/core" "github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
) )
@ -15,7 +16,6 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
status := core.GetStatus() status := core.GetStatus()
response := webStatusResponse{ response := webStatusResponse{
Online: status.Online, Online: status.Online,
ViewerCount: status.ViewerCount,
ServerTime: time.Now(), ServerTime: time.Now(),
LastConnectTime: status.LastConnectTime, LastConnectTime: status.LastConnectTime,
LastDisconnectTime: status.LastDisconnectTime, LastDisconnectTime: status.LastDisconnectTime,
@ -23,6 +23,10 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
StreamTitle: status.StreamTitle, StreamTitle: status.StreamTitle,
} }
if !data.GetHideViewerCount() {
response.ViewerCount = status.ViewerCount
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w) middleware.DisableCache(w)
@ -33,7 +37,7 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
type webStatusResponse struct { type webStatusResponse struct {
Online bool `json:"online"` Online bool `json:"online"`
ViewerCount int `json:"viewerCount"` ViewerCount int `json:"viewerCount,omitempty"`
ServerTime time.Time `json:"serverTime"` ServerTime time.Time `json:"serverTime"`
LastConnectTime *utils.NullTime `json:"lastConnectTime"` LastConnectTime *utils.NullTime `json:"lastConnectTime"`
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`

View File

@ -65,6 +65,7 @@ const (
browserPushPrivateKeyKey = "browser_push_private_key" browserPushPrivateKeyKey = "browser_push_private_key"
twitterConfigurationKey = "twitter_configuration" twitterConfigurationKey = "twitter_configuration"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications" hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count"
) )
// GetExtraPageBodyContent will return the user-supplied body content. // GetExtraPageBodyContent will return the user-supplied body content.
@ -908,3 +909,14 @@ func GetHasPerformedInitialNotificationsConfig() bool {
configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey) configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey)
return configured return configured
} }
// GetHideViewerCount will return if the viewer count shold be hidden.
func GetHideViewerCount() bool {
hide, _ := _datastore.GetBool(hideViewerCountKey)
return hide
}
// SetHideViewerCount will set if the viewer count should be hidden.
func SetHideViewerCount(hide bool) error {
return _datastore.SetBool(hideViewerCountKey, hide)
}

View File

@ -312,6 +312,9 @@ func Start() error {
// Video playback metrics // Video playback metrics
http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics))
// Is the viewer count hidden from viewers
http.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount))
// Inline chat moderation actions // Inline chat moderation actions
// Update chat message visibility // Update chat message visibility

View File

@ -9,192 +9,211 @@ const tags = [randomString(), randomString(), randomString()];
const latencyLevel = Math.floor(Math.random() * 4); const latencyLevel = Math.floor(Math.random() * 4);
const streamOutputVariants = { const streamOutputVariants = {
videoBitrate: randomNumber() * 100, videoBitrate: randomNumber() * 100,
framerate: 42, framerate: 42,
cpuUsageLevel: 2, cpuUsageLevel: 2,
scaledHeight: randomNumber() * 100, scaledHeight: randomNumber() * 100,
scaledWidth: randomNumber() * 100, scaledWidth: randomNumber() * 100,
}; };
const socialHandles = [ const socialHandles = [
{ {
url: 'http://facebook.org/' + randomString(), url: 'http://facebook.org/' + randomString(),
platform: randomString(), platform: randomString(),
}, },
]; ];
const s3Config = { const s3Config = {
enabled: true, enabled: true,
endpoint: 'http://' + randomString(), endpoint: 'http://' + randomString(),
accessKey: randomString(), accessKey: randomString(),
secret: randomString(), secret: randomString(),
bucket: randomString(), bucket: randomString(),
region: randomString(), region: randomString(),
forcePathStyle: true, forcePathStyle: true,
}; };
const forbiddenUsernames = [randomString(), randomString(), randomString()]; const forbiddenUsernames = [randomString(), randomString(), randomString()];
test('set server name', async (done) => { test('set server name', async (done) => {
const res = await sendConfigChangeRequest('name', serverName); const res = await sendConfigChangeRequest('name', serverName);
done(); done();
}); });
test('set stream title', async (done) => { test('set stream title', async (done) => {
const res = await sendConfigChangeRequest('streamtitle', streamTitle); const res = await sendConfigChangeRequest('streamtitle', streamTitle);
done(); done();
}); });
test('set server summary', async (done) => { test('set server summary', async (done) => {
const res = await sendConfigChangeRequest('serversummary', serverSummary); const res = await sendConfigChangeRequest('serversummary', serverSummary);
done(); done();
}); });
test('set extra page content', async (done) => { test('set extra page content', async (done) => {
const res = await sendConfigChangeRequest('pagecontent', pageContent); const res = await sendConfigChangeRequest('pagecontent', pageContent);
done(); done();
}); });
test('set tags', async (done) => { test('set tags', async (done) => {
const res = await sendConfigChangeRequest('tags', tags); const res = await sendConfigChangeRequest('tags', tags);
done(); done();
}); });
test('set latency level', async (done) => { test('set latency level', async (done) => {
const res = await sendConfigChangeRequest( const res = await sendConfigChangeRequest(
'video/streamlatencylevel', 'video/streamlatencylevel',
latencyLevel latencyLevel
); );
done(); done();
}); });
test('set video stream output variants', async (done) => { test('set video stream output variants', async (done) => {
const res = await sendConfigChangeRequest('video/streamoutputvariants', [ const res = await sendConfigChangeRequest('video/streamoutputvariants', [
streamOutputVariants, streamOutputVariants,
]); ]);
done(); done();
}); });
test('set social handles', async (done) => { test('set social handles', async (done) => {
const res = await sendConfigChangeRequest('socialhandles', socialHandles); const res = await sendConfigChangeRequest('socialhandles', socialHandles);
done(); done();
}); });
test('set s3 configuration', async (done) => { test('set s3 configuration', async (done) => {
const res = await sendConfigChangeRequest('s3', s3Config); const res = await sendConfigChangeRequest('s3', s3Config);
done(); done();
}); });
test('set forbidden usernames', async (done) => { test('set forbidden usernames', async (done) => {
const res = await sendConfigChangeRequest('chat/forbiddenusernames', forbiddenUsernames); const res = await sendConfigChangeRequest(
done(); 'chat/forbiddenusernames',
forbiddenUsernames
);
done();
});
test('set hide viewer count', async (done) => {
const res = await sendConfigChangeRequest('hideviewercount', true);
done();
}); });
test('verify updated config values', async (done) => { test('verify updated config values', async (done) => {
const res = await request.get('/api/config'); const res = await request.get('/api/config');
expect(res.body.name).toBe(serverName); expect(res.body.name).toBe(serverName);
expect(res.body.streamTitle).toBe(streamTitle); expect(res.body.streamTitle).toBe(streamTitle);
expect(res.body.summary).toBe(`<p>${serverSummary}</p>`); expect(res.body.summary).toBe(`<p>${serverSummary}</p>`);
expect(res.body.extraPageContent).toBe(pageContent); expect(res.body.extraPageContent).toBe(pageContent);
expect(res.body.logo).toBe('/logo'); expect(res.body.logo).toBe('/logo');
expect(res.body.socialHandles).toStrictEqual(socialHandles); expect(res.body.socialHandles).toStrictEqual(socialHandles);
done(); done();
}); });
// Test that the raw video details being broadcasted are coming through // Test that the raw video details being broadcasted are coming through
test('stream details are correct', (done) => { test('admin stream details are correct', (done) => {
request request
.get('/api/admin/status') .get('/api/admin/status')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.expect(200) .expect(200)
.then((res) => { .then((res) => {
expect(res.body.broadcaster.streamDetails.width).toBe(320); expect(res.body.broadcaster.streamDetails.width).toBe(320);
expect(res.body.broadcaster.streamDetails.height).toBe(180); expect(res.body.broadcaster.streamDetails.height).toBe(180);
expect(res.body.broadcaster.streamDetails.framerate).toBe(24); expect(res.body.broadcaster.streamDetails.framerate).toBe(24);
expect(res.body.broadcaster.streamDetails.videoBitrate).toBe(1269); expect(res.body.broadcaster.streamDetails.videoBitrate).toBe(1269);
expect(res.body.broadcaster.streamDetails.videoCodec).toBe('H.264'); expect(res.body.broadcaster.streamDetails.videoCodec).toBe('H.264');
expect(res.body.broadcaster.streamDetails.audioCodec).toBe('AAC'); expect(res.body.broadcaster.streamDetails.audioCodec).toBe('AAC');
expect(res.body.online).toBe(true); expect(res.body.online).toBe(true);
done(); done();
}); });
}); });
test('admin configuration is correct', (done) => { test('admin configuration is correct', (done) => {
request request
.get('/api/admin/serverconfig') .get('/api/admin/serverconfig')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.expect(200) .expect(200)
.then((res) => { .then((res) => {
expect(res.body.instanceDetails.name).toBe(serverName); expect(res.body.instanceDetails.name).toBe(serverName);
expect(res.body.instanceDetails.summary).toBe(serverSummary); expect(res.body.instanceDetails.summary).toBe(serverSummary);
expect(res.body.instanceDetails.tags).toStrictEqual(tags); expect(res.body.instanceDetails.tags).toStrictEqual(tags);
expect(res.body.instanceDetails.socialHandles).toStrictEqual( expect(res.body.instanceDetails.socialHandles).toStrictEqual(
socialHandles socialHandles
); );
expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames); expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames);
expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel); expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel);
expect(res.body.videoSettings.videoQualityVariants[0].framerate).toBe( expect(res.body.videoSettings.videoQualityVariants[0].framerate).toBe(
streamOutputVariants.framerate streamOutputVariants.framerate
); );
expect(res.body.videoSettings.videoQualityVariants[0].cpuUsageLevel).toBe( expect(res.body.videoSettings.videoQualityVariants[0].cpuUsageLevel).toBe(
streamOutputVariants.cpuUsageLevel streamOutputVariants.cpuUsageLevel
); );
expect(res.body.yp.enabled).toBe(false); expect(res.body.yp.enabled).toBe(false);
expect(res.body.streamKey).toBe('abc123'); expect(res.body.streamKey).toBe('abc123');
expect(res.body.s3.enabled).toBe(s3Config.enabled); expect(res.body.s3.enabled).toBe(s3Config.enabled);
expect(res.body.s3.endpoint).toBe(s3Config.endpoint); expect(res.body.s3.endpoint).toBe(s3Config.endpoint);
expect(res.body.s3.accessKey).toBe(s3Config.accessKey); expect(res.body.s3.accessKey).toBe(s3Config.accessKey);
expect(res.body.s3.secret).toBe(s3Config.secret); expect(res.body.s3.secret).toBe(s3Config.secret);
expect(res.body.s3.bucket).toBe(s3Config.bucket); expect(res.body.s3.bucket).toBe(s3Config.bucket);
expect(res.body.s3.region).toBe(s3Config.region); expect(res.body.s3.region).toBe(s3Config.region);
expect(res.body.s3.forcePathStyle).toBeTruthy(); expect(res.body.s3.forcePathStyle).toBe(true);
done(); expect(res.body.hideViewerCount).toBe(true);
}); done();
});
}); });
test('frontend configuration is correct', (done) => { test('frontend configuration is correct', (done) => {
request request
.get('/api/config') .get('/api/config')
.expect(200) .expect(200)
.then((res) => { .then((res) => {
expect(res.body.name).toBe(serverName); expect(res.body.name).toBe(serverName);
expect(res.body.logo).toBe('/logo'); expect(res.body.logo).toBe('/logo');
expect(res.body.socialHandles).toStrictEqual(socialHandles); expect(res.body.socialHandles).toStrictEqual(socialHandles);
done(); done();
}); });
});
test('frontend status is correct', (done) => {
request
.get('/api/status')
.expect(200)
.then((res) => {
expect(res.body.viewerCount).toBe(undefined);
done();
});
}); });
async function sendConfigChangeRequest(endpoint, value) { async function sendConfigChangeRequest(endpoint, value) {
const url = '/api/admin/config/' + endpoint; const url = '/api/admin/config/' + endpoint;
const res = await request const res = await request
.post(url) .post(url)
.auth('admin', 'abc123') .auth('admin', 'abc123')
.send({ value: value }) .send({ value: value })
.expect(200); .expect(200);
expect(res.body.success).toBe(true); expect(res.body.success).toBe(true);
return res; return res;
} }
async function sendConfigChangePayload(endpoint, payload) { async function sendConfigChangePayload(endpoint, payload) {
const url = '/api/admin/config/' + endpoint; const url = '/api/admin/config/' + endpoint;
const res = await request const res = await request
.post(url) .post(url)
.auth('admin', 'abc123') .auth('admin', 'abc123')
.send(payload) .send(payload)
.expect(200); .expect(200);
expect(res.body.success).toBe(true); expect(res.body.success).toBe(true);
return res; return res;
} }
function randomString(length = 20) { function randomString(length = 20) {
return Math.random().toString(16).substr(2, length); return Math.random().toString(16).substr(2, length);
} }
function randomNumber() { function randomNumber() {
return Math.floor(Math.random() * 5); return Math.floor(Math.random() * 5);
} }

View File

@ -15,6 +15,7 @@ import {
API_YP_SWITCH, API_YP_SWITCH,
FIELD_PROPS_YP, FIELD_PROPS_YP,
FIELD_PROPS_NSFW, FIELD_PROPS_NSFW,
FIELD_PROPS_HIDE_VIEWER_COUNT,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
@ -61,6 +62,10 @@ export default function EditInstanceDetails() {
}); });
}; };
function handleHideViewerCountChange(enabled: boolean) {
handleFieldChange({ fieldName: 'hideViewerCount', value: enabled });
}
const hasInstanceUrl = instanceUrl !== ''; const hasInstanceUrl = instanceUrl !== '';
return ( return (
@ -100,6 +105,14 @@ export default function EditInstanceDetails() {
{/* Logo section */} {/* Logo section */}
<EditLogo /> <EditLogo />
<ToggleSwitch
fieldName="hideViewerCount"
useSubmit
{...FIELD_PROPS_HIDE_VIEWER_COUNT}
checked={formDataValues.hideViewerCount}
onChange={handleHideViewerCountChange}
/>
<br /> <br />
<p className="description"> <p className="description">
Increase your audience by appearing in the{' '} Increase your audience by appearing in the{' '}

View File

@ -40,12 +40,12 @@ export default function Statusbar(props: Props) {
if (online && lastConnectTime) { if (online && lastConnectTime) {
const duration = makeDurationString(new Date(lastConnectTime)); const duration = makeDurationString(new Date(lastConnectTime));
onlineMessage = online ? `Live for ${duration}` : 'Offline'; onlineMessage = online ? `Live for ${duration}` : 'Offline';
rightSideMessage = ( rightSideMessage = viewerCount > 0 && (
<span> <span>
<EyeOutlined /> {viewerCount} <EyeOutlined /> {viewerCount}
</span> </span>
); );
} else { } else if (!online) {
onlineMessage = 'Offline'; onlineMessage = 'Offline';
if (lastDisconnectTime) { if (lastDisconnectTime) {
rightSideMessage = `Last live ${formatDistanceToNow(new Date(lastDisconnectTime))} ago.`; rightSideMessage = `Last live ${formatDistanceToNow(new Date(lastDisconnectTime))} ago.`;

View File

@ -152,4 +152,5 @@ export interface ConfigDetails {
notifications: NotificationsConfig; notifications: NotificationsConfig;
chatJoinMessagesEnabled: boolean; chatJoinMessagesEnabled: boolean;
chatEstablishedUserMode: boolean; chatEstablishedUserMode: boolean;
hideViewerCount: boolean;
} }

View File

@ -29,6 +29,7 @@ export const API_VIDEO_SEGMENTS = '/video/streamlatencylevel';
export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
export const API_WEB_PORT = '/webserverport'; export const API_WEB_PORT = '/webserverport';
export const API_YP_SWITCH = '/directoryenabled'; export const API_YP_SWITCH = '/directoryenabled';
export const API_HIDE_VIEWER_COUNT = '/hideviewercount';
export const API_CHAT_DISABLE = '/chat/disable'; export const API_CHAT_DISABLE = '/chat/disable';
export const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled'; export const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled';
export const API_CHAT_ESTABLISHED_MODE = '/chat/establishedusermode'; export const API_CHAT_ESTABLISHED_MODE = '/chat/establishedusermode';
@ -188,6 +189,13 @@ export const FIELD_PROPS_YP = {
tip: 'Turn this ON to request to show up in the directory.', tip: 'Turn this ON to request to show up in the directory.',
}; };
export const FIELD_PROPS_HIDE_VIEWER_COUNT = {
apiPath: API_HIDE_VIEWER_COUNT,
configPath: '',
label: 'Hide viewer count',
tip: 'Turn this ON to hide the viewer count the web page.',
};
export const DEFAULT_VARIANT_STATE: VideoVariant = { export const DEFAULT_VARIANT_STATE: VideoVariant = {
framerate: 24, framerate: 24,
videoPassthrough: false, videoPassthrough: false,

View File

@ -75,6 +75,7 @@ export const initialServerConfigState: ConfigDetails = {
chatDisabled: false, chatDisabled: false,
chatJoinMessagesEnabled: true, chatJoinMessagesEnabled: true,
chatEstablishedUserMode: false, chatEstablishedUserMode: false,
hideViewerCount: false,
}; };
const initialServerStatusState = { const initialServerStatusState = {
@ -156,6 +157,7 @@ const ServerStatusProvider = ({ children }) => {
}; };
}, []); }, []);
// eslint-disable-next-line react/jsx-no-constructed-context-values
const providerValue = { const providerValue = {
...status, ...status,
serverConfig: config, serverConfig: config,