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")
}
// 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 {
if r.Method != controllers.POST {
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(),
SocketHostOverride: data.GetWebsocketOverrideHost(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
HideViewerCount: data.GetHideViewerCount(),
VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level,
@ -113,6 +114,7 @@ type serverConfigAdminResponse struct {
SuggestedUsernames []string `json:"suggestedUsernames"`
SocketHostOverride string `json:"socketHostOverride,omitempty"`
Notifications notificationsConfigResponse `json:"notifications"`
HideViewerCount bool `json:"hideViewerCount"`
}
type videoSettings struct {

View file

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

View file

@ -65,6 +65,7 @@ const (
browserPushPrivateKeyKey = "browser_push_private_key"
twitterConfigurationKey = "twitter_configuration"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count"
)
// GetExtraPageBodyContent will return the user-supplied body content.
@ -908,3 +909,14 @@ func GetHasPerformedInitialNotificationsConfig() bool {
configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey)
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
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
// Update chat message visibility

View file

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

View file

@ -15,6 +15,7 @@ import {
API_YP_SWITCH,
FIELD_PROPS_YP,
FIELD_PROPS_NSFW,
FIELD_PROPS_HIDE_VIEWER_COUNT,
} from '../../utils/config-constants';
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 !== '';
return (
@ -100,6 +105,14 @@ export default function EditInstanceDetails() {
{/* Logo section */}
<EditLogo />
<ToggleSwitch
fieldName="hideViewerCount"
useSubmit
{...FIELD_PROPS_HIDE_VIEWER_COUNT}
checked={formDataValues.hideViewerCount}
onChange={handleHideViewerCountChange}
/>
<br />
<p className="description">
Increase your audience by appearing in the{' '}

View file

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

View file

@ -152,4 +152,5 @@ export interface ConfigDetails {
notifications: NotificationsConfig;
chatJoinMessagesEnabled: 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_WEB_PORT = '/webserverport';
export const API_YP_SWITCH = '/directoryenabled';
export const API_HIDE_VIEWER_COUNT = '/hideviewercount';
export const API_CHAT_DISABLE = '/chat/disable';
export const API_CHAT_JOIN_MESSAGES_ENABLED = '/chat/joinmessagesenabled';
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.',
};
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 = {
framerate: 24,
videoPassthrough: false,

View file

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