Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions obs-studio-server/source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <stdlib.h>
#include <mach-o/dyld.h>
#include <string>
#endif

#define BUFFSIZE 512
Expand Down Expand Up @@ -130,6 +134,53 @@ static void Shutdown(void *data, const int64_t id, const std::vector<ipc::value>
}
} // namespace System

#ifdef __APPLE__
static bool HasAppBundleAncestor(const std::string &path)
{
size_t start = 0;
while (start < path.size()) {
size_t end = path.find('/', start);
if (end == std::string::npos)
end = path.size();
std::string_view component(path.data() + start, end - start);
if (component.size() > 4 && component.substr(component.size() - 4) == ".app")
return true;
start = end + 1;
}
return false;
}

static void SetCefMainBundlePath(const char *argv0)
{
uint32_t size = 0;
_NSGetExecutablePath(nullptr, &size);
std::string exePath(size, '\0');
if (_NSGetExecutablePath(exePath.data(), &size) != 0)
return;

char realExe[PATH_MAX];
if (!realpath(exePath.c_str(), realExe))
return;
std::string exeDir(realExe);
if (HasAppBundleAncestor(exeDir))
return;
exeDir.erase(exeDir.rfind('/'));

char realHelper[PATH_MAX];
std::string helperPath = exeDir + "/../Frameworks/obs64 Helper.app";
if (!realpath(helperPath.c_str(), realHelper))
return;
struct stat sb;
if (stat(realHelper, &sb) != 0 || !S_ISDIR(sb.st_mode))
return;

// CEF children rendezvous only if they resolve the same outer bundle as obs64.
std::string bundleArg = std::string("--main-bundle-path=") + realHelper;
const char *args[] = {argv0, bundleArg.c_str()};
obs_set_cmdline_args(2, args);
}
#endif

int main(int argc, char *argv[])
{
#ifdef _WIN32
Expand All @@ -150,6 +201,8 @@ int main(int argc, char *argv[])
int out_err = open(slobsStdErrPath.data(), O_WRONLY | O_CREAT | O_DSYNC);
}

SetCefMainBundlePath(argv[0]);

g_util_osx = new UtilInt();
g_util_osx->init();
#endif
Expand Down
201 changes: 201 additions & 0 deletions tests/osn-tests/src/test_browser_source_load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import 'mocha';
import { expect } from 'chai';
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import { execSync } from 'child_process';
import { AddressInfo } from 'net';
import * as osn from '../osn';
import { logInfo, logEmptyLine } from '../util/logger';
import { OBSHandler } from '../util/obs_handler';
import { deleteConfigFiles, sleep } from '../util/general';

const testName = 'browser-source-load';
const loadTimeoutMs = 30000;

interface IRequestRecord {
path: string;
time: number;
}

const pageHtml = '<!DOCTYPE html><html><body><script src="/app.js"></script></body></html>';
const appJs = "fetch('/report?ok=1');";

describe(testName, () => {
let obs: OBSHandler;
let server: http.Server;
let pageUrl: string;
let requests: IRequestRecord[] = [];
let scene: osn.IScene;
let source: osn.IInput;
let hasTestFailed = false;

const startTime = Date.now();

function count(requestPath: string): number {
return requests.filter((r) => r.path.split('?')[0] === requestPath).length;
}

function describeRequests(): string {
if (requests.length === 0) {
return 'no requests seen';
}
return requests.map((r) => `+${r.time - startTime}ms ${r.path}`).join(', ');
}

async function waitFor(condition: () => boolean, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (condition()) {
return true;
}
await sleep(100);
}
return condition();
}

function runDiagnostics() {
if (process.platform !== 'darwin') {
return;
}
try {
const ps = execSync('ps -axo pid,ppid,command', { encoding: 'utf8' });
const lines = ps.split('\n').filter((line) => line.includes('obs64'));
logInfo(testName, 'obs64 processes:\n' + (lines.join('\n') || '(none)'));
} catch (e) {
logInfo(testName, 'ps failed: ' + e);
}

const frameworkDirs = [
path.join(osn.wd, 'Frameworks'),
path.join(osn.wd, '..', '..', 'Contents', 'Frameworks'),
];
for (const dir of frameworkDirs) {
const resolved = path.resolve(dir);
if (fs.existsSync(resolved)) {
logInfo(testName, `${resolved}:\n` + fs.readdirSync(resolved).join('\n'));
} else {
logInfo(testName, `${resolved} does not exist`);
}
}
}

function failWithStage(stage: string, reached: string): never {
runDiagnostics();
throw new Error(`Browser source did not request ${stage}; reached stage: ${reached}. Requests: ${describeRequests()}`);
}

async function expectLoadSequence(documentCount: number) {
if (!(await waitFor(() => count('/') >= documentCount, loadTimeoutMs))) {
failWithStage('the document', 'none');
}
if (!(await waitFor(() => count('/app.js') >= documentCount, loadTimeoutMs))) {
failWithStage('/app.js', 'document');
}
if (!(await waitFor(() => count('/report') >= documentCount, loadTimeoutMs))) {
failWithStage('/report', 'app.js');
}
}

before(async function () {
logInfo(testName, 'Starting ' + testName + ' tests');

server = http.createServer((req, res) => {
const reqPath = req.url || '';
requests.push({ path: reqPath, time: Date.now() });
const pathname = reqPath.split('?')[0];
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' });
res.end(pageHtml);
} else if (pathname === '/app.js') {
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' });
res.end(appJs);
} else {
res.writeHead(200, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' });
res.end('ok');
}
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
pageUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
logInfo(testName, 'Fixture server listening at ' + pageUrl);

deleteConfigFiles();
obs = new OBSHandler(testName);
});

after(async function () {
try {
if (source) {
source.release();
source = null;
}
if (scene) {
scene.getItems().forEach((item) => item.remove());
osn.Global.setOutputSource(0, null as any);
scene.release();
scene = null;
}

if (obs) {
obs.shutdown();

if (hasTestFailed === true) {
logInfo(testName, 'One or more test cases failed. Uploading cache');
await obs.uploadTestCache();
}
}
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}

obs = null;
deleteConfigFiles();
logInfo(testName, 'Finished ' + testName + ' tests');
logEmptyLine();
});

afterEach(function () {
if (this.currentTest.state == 'failed') {
hasTestFailed = true;
}
});

it('Browser source loads its page and runs its script', async function () {
this.timeout(120000);

// Mocha retries rerun this test without rerunning before(), so create only once.
if (!source) {
scene = osn.SceneFactory.create('browser-load-scene');
source = osn.InputFactory.create('browser_source', 'browser-load-source', {
url: pageUrl,
width: 640,
height: 360,
});
expect(source).to.not.equal(undefined, 'Failed to create browser_source');
scene.add(source);
osn.Global.setOutputSource(0, scene);
}

await expectLoadSequence(1);
});

it('Browser source reloads on refresh', async function () {
this.timeout(120000);
expect(source, 'Browser source was not created by the previous test').to.exist;

const documentsBefore = count('/');
const refreshButton: any = source.properties.get('refreshnocache');
if (refreshButton && typeof refreshButton.buttonClicked === 'function') {
logInfo(testName, 'Refreshing via refreshnocache button');
refreshButton.buttonClicked(source);
} else {
logInfo(testName, 'refreshnocache button not exposed, refreshing by changing url');
source.update({ url: pageUrl + '?refresh=1' });
}

if (!(await waitFor(() => count('/') > documentsBefore, loadTimeoutMs))) {
runDiagnostics();
throw new Error(`Browser source did not request the document again after refresh. Requests: ${describeRequests()}`);
}
});
});
Loading