|
20 | 20 | #include "osn-error.hpp" |
21 | 21 | #include "shared.hpp" |
22 | 22 | #include <osn-video.hpp> |
| 23 | +#include <util/platform.h> |
| 24 | +#include <algorithm> |
| 25 | +#include <mutex> |
| 26 | + |
| 27 | +// Serialises resolve-and-claim against the release in OnOutputStopped(). Start() runs on an IPC |
| 28 | +// worker thread and the release on libobs' capture thread, and the check-then-set has to be atomic |
| 29 | +// as a unit -- the manager's own lock is dropped when for_each returns, so it is not enough. |
| 30 | +static std::mutex s_filenameClaimMutex; |
| 31 | + |
| 32 | +// Windows paths are case-insensitive and accept either separator, so compare on a folded key while |
| 33 | +// the claim itself keeps the exact string the muxer was given. Elsewhere the comparison is exact: |
| 34 | +// on POSIX a backslash is an ordinary filename character, so folding it into a separator would make |
| 35 | +// two different files look like one. |
| 36 | +static std::string claim_key(const std::string &path) |
| 37 | +{ |
| 38 | +#ifdef WIN32 |
| 39 | + std::string key = path; |
| 40 | + std::replace(key.begin(), key.end(), '\\', '/'); |
| 41 | + std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return (char)tolower(c); }); |
| 42 | + return key; |
| 43 | +#else |
| 44 | + return path; |
| 45 | +#endif |
| 46 | +} |
| 47 | + |
| 48 | +// Caller must hold s_filenameClaimMutex. |
| 49 | +static bool path_claimed_by_other(const std::string &candidate, const osn::FileOutput *owner) |
| 50 | +{ |
| 51 | + const std::string wanted = claim_key(candidate); |
| 52 | + bool claimed = false; |
| 53 | + |
| 54 | + osn::IFileOutput::Manager::GetInstance().for_each([&](osn::FileOutput *output) { |
| 55 | + // Idle outputs, replay buffers (which never claim) and GetLegacySettings' throwaway |
| 56 | + // objects all carry an empty claim. |
| 57 | + if (claimed || !output || output == owner || output->claimedFilePath.empty()) |
| 58 | + return; |
| 59 | + |
| 60 | + if (claim_key(output->claimedFilePath) == wanted) |
| 61 | + claimed = true; |
| 62 | + }); |
| 63 | + |
| 64 | + return claimed; |
| 65 | +} |
| 66 | + |
| 67 | +void osn::IFileOutput::FindBestFilename(std::string &strPath, bool noSpace, FileOutput *owner, bool allowOverwrite) |
| 68 | +{ |
| 69 | + std::lock_guard<std::mutex> lock(s_filenameClaimMutex); |
| 70 | + |
| 71 | + // Insert before the extension, or at the very end when there is none. Only the final component |
| 72 | + // counts: a dot in a directory name is not an extension. An extensionless name is reachable -- |
| 73 | + // osn_generate_formatted_filename() appends the extension and then truncates the whole thing to |
| 74 | + // 255 bytes -- and giving up there would hand two live outputs the same path. |
| 75 | + const size_t sep = strPath.find_last_of("/\\"); |
| 76 | + const size_t nameStart = (sep == std::string::npos) ? 0 : sep + 1; |
| 77 | + const size_t dot = strPath.find_last_of('.'); |
| 78 | + const size_t insertAt = (dot != std::string::npos && dot > nameStart) ? dot : strPath.size(); |
| 79 | + |
| 80 | + std::string candidate = strPath; |
| 81 | + int num = 2; |
| 82 | + bool blockedByLiveOutput = false; |
| 83 | + |
| 84 | + for (;;) { |
| 85 | + const bool onDisk = !allowOverwrite && os_file_exists(candidate.c_str()); |
| 86 | + const bool heldByPeer = path_claimed_by_other(candidate, owner); |
| 87 | + |
| 88 | + if (!onDisk && !heldByPeer) |
| 89 | + break; |
| 90 | + if (heldByPeer) |
| 91 | + blockedByLiveOutput = true; |
| 92 | + |
| 93 | + std::string numStr = noSpace ? "_" : " ("; |
| 94 | + numStr += std::to_string(num++); |
| 95 | + if (!noSpace) |
| 96 | + numStr += ")"; |
| 97 | + |
| 98 | + candidate = strPath; |
| 99 | + candidate.insert(insertAt, numStr); |
| 100 | + } |
| 101 | + |
| 102 | + // Stepping over a file left on disk is ordinary and stays quiet. Stepping over a path another |
| 103 | + // running output holds is not: it means a client pointed two outputs at one file -- dual output |
| 104 | + // without distinct prefix/suffix, most likely -- and got a name it did not ask for. |
| 105 | + if (blockedByLiveOutput) |
| 106 | + blog(LOG_WARNING, "Recording path '%s' is in use by another active output, writing to '%s' instead.", strPath.c_str(), candidate.c_str()); |
| 107 | + |
| 108 | + strPath = candidate; |
| 109 | + |
| 110 | + // Never move the claim of an output that is already running. A duplicate start() resolves a |
| 111 | + // fresh name -- the timestamp has moved on -- but obs_output_start() then refuses and the muxer |
| 112 | + // stays on its original file. Reassigning here would unclaim the file actually being written. |
| 113 | + if (owner && !obs_output_active(owner->GetOutput())) |
| 114 | + owner->claimedFilePath = strPath; |
| 115 | +} |
| 116 | + |
| 117 | +void osn::FileOutput::OnOutputStopped() |
| 118 | +{ |
| 119 | + std::lock_guard<std::mutex> lock(s_filenameClaimMutex); |
| 120 | + claimedFilePath.clear(); |
| 121 | +} |
23 | 122 |
|
24 | 123 | void osn::IFileOutput::Register(ipc::server &srv) |
25 | 124 | { |
|
0 commit comments