Skip to content

Commit 0e2ce82

Browse files
committed
fix collaboration sound mutting issue (reported by EthanC112)
1 parent 632d158 commit 0e2ce82

File tree

2 files changed

+76
-8
lines changed

2 files changed

+76
-8
lines changed

src/addons/addons/collaboration/helpers/assetSync.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -432,15 +432,47 @@ export async function syncCurrentCostumeData(isFinalSync = false) {
432432
}
433433

434434
/**
435-
* Generates a SHA-256 hash of a given ArrayBuffer.
435+
* Generates a SHA-256 hash of a given ArrayBuffer or ArrayBufferView.
436436
* This is used to detect changes in binary asset data (costumes, sounds).
437-
* @param {ArrayBuffer} messageBuffer - The binary data to hash.
437+
* @param {ArrayBuffer|ArrayBufferView|Uint8Array} messageBuffer - The binary data to hash.
438438
* @returns {Promise<string|null>} A promise that resolves to the SHA-256 hash as a hex string, or null on error.
439439
*/
440440
async function digestMessage(messageBuffer) {
441-
if (!messageBuffer || messageBuffer.byteLength === 0) return 'empty'; // Handle empty data case.
441+
if (!messageBuffer) return 'empty'; // Handle null/undefined case.
442+
443+
// Convert to ArrayBuffer if it's an ArrayBufferView (like Uint8Array)
444+
let arrayBufferToHash;
445+
if (messageBuffer instanceof ArrayBuffer) {
446+
arrayBufferToHash = messageBuffer;
447+
} else if (messageBuffer.buffer && messageBuffer.buffer instanceof ArrayBuffer) {
448+
// Handle ArrayBufferView types (Uint8Array, etc.)
449+
// If it's a view of the entire buffer, use the buffer directly
450+
// Otherwise, create a new view with just the relevant portion
451+
if (messageBuffer.byteOffset === 0 && messageBuffer.byteLength === messageBuffer.buffer.byteLength) {
452+
arrayBufferToHash = messageBuffer.buffer;
453+
} else {
454+
// Create a new ArrayBuffer with just the view's data
455+
arrayBufferToHash = messageBuffer.buffer.slice(messageBuffer.byteOffset, messageBuffer.byteOffset + messageBuffer.byteLength);
456+
}
457+
} else if (typeof messageBuffer === 'string') {
458+
// Handle string data (base64 encoded or data URL)
459+
console.error('Collab: digestMessage received string data instead of binary. This should not happen.');
460+
return null;
461+
} else {
462+
// Unknown type - try to check if it has byteLength
463+
if (typeof messageBuffer.byteLength === 'number') {
464+
console.error('Collab: digestMessage received unexpected type with byteLength:', typeof messageBuffer, messageBuffer);
465+
return null;
466+
} else {
467+
console.error('Collab: digestMessage received invalid data type:', typeof messageBuffer, messageBuffer);
468+
return null;
469+
}
470+
}
471+
472+
if (arrayBufferToHash.byteLength === 0) return 'empty'; // Handle empty data case.
473+
442474
try {
443-
const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer); // Use Web Cryptography API.
475+
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBufferToHash); // Use Web Cryptography API.
444476
const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert ArrayBuffer to Array of bytes.
445477
// Convert bytes to hex string.
446478
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
@@ -598,7 +630,16 @@ export async function updateSoundProgrammatically(target, soundIndex, soundAsset
598630
if (constants.debugging) console.log(`Collab RX [soundEdited]: AudioBuffer decoded. Calling vm.updateSoundBuffer for sound ${soundIndex} on target "${target.getName()}".`);
599631

600632
// Call the VM method to update the sound's asset, data format, sample rate, and other metadata.
601-
constants.mutableRefs.vm.updateSoundBuffer(soundIndex, audioBuffer, dataFormat, target);
633+
// IMPORTANT: Pass the Uint8Array data as the third parameter to preserve the raw asset data
634+
// for project save/load. The signature matches sound-editor.jsx usage.
635+
// This ensures the sound asset data is properly saved and can be reloaded correctly.
636+
constants.mutableRefs.vm.updateSoundBuffer(soundIndex, audioBuffer, uint8ArrayData);
637+
638+
// Update the sound's dataFormat if it differs (this ensures metadata is correct)
639+
if (soundToUpdate.asset && soundToUpdate.asset.dataFormat !== dataFormat) {
640+
soundToUpdate.asset.dataFormat = dataFormat;
641+
soundToUpdate.dataFormat = dataFormat;
642+
}
602643

603644
// If the updated sound belongs to the currently editing target, trigger UI refreshes.
604645
if (target.id === constants.mutableRefs.vm.runtime.getEditingTarget()?.id) {
@@ -611,7 +652,7 @@ export async function updateSoundProgrammatically(target, soundIndex, soundAsset
611652
if (soundEditor && constants.mutableRefs.vm.editingTarget && constants.mutableRefs.vm.editingTarget.id === target.id) {
612653
const redux = window.ReduxStore; // Assuming Redux store is globally accessible.
613654
// Check if the sound editor is open and the currently edited sound matches the one being updated.
614-
if (redux && redux.getState().scratchGui.soundEditor.soundIndex === soundIndex) {
655+
if (redux && redux.getState().scratchGui.soundEditor && redux.getState().scratchGui.soundEditor.soundIndex === soundIndex) {
615656
if (constants.debugging) console.log(`Collab RX [soundEdited]: Attempting to refresh sound editor UI for target ${target.getName()}, sound ${soundIndex}`);
616657
// Currently, a direct Redux dispatch to force re-render might be needed,
617658
// or rely on a more granular VM event if Scratch GUI supports it.

src/addons/addons/collaboration/userscript.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,35 @@ window.assetLocked = function (assetIndexToCheck, type) {
122122
}
123123

124124
// Set the local editing state in constants.localUserInfo and update awareness.
125-
if (type === 1) assetSync.setLocalEditingCostume(targetNameOfAsset, assetIndexToCheck);
126-
else assetSync.setLocalEditingSound(targetNameOfAsset, assetIndexToCheck);
125+
// For sounds, verify that sounds are loaded before attempting to set editing state.
126+
// If sounds aren't loaded yet, return false (unlocked) to avoid blocking the UI.
127+
if (type === 1) {
128+
assetSync.setLocalEditingCostume(targetNameOfAsset, assetIndexToCheck);
129+
} else {
130+
// For sounds, check if sounds are available first
131+
const target = targetNameOfAsset === 'Stage' ? constants.mutableRefs.vm.runtime.getTargetForStage() : constants.mutableRefs.vm.runtime.getSpriteTargetByName(targetNameOfAsset);
132+
if (target) {
133+
const sounds = target.getSounds();
134+
if (sounds && sounds.length > assetIndexToCheck && sounds[assetIndexToCheck] && sounds[assetIndexToCheck].asset && sounds[assetIndexToCheck].asset.data) {
135+
assetSync.setLocalEditingSound(targetNameOfAsset, assetIndexToCheck);
136+
} else {
137+
// Sounds not loaded yet - defer setting editing state, but don't block UI
138+
if (constants.debugging) {
139+
console.log(`Collab AssetLock: Sounds not fully loaded yet for "${targetNameOfAsset}". Will retry when sound is available.`);
140+
}
141+
// Try again after a short delay to allow sounds to load
142+
setTimeout(() => {
143+
const retryTarget = targetNameOfAsset === 'Stage' ? constants.mutableRefs.vm.runtime.getTargetForStage() : constants.mutableRefs.vm.runtime.getSpriteTargetByName(targetNameOfAsset);
144+
if (retryTarget) {
145+
const retrySounds = retryTarget.getSounds();
146+
if (retrySounds && retrySounds.length > assetIndexToCheck && retrySounds[assetIndexToCheck] && retrySounds[assetIndexToCheck].asset && retrySounds[assetIndexToCheck].asset.data) {
147+
assetSync.setLocalEditingSound(targetNameOfAsset, assetIndexToCheck);
148+
}
149+
}
150+
}, 100);
151+
}
152+
}
153+
}
127154

128155
return false; // Return false to indicate the UI should be enabled.
129156
};

0 commit comments

Comments
 (0)